about summary refs log tree commit diff
path: root/docs/tool/Modules/NaturalDocs
diff options
context:
space:
mode:
Diffstat (limited to 'docs/tool/Modules/NaturalDocs')
-rw-r--r--docs/tool/Modules/NaturalDocs/BinaryFile.pm294
-rw-r--r--docs/tool/Modules/NaturalDocs/Builder.pm280
-rw-r--r--docs/tool/Modules/NaturalDocs/Builder/Base.pm348
-rw-r--r--docs/tool/Modules/NaturalDocs/Builder/FramedHTML.pm345
-rw-r--r--docs/tool/Modules/NaturalDocs/Builder/HTML.pm398
-rw-r--r--docs/tool/Modules/NaturalDocs/Builder/HTMLBase.pm3693
-rw-r--r--docs/tool/Modules/NaturalDocs/ClassHierarchy.pm860
-rw-r--r--docs/tool/Modules/NaturalDocs/ClassHierarchy/Class.pm412
-rw-r--r--docs/tool/Modules/NaturalDocs/ClassHierarchy/File.pm157
-rw-r--r--docs/tool/Modules/NaturalDocs/ConfigFile.pm497
-rw-r--r--docs/tool/Modules/NaturalDocs/Constants.pm165
-rw-r--r--docs/tool/Modules/NaturalDocs/DefineMembers.pm100
-rw-r--r--docs/tool/Modules/NaturalDocs/Error.pm305
-rw-r--r--docs/tool/Modules/NaturalDocs/File.pm540
-rw-r--r--docs/tool/Modules/NaturalDocs/ImageReferenceTable.pm383
-rw-r--r--docs/tool/Modules/NaturalDocs/ImageReferenceTable/Reference.pm44
-rw-r--r--docs/tool/Modules/NaturalDocs/ImageReferenceTable/String.pm110
-rw-r--r--docs/tool/Modules/NaturalDocs/Languages.pm1475
-rw-r--r--docs/tool/Modules/NaturalDocs/Languages/ActionScript.pm1473
-rw-r--r--docs/tool/Modules/NaturalDocs/Languages/Ada.pm38
-rw-r--r--docs/tool/Modules/NaturalDocs/Languages/Advanced.pm828
-rw-r--r--docs/tool/Modules/NaturalDocs/Languages/Advanced/Scope.pm95
-rw-r--r--docs/tool/Modules/NaturalDocs/Languages/Advanced/ScopeChange.pm70
-rw-r--r--docs/tool/Modules/NaturalDocs/Languages/Base.pm832
-rw-r--r--docs/tool/Modules/NaturalDocs/Languages/CSharp.pm1484
-rw-r--r--docs/tool/Modules/NaturalDocs/Languages/PLSQL.pm319
-rw-r--r--docs/tool/Modules/NaturalDocs/Languages/Pascal.pm143
-rw-r--r--docs/tool/Modules/NaturalDocs/Languages/Perl.pm1370
-rw-r--r--docs/tool/Modules/NaturalDocs/Languages/Prototype.pm92
-rw-r--r--docs/tool/Modules/NaturalDocs/Languages/Prototype/Parameter.pm87
-rw-r--r--docs/tool/Modules/NaturalDocs/Languages/Simple.pm503
-rw-r--r--docs/tool/Modules/NaturalDocs/Languages/Tcl.pm219
-rw-r--r--docs/tool/Modules/NaturalDocs/Menu.pm3406
-rw-r--r--docs/tool/Modules/NaturalDocs/Menu/Entry.pm201
-rw-r--r--docs/tool/Modules/NaturalDocs/NDMarkup.pm76
-rw-r--r--docs/tool/Modules/NaturalDocs/Parser.pm1331
-rw-r--r--docs/tool/Modules/NaturalDocs/Parser/JavaDoc.pm464
-rw-r--r--docs/tool/Modules/NaturalDocs/Parser/Native.pm1060
-rw-r--r--docs/tool/Modules/NaturalDocs/Parser/ParsedTopic.pm253
-rw-r--r--docs/tool/Modules/NaturalDocs/Project.pm1402
-rw-r--r--docs/tool/Modules/NaturalDocs/Project/ImageFile.pm160
-rw-r--r--docs/tool/Modules/NaturalDocs/Project/SourceFile.pm113
-rw-r--r--docs/tool/Modules/NaturalDocs/ReferenceString.pm334
-rw-r--r--docs/tool/Modules/NaturalDocs/Settings.pm1418
-rw-r--r--docs/tool/Modules/NaturalDocs/Settings/BuildTarget.pm66
-rw-r--r--docs/tool/Modules/NaturalDocs/SourceDB.pm678
-rw-r--r--docs/tool/Modules/NaturalDocs/SourceDB/Extension.pm84
-rw-r--r--docs/tool/Modules/NaturalDocs/SourceDB/File.pm129
-rw-r--r--docs/tool/Modules/NaturalDocs/SourceDB/Item.pm201
-rw-r--r--docs/tool/Modules/NaturalDocs/SourceDB/ItemDefinition.pm45
-rw-r--r--docs/tool/Modules/NaturalDocs/SourceDB/WatchedFileDefinitions.pm159
-rw-r--r--docs/tool/Modules/NaturalDocs/StatusMessage.pm102
-rw-r--r--docs/tool/Modules/NaturalDocs/SymbolString.pm212
-rw-r--r--docs/tool/Modules/NaturalDocs/SymbolTable.pm1984
-rw-r--r--docs/tool/Modules/NaturalDocs/SymbolTable/File.pm186
-rw-r--r--docs/tool/Modules/NaturalDocs/SymbolTable/IndexElement.pm522
-rw-r--r--docs/tool/Modules/NaturalDocs/SymbolTable/Reference.pm273
-rw-r--r--docs/tool/Modules/NaturalDocs/SymbolTable/ReferenceTarget.pm97
-rw-r--r--docs/tool/Modules/NaturalDocs/SymbolTable/Symbol.pm428
-rw-r--r--docs/tool/Modules/NaturalDocs/SymbolTable/SymbolDefinition.pm96
-rw-r--r--docs/tool/Modules/NaturalDocs/Topics.pm1319
-rw-r--r--docs/tool/Modules/NaturalDocs/Topics/Type.pm151
-rw-r--r--docs/tool/Modules/NaturalDocs/Version.pm384
63 files changed, 35293 insertions, 0 deletions
diff --git a/docs/tool/Modules/NaturalDocs/BinaryFile.pm b/docs/tool/Modules/NaturalDocs/BinaryFile.pm
new file mode 100644
index 00000000..5c1adb8d
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/BinaryFile.pm
@@ -0,0 +1,294 @@
+###############################################################################
+#
+#   Package: NaturalDocs::BinaryFile
+#
+###############################################################################
+#
+#   A package to manage Natural Docs' binary data files.
+#
+#   Usage:
+#
+#       - Only one data file can be managed with this package at a time.  You must close the file before opening another
+#         one.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::BinaryFile;
+
+use vars qw(@EXPORT @ISA);
+require Exporter;
+@ISA = qw(Exporter);
+
+@EXPORT = ('BINARY_FORMAT');
+
+
+###############################################################################
+# Group: Format
+
+#
+#   Topic: Standard Header
+#
+#   > [UInt8: BINARY_FORMAT]
+#   > [VersionInt: app version]
+#
+#   The first byte is <BINARY_FORMAT>, which distinguishes binary configuration files from text ones, since Natural Docs
+#   used to use text data files with the same name.
+#
+#   The next section is the version of Natural Docs that wrote the file, as defined by <NaturalDocs::Settings->AppVersion>
+#   and written by <NaturalDocs::Version->ToBinaryFile()>.
+#
+
+#
+#   Topic: Data Types
+#
+#   All the integer data types are written most significant byte first, aka big endian.
+#
+#   An AString16 is a UInt16 followed by that many 8-bit ASCII characters.  It doesn't include a null character at the end.  Undef
+#   strings are represented by a zero for the UInt16 and nothing following it.
+#
+
+#
+#   Constant: BINARY_FORMAT
+#
+#   An 8-bit constant that's used as the first byte of binary data files.  This is used so that you can easily distinguish between
+#   binary and old-style text data files.  It's not a character that would appear in plain text files.
+#
+use constant BINARY_FORMAT => pack('C', 0x06);
+# Which is ACK or acknowledge in ASCII.  Is the cool spade character in DOS displays.
+
+
+###############################################################################
+# Group: Variables
+
+#
+#   handle: FH_BINARYDATAFILE
+#
+#   The file handle used for the data file.
+#
+
+
+#
+#   string: currentFile
+#
+#   The <FileName> for the current configuration file being parsed.
+#
+my $currentFile;
+
+
+
+###############################################################################
+# Group: File Functions
+
+
+#
+#   Function: OpenForReading
+#
+#   Opens a binary file for reading.
+#
+#   Parameters:
+#
+#       minimumVersion - The minimum version of the file format that is acceptible.  May be undef.
+#
+#   Returns:
+#
+#       The format <VersionInt> or undef if it failed.  It could fail for any of the following reasons.
+#
+#       - The file doesn't exist.
+#       - The file couldn't be opened.
+#       - The file didn't have the proper header.
+#       - Either the application or the file was from a development release, and they're not the exact same development release.
+#       - The file's format was less than the minimum version, if one was defined.
+#       - The file was from a later application version than the current.
+#
+sub OpenForReading #(FileName file, optional VersionInt minimumVersion) => VersionInt
+    {
+    my ($self, $file, $minimumVersion) = @_;
+
+    if (defined $currentFile)
+        {  die "Tried to open binary file " . $file . " for reading when " . $currentFile . " was already open.";  };
+
+    $currentFile = $file;
+
+    if (open(FH_BINARYDATAFILE, '<' . $currentFile))
+        {
+        # See if it's binary.
+        binmode(FH_BINARYDATAFILE);
+
+        my $firstChar;
+        read(FH_BINARYDATAFILE, $firstChar, 1);
+
+        if ($firstChar == ::BINARY_FORMAT())
+            {
+            my $version = NaturalDocs::Version->FromBinaryFile(\*FH_BINARYDATAFILE);
+
+            if (NaturalDocs::Version->CheckFileFormat($version, $minimumVersion))
+                {  return $version;  };
+            };
+
+        close(FH_BINARYDATAFILE);
+        };
+
+    $currentFile = undef;
+    return undef;
+    };
+
+
+#
+#   Function: OpenForWriting
+#
+#   Opens a binary file for writing and writes the standard header.  Dies if the file cannot be opened.
+#
+sub OpenForWriting #(FileName file)
+    {
+    my ($self, $file) = @_;
+
+    if (defined $currentFile)
+        {  die "Tried to open binary file " . $file . " for writing when " . $currentFile . " was already open.";  };
+
+    $currentFile = $file;
+
+    open (FH_BINARYDATAFILE, '>' . $currentFile)
+        or die "Couldn't save " . $file . ".\n";
+
+    binmode(FH_BINARYDATAFILE);
+
+    print FH_BINARYDATAFILE '' . ::BINARY_FORMAT();
+    NaturalDocs::Version->ToBinaryFile(\*FH_BINARYDATAFILE, NaturalDocs::Settings->AppVersion());
+    };
+
+
+#
+#   Function: Close
+#
+#   Closes the current configuration file.
+#
+sub Close
+    {
+    my $self = shift;
+
+    if (!$currentFile)
+        {  die "Tried to close a binary file when one wasn't open.";  };
+
+    close(FH_BINARYDATAFILE);
+    $currentFile = undef;
+    };
+
+
+
+###############################################################################
+# Group: Reading Functions
+
+
+#
+#   Function: GetUInt8
+#   Reads and returns a UInt8 from the open file.
+#
+sub GetUInt8 # => UInt8
+    {
+    my $raw;
+    read(FH_BINARYDATAFILE, $raw, 1);
+
+    return unpack('C', $raw);
+    };
+
+#
+#   Function: GetUInt16
+#   Reads and returns a UInt16 from the open file.
+#
+sub GetUInt16 # => UInt16
+    {
+    my $raw;
+    read(FH_BINARYDATAFILE, $raw, 2);
+
+    return unpack('n', $raw);
+    };
+
+#
+#   Function: GetUInt32
+#   Reads and returns a UInt32 from the open file.
+#
+sub GetUInt32 # => UInt32
+    {
+    my $raw;
+    read(FH_BINARYDATAFILE, $raw, 4);
+
+    return unpack('N', $raw);
+    };
+
+#
+#   Function: GetAString16
+#   Reads and returns an AString16 from the open file.  Supports undef strings.
+#
+sub GetAString16 # => string
+    {
+    my $rawLength;
+    read(FH_BINARYDATAFILE, $rawLength, 2);
+    my $length = unpack('n', $rawLength);
+
+    if (!$length)
+        {  return undef;  };
+
+    my $string;
+    read(FH_BINARYDATAFILE, $string, $length);
+
+    return $string;
+    };
+
+
+
+###############################################################################
+# Group: Writing Functions
+
+
+#
+#   Function: WriteUInt8
+#   Writes a UInt8 to the open file.
+#
+sub WriteUInt8 #(UInt8 value)
+    {
+    my ($self, $value) = @_;
+    print FH_BINARYDATAFILE pack('C', $value);
+    };
+
+#
+#   Function: WriteUInt16
+#   Writes a UInt32 to the open file.
+#
+sub WriteUInt16 #(UInt16 value)
+    {
+    my ($self, $value) = @_;
+    print FH_BINARYDATAFILE pack('n', $value);
+    };
+
+#
+#   Function: WriteUInt32
+#   Writes a UInt32 to the open file.
+#
+sub WriteUInt32 #(UInt32 value)
+    {
+    my ($self, $value) = @_;
+    print FH_BINARYDATAFILE pack('N', $value);
+    };
+
+#
+#   Function: WriteAString16
+#   Writes an AString16 to the open file.  Supports undef strings.
+#
+sub WriteAString16 #(string value)
+    {
+    my ($self, $string) = @_;
+
+    if (length($string))
+        {  print FH_BINARYDATAFILE pack('nA*', length($string), $string);  }
+    else
+        {  print FH_BINARYDATAFILE pack('n', 0);  };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Builder.pm b/docs/tool/Modules/NaturalDocs/Builder.pm
new file mode 100644
index 00000000..cdcdff72
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Builder.pm
@@ -0,0 +1,280 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Builder
+#
+###############################################################################
+#
+#   A package that takes parsed source file and builds the output for it.
+#
+#   Usage and Dependencies:
+#
+#       - <Add()> can be called immediately.
+#       - <OutputPackages()> and <OutputPackageOf()> can be called once all sub-packages have been registered via <Add()>.
+#         Since this is normally done in their INIT functions, they should be available to all normal functions immediately.
+#
+#       - Prior to calling <Run()>, <NaturalDocs::Settings>, <NaturalDocs::Project>, <NaturalDocs::Menu>, and
+#         <NaturalDocs::Parser> must be initialized.  <NaturalDocs::Settings->GenerateDirectoryNames()> must be called.
+#         <NaturalDocs::SymbolTable> and <NaturalDocs::ClassHierarchy> must be initialized and fully resolved.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+
+use strict;
+use integer;
+
+use NaturalDocs::Builder::Base;
+use NaturalDocs::Builder::HTML;
+use NaturalDocs::Builder::FramedHTML;
+
+package NaturalDocs::Builder;
+
+
+###############################################################################
+# Group: Variables
+
+#
+#   Array: outputPackages
+#
+#   An array of the output packages available for use.
+#
+my @outputPackages;
+
+
+###############################################################################
+# Group: Functions
+
+
+#
+#   Function: OutputPackages
+#
+#   Returns an arrayref of the output packages available for use.  The arrayref is not a copy of the data, so don't change it.
+#
+#   Add output packages to this list with the <Add()> function.
+#
+sub OutputPackages
+    {  return \@outputPackages;  };
+
+
+#
+#   Function: OutputPackageOf
+#
+#   Returns the output package corresponding to the passed command line option, or undef if none.
+#
+sub OutputPackageOf #(commandLineOption)
+    {
+    my ($self, $commandLineOption) = @_;
+
+    $commandLineOption = lc($commandLineOption);
+
+    foreach my $package (@outputPackages)
+        {
+        if (lc($package->CommandLineOption()) eq $commandLineOption)
+            {  return $package;  };
+        };
+
+    return undef;
+    };
+
+
+
+#
+#   Function: Add
+#
+#   Adds an output package to those available for use.  All output packages must call this function in order to be recognized.
+#
+#   Parameters:
+#
+#       package - The package name.
+#
+sub Add #(package)
+    {
+    my ($self, $package) = @_;
+
+    # Output packages shouldn't register themselves more than once, so we don't need to check for it.
+    push @outputPackages, $package;
+    };
+
+
+#
+#   Function: Run
+#
+#   Runs the build process.  This must be called *every time* Natural Docs is run, regardless of whether any source files changed
+#   or not.  Some output packages have dependencies on files outside of the source tree that need to be checked.
+#
+#   Since there are multiple stages to the build process, this function will handle its own status messages.  There's no need to print
+#   "Building files..." or something similar beforehand.
+#
+sub Run
+    {
+    my ($self) = @_;
+
+
+    # Determine what we're doing.
+
+    my $buildTargets = NaturalDocs::Settings->BuildTargets();
+
+    my $filesToBuild = NaturalDocs::Project->FilesToBuild();
+    my $numberOfFilesToBuild = (scalar keys %$filesToBuild) * (scalar @$buildTargets);
+
+    my $filesToPurge = NaturalDocs::Project->FilesToPurge();
+    my $numberOfFilesToPurge = (scalar keys %$filesToPurge) * (scalar @$buildTargets);
+
+    my $imagesToUpdate = NaturalDocs::Project->ImageFilesToUpdate();
+    my $numberOfImagesToUpdate = (scalar keys %$imagesToUpdate) * (scalar @$buildTargets);
+
+    my $imagesToPurge = NaturalDocs::Project->ImageFilesToPurge();
+    my $numberOfImagesToPurge = (scalar keys %$imagesToPurge) * (scalar @$buildTargets);
+
+    my %indexesToBuild;
+    my %indexesToPurge;
+
+    my $currentIndexes = NaturalDocs::Menu->Indexes();
+    my $previousIndexes = NaturalDocs::Menu->PreviousIndexes();
+
+    foreach my $index (keys %$currentIndexes)
+        {
+        if (NaturalDocs::SymbolTable->IndexChanged($index) || !exists $previousIndexes->{$index})
+            {
+            $indexesToBuild{$index} = 1;
+            };
+        };
+
+    # All indexes that still exist should have been deleted.
+    foreach my $index (keys %$previousIndexes)
+        {
+        if (!exists $currentIndexes->{$index})
+            {
+            $indexesToPurge{$index} = 1;
+            };
+        };
+
+    my $numberOfIndexesToBuild = (scalar keys %indexesToBuild) * (scalar @$buildTargets);
+    my $numberOfIndexesToPurge = (scalar keys %indexesToPurge) * (scalar @$buildTargets);
+
+
+    # Start the build process
+
+    foreach my $buildTarget (@$buildTargets)
+        {
+        $buildTarget->Builder()->BeginBuild( $numberOfFilesToBuild || $numberOfFilesToPurge ||
+                                                               $numberOfImagesToUpdate || $numberOfImagesToPurge ||
+                                                               $numberOfIndexesToBuild || $numberOfIndexesToPurge ||
+                                                               NaturalDocs::Menu->HasChanged() );
+        };
+
+    if ($numberOfFilesToPurge)
+        {
+        NaturalDocs::StatusMessage->Start('Purging ' . $numberOfFilesToPurge
+                                                          . ' file' . ($numberOfFilesToPurge > 1 ? 's' : '') . '...',
+                                                             scalar @$buildTargets);
+
+        foreach my $buildTarget (@$buildTargets)
+            {
+            $buildTarget->Builder()->PurgeFiles($filesToPurge);
+            NaturalDocs::StatusMessage->CompletedItem();
+            };
+        };
+
+    if ($numberOfIndexesToPurge)
+        {
+        NaturalDocs::StatusMessage->Start('Purging ' . $numberOfIndexesToPurge
+                                                           . ' index' . ($numberOfIndexesToPurge > 1 ? 'es' : '') . '...',
+                                                             scalar @$buildTargets);
+
+        foreach my $buildTarget (@$buildTargets)
+            {
+            $buildTarget->Builder()->PurgeIndexes(\%indexesToPurge);
+            NaturalDocs::StatusMessage->CompletedItem();
+            };
+        };
+
+    if ($numberOfImagesToPurge)
+        {
+        NaturalDocs::StatusMessage->Start('Purging ' . $numberOfImagesToPurge
+                                                          . ' image' . ($numberOfImagesToPurge > 1 ? 's' : '') . '...',
+                                                             scalar @$buildTargets);
+
+        foreach my $buildTarget (@$buildTargets)
+            {
+            $buildTarget->Builder()->PurgeImages($imagesToPurge);
+            NaturalDocs::StatusMessage->CompletedItem();
+            };
+        };
+
+    if ($numberOfFilesToBuild)
+        {
+        NaturalDocs::StatusMessage->Start('Building ' . $numberOfFilesToBuild
+                                                           . ' file' . ($numberOfFilesToBuild > 1 ? 's' : '') . '...',
+                                                             $numberOfFilesToBuild);
+
+        foreach my $file (keys %$filesToBuild)
+            {
+            my $parsedFile = NaturalDocs::Parser->ParseForBuild($file);
+
+            NaturalDocs::Error->OnStartBuilding($file);
+
+            foreach my $buildTarget (@$buildTargets)
+                {
+                $buildTarget->Builder()->BuildFile($file, $parsedFile);
+                NaturalDocs::StatusMessage->CompletedItem();
+                };
+
+            NaturalDocs::Error->OnEndBuilding($file);
+            };
+        };
+
+    if ($numberOfIndexesToBuild)
+        {
+        NaturalDocs::StatusMessage->Start('Building ' . $numberOfIndexesToBuild
+                                                          . ' index' . ($numberOfIndexesToBuild > 1 ? 'es' : '') . '...',
+                                                             $numberOfIndexesToBuild);
+
+        foreach my $index (keys %indexesToBuild)
+            {
+            foreach my $buildTarget (@$buildTargets)
+                {
+                $buildTarget->Builder()->BuildIndex($index);
+                NaturalDocs::StatusMessage->CompletedItem();
+                };
+            };
+        };
+
+    if ($numberOfImagesToUpdate)
+        {
+        NaturalDocs::StatusMessage->Start('Updating ' . $numberOfImagesToUpdate
+                                                          . ' image' . ($numberOfImagesToUpdate > 1 ? 's' : '') . '...',
+                                                             $numberOfImagesToUpdate);
+
+        foreach my $image (keys %$imagesToUpdate)
+            {
+            foreach my $buildTarget (@$buildTargets)
+                {
+                $buildTarget->Builder()->UpdateImage($image);
+                NaturalDocs::StatusMessage->CompletedItem();
+                };
+            };
+        };
+
+    if (NaturalDocs::Menu->HasChanged())
+        {
+        if (!NaturalDocs::Settings->IsQuiet())
+            {  print "Updating menu...\n";  };
+
+        foreach my $buildTarget (@$buildTargets)
+            {  $buildTarget->Builder()->UpdateMenu();  };
+        };
+
+    foreach my $buildTarget (@$buildTargets)
+        {
+        $buildTarget->Builder()->EndBuild($numberOfFilesToBuild || $numberOfFilesToPurge ||
+                                                           $numberOfIndexesToBuild || $numberOfIndexesToPurge ||
+                                                           $numberOfImagesToUpdate || $numberOfImagesToPurge ||
+                                                           NaturalDocs::Menu->HasChanged());
+        };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Builder/Base.pm b/docs/tool/Modules/NaturalDocs/Builder/Base.pm
new file mode 100644
index 00000000..0298264d
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Builder/Base.pm
@@ -0,0 +1,348 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Builder::Base
+#
+###############################################################################
+#
+#   A base class for all Builder output formats.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Builder::Base;
+
+
+###############################################################################
+# Group: Notes
+
+
+#
+#   Topic: Implementation
+#
+#   Builder packages are implemented as blessed arrayrefs, not hashrefs.  This is done for all objects in Natural Docs for
+#   efficiency reasons.  You create members by defining constants via <NaturalDocs::DefineMembers> and using them as
+#   indexes into the array.
+#
+
+#
+#   Topic: Function Order
+#
+#   The functions in the build process will always be called in the following order.
+#
+#   - <BeginBuild()> will always be called.
+#   - <PurgeFiles()> will be called next only if there's files that need to be purged.
+#   - <PurgeIndexes()> will be called next only if there's indexes that need to be purged.
+#   - <PurgeImages()> will e called next only if there's images that need to be purged.
+#   - <BuildFile()> will be called once for each file that needs to be built, if any.
+#   - <BuildIndex()> will be called once for each index that changed and is part of the menu, if any.
+#   - <UpdateImage()> will be called once for each image that needs to be updated, if any.
+#   - <UpdateMenu()> will be called next only if the menu changed.
+#   - <EndBuild()> will always be called.
+#
+
+#
+#   Topic: How to Approach
+#
+#   Here's an idea of how to approach making packages for different output types.
+#
+#
+#   Multiple Output Files, Embedded Menu:
+#
+#       This example is for when you want to build one output file per source file, each with its own copy of the menu within it.
+#       This is how <NaturalDocs::Builder::HTML> works.
+#
+#       Make sure you create a function that generates just the menu for a particular source file.  We'll need to generate menus for
+#       both building a file from scratch and for updating the menu on an existing output file, so it's better to give it its own function.
+#       You may want to surround it with something that can be easily detected in the output file to make replacing easier.
+#
+#       <BeginBuild()> isn't important.  You don't need to implement it.
+#
+#       Implement <PurgeFiles()> to delete the output files associated with the purged files.
+#
+#       Implement <PurgeIndexes()> to delete the output files associated with the purged indexes.
+#
+#       Implement <BuildFile()> to create an output file for the parsed source file.  Use the menu function described earlier.
+#
+#       Implement <BuildIndex()> to create an output file for each index.  Use the menu function described earlier for each page.
+#
+#       Implement <UpdateMenu()> to go through the list of unbuilt files and update their menus.  You can get the list from
+#       <NaturalDocs::Project->UnbuiltFilesWithContent()>.  You need to open their output files, replace the menu, and save it back
+#       to disk.  Yes, it would be simpler from a programmer's point of view to just rebuild the file completely, but that would be
+#       _very_ inefficient since there could potentially be a _lot_ of files in this group.
+#
+#       Also make sure <UpdateMenu()> goes through the unchanged indexes and updates them as well.
+#
+#       <EndBuild()> isn't important.  You don't need to implement it.
+#
+#
+#   Multiple Output Files, Menu in File:
+#
+#       This example is for when you want to build one output file per source file, but keep the menu in its own separate file.  This
+#       is how <NaturalDocs::Builder::FramedHTML> works.
+#
+#       <BeginBuild()> isn't important.  You don't need to implement it.
+#
+#       Implement <PurgeFiles()> to delete the output files associated with the purged files.
+#
+#       Implement <PurgeIndexes()> to delete the output files associated with the purged indexes.
+#
+#       Implement <BuildFile()> to generate an output file from the parsed source file.
+#
+#       Implement <BuildIndex()> to generate an output file for each index.
+#
+#       Implement <UpdateMenu()> to rebuild the menu file.
+#
+#       <EndBuild()> isn't important.  You don't need to implement it.
+#
+#
+#   Single Output File using Intermediate Files:
+#
+#       This example is for when you want to build one output file, such as a PDF file, but use intermediate files to handle differential
+#       building.  This would be much like how a compiler compiles each source file into a object file, and then a linker stitches them
+#       all together into the final executable file.
+#
+#       <BeginBuild()> isn't important.  You don't need to implement it.
+#
+#       Implement <PurgeFiles()> to delete the intermediate files associated with the purged files.
+#
+#       Implement <PurgeIndexes()> to delete the intermediate files associated with the purged indexes.
+#
+#       Implement <BuildFile()> to generate an intermediate file from the parsed source file.
+#
+#       Implement <BuildIndex()> to generate an intermediate file for the specified index.
+#
+#       Implement <UpdateMenu()> to generate the intermediate file for the menu.
+#
+#       Implement <EndBuild()> so that if the project changed, it stitches the intermediate files together into the final
+#       output file.  Make sure you check the parameter because the function will be called when nothing changes too.
+#
+#
+#   Single Output File using Direct Changes:
+#
+#       This example is for when you want to build one output file, such as a PDF file, but engineering it in such a way that you don't
+#       need to use intermediate files.  In other words, you're able to add, delete, and modify entries directly in the output file.
+#
+#       Implement <BeginBuild()> so that if the project changed, it opens the output file and does anything it needs to do
+#       to get ready for editing.
+#
+#       Implement <PurgeFiles()> to remove the entries associated with the purged files.
+#
+#       Implement <PurgeIndexes()> to remove the entries associated with the purged indexes.
+#
+#       Implement <BuildFile()> to add or replace a section of the output file with a new one generated from the parsed file.
+#
+#       Implement <BuildIndex()> to add or replace an index in the output file with a new one generated from the specified index.
+#
+#       Implement <EndBuild()> so that if the project changed, it saves the output file to disk.
+#
+#       How you handle the menu depends on how the output file references other sections of itself.  If it can do so by name, then
+#       you can implement <UpdateMenu()> to update the menu section of the file and you're done.  If it has to reference itself
+#       by address or offset, it gets trickier.  You should skip <UpdateMenu()> and instead rebuild the menu in <EndBuild()> if
+#       the parameter is true.  This lets you do it whenever anything changes in a file, rather than just when the menu
+#       visibly changes.  How you keep track of the locations and how they change is your problem.
+#
+
+
+###############################################################################
+#
+#   Group: Required Interface Functions
+#
+#   All Builder classes *must* define these functions.
+#
+
+
+#
+#   Function: INIT
+#
+#   Define this function to call <NaturalDocs::Builder->Add()> so that <NaturalDocs::Builder> knows about this package.
+#   Packages are defined this way so that new ones can be added without messing around in other code.
+#
+
+
+#
+#   Function: CommandLineOption
+#
+#   Define this function to return the text that should be put in the command line after -o to use this package.  It cannot have
+#   spaces and is not case sensitive.
+#
+#   For example, <NaturalDocs::Builder::HTML> returns 'html' so someone could use -o html [directory] to use that package.
+#
+sub CommandLineOption
+    {
+    NaturalDocs::Error->SoftDeath($_[0] . " didn't define CommandLineOption().");
+    };
+
+
+#
+#   Function: BuildFile
+#
+#   Define this function to convert a parsed file to this package's output format.  This function will be called once for every source
+#   file that needs to be rebuilt.  However, if a file hasn't changed since the last time Natural Docs was run, it will not be sent to
+#   this function.  All packages must support differential build.
+#
+#   Parameters:
+#
+#       sourceFile  - The name of the source file.
+#       parsedFile  - The parsed source file, as an arrayref of <NaturalDocs::Parser::ParsedTopic> objects.
+#
+sub BuildFile #(sourceFile, parsedFile)
+    {
+    NaturalDocs::Error->SoftDeath($_[0] . " didn't define BuildFile().");
+    };
+
+
+###############################################################################
+#
+#   Group: Optional Interface Functions
+#
+#   These functions can be implemented but packages are not required to do so.
+#
+
+
+#
+#   Function: New
+#
+#   Creates and returns a new object.
+#
+#   Note that this is the only function where the first parameter will be the package name, not the object itself.
+#
+sub New
+    {
+    my $package = shift;
+
+    my $object = [ ];
+    bless $object, $package;
+
+    return $object;
+    };
+
+
+#
+#   Function: BeginBuild
+#
+#   Define this function if the package needs to do anything at the beginning of the build process.  This function will be called
+#   every time Natural Docs is run, even if the project hasn't changed.  This allows you to manage dependencies specific
+#   to the output format that may change independently from the source tree and menu.  For example,
+#   <NaturalDocs::Builder::HTML> needs to keep the CSS files in sync regardless of whether the source tree changed or not.
+#
+#   Parameters:
+#
+#       hasChanged - Whether the project has changed, such as source files or the menu file.  If false, nothing else is going to be
+#                            called except <EndBuild()>.
+#
+sub BeginBuild #(hasChanged)
+    {
+    };
+
+
+#
+#   Function: EndBuild
+#
+#   Define this function if the package needs to do anything at the end of the build process.  This function will be called every time
+#   Natural Docs is run, even if the project hasn't changed.  This allows you to manage dependencies specific to the output
+#   format that may change independently from the source tree.  For example, <NaturalDocs::Builder::HTML> needs to keep the
+#   CSS files in sync regardless of whether the source tree changed or not.
+#
+#   Parameters:
+#
+#       hasChanged - Whether the project has changed, such as source files or the menu file.  If false, the only other function that
+#                            was called was <BeginBuild()>.
+#
+sub EndBuild #(hasChanged)
+    {
+    };
+
+
+#
+#   Function: BuildIndex
+#
+#   Define this function to create an index for the passed topic.  You can get the index from
+#   <NaturalDocs::SymbolTable->Index()>.
+#
+#   The reason it's not passed directly to this function is because indexes may be time-consuming to create.  As such, they're
+#   generated on demand because some output packages may choose not to implement them.
+#
+#   Parameters:
+#
+#       topic  - The <TopicType> to limit the index by.
+#
+sub BuildIndex #(topic)
+    {
+    };
+
+
+#
+#   Function: UpdateImage
+#
+#   Define this function to add or update the passed image in the output.
+#
+#   Parameters:
+#
+#       file - The image <FileName>
+#
+sub UpdateImage #(file)
+    {
+    };
+
+
+#
+#   Function: PurgeFiles
+#
+#   Define this function to make the package remove all output related to the passed files.  These files no longer have Natural Docs
+#   content.
+#
+#   Parameters:
+#
+#       files - An existence hashref of the files to purge.
+#
+sub PurgeFiles #(files)
+    {
+    };
+
+
+#
+#   Function: PurgeIndexes
+#
+#   Define this function to make the package remove all output related to the passed indexes.  These indexes are no longer part
+#   of the menu.
+#
+#   Parameters:
+#
+#       indexes  - An existence hashref of the <TopicTypes> of the indexes to purge.
+#
+sub PurgeIndexes #(indexes)
+    {
+    };
+
+
+#
+#   Function: PurgeImages
+#
+#   Define this function to make the package remove all output related to the passed image files.  These files are no longer used
+#   by the documentation.
+#
+#   Parameters:
+#
+#       files - An existence hashref of the image <FileNames> to purge.
+#
+sub PurgeImages #(files)
+    {
+    };
+
+
+#
+#   Function: UpdateMenu
+#
+#   Define this function to make the package update the menu.  It will only be called if the menu changed.
+#
+sub UpdateMenu
+    {
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Builder/FramedHTML.pm b/docs/tool/Modules/NaturalDocs/Builder/FramedHTML.pm
new file mode 100644
index 00000000..ab020aa6
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Builder/FramedHTML.pm
@@ -0,0 +1,345 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Builder::FramedHTML
+#
+###############################################################################
+#
+#   A package that generates output in HTML with frames.
+#
+#   All functions are called with Package->Function() notation.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+
+use strict;
+use integer;
+
+package NaturalDocs::Builder::FramedHTML;
+
+use base 'NaturalDocs::Builder::HTMLBase';
+
+
+###############################################################################
+# Group: Implemented Interface Functions
+
+
+#
+#   Function: INIT
+#
+#   Registers the package with <NaturalDocs::Builder>.
+#
+sub INIT
+    {
+    NaturalDocs::Builder->Add(__PACKAGE__);
+    };
+
+
+#
+#   Function: CommandLineOption
+#
+#   Returns the option to follow -o to use this package.  In this case, "html".
+#
+sub CommandLineOption
+    {
+    return 'FramedHTML';
+    };
+
+
+#
+#   Function: BuildFile
+#
+#   Builds the output file from the parsed source file.
+#
+#   Parameters:
+#
+#       sourcefile       - The <FileName> of the source file.
+#       parsedFile      - An arrayref of the source file as <NaturalDocs::Parser::ParsedTopic> objects.
+#
+sub BuildFile #(sourceFile, parsedFile)
+    {
+    my ($self, $sourceFile, $parsedFile) = @_;
+
+    my $outputFile = $self->OutputFileOf($sourceFile);
+
+
+    # 99.99% of the time the output directory will already exist, so this will actually be more efficient.  It only won't exist
+    # if a new file was added in a new subdirectory and this is the first time that file was ever parsed.
+    if (!open(OUTPUTFILEHANDLE, '>' . $outputFile))
+        {
+        NaturalDocs::File->CreatePath( NaturalDocs::File->NoFileName($outputFile) );
+
+        open(OUTPUTFILEHANDLE, '>' . $outputFile)
+            or die "Couldn't create output file " . $outputFile . "\n";
+        };
+
+    print OUTPUTFILEHANDLE
+
+
+
+        # IE 6 doesn't like any doctype here at all.  Add one (strict or transitional doesn't matter) and it makes the page slightly too
+        # wide for the frame.  Mozilla and Opera handle it like champs either way because they Don't Suck(tm).
+
+        # '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" '
+        # . '"http://www.w3.org/TR/REC-html40/loose.dtd">' . "\n\n"
+
+        '<html><head>'
+
+            . (NaturalDocs::Settings->CharSet() ?
+                '<meta http-equiv="Content-Type" content="text/html; charset=' . NaturalDocs::Settings->CharSet() . '">' : '')
+
+            . '<title>'
+                . $self->BuildTitle($sourceFile)
+            . '</title>'
+
+            . '<link rel="stylesheet" type="text/css" href="' . $self->MakeRelativeURL($outputFile, $self->MainCSSFile(), 1) . '">'
+
+            . '<script language=JavaScript src="' . $self->MakeRelativeURL($outputFile, $self->MainJavaScriptFile(), 1) . '"></script>'
+
+        . '</head><body class="FramedContentPage" onLoad="NDOnLoad()">'
+            . $self->OpeningBrowserStyles()
+
+            . $self->StandardComments()
+
+            . "\n\n\n"
+                . $self->BuildContent($sourceFile, $parsedFile)
+            . "\n\n\n"
+
+            . $self->BuildToolTips()
+
+            . $self->ClosingBrowserStyles()
+        . '</body></html>';
+
+
+    close(OUTPUTFILEHANDLE);
+    };
+
+
+#
+#   Function: BuildIndex
+#
+#   Builds an index for the passed type.
+#
+#   Parameters:
+#
+#       type  - The <TopicType> to limit the index to, or undef if none.
+#
+sub BuildIndex #(type)
+    {
+    my ($self, $type) = @_;
+
+    my $indexTitle = $self->IndexTitleOf($type);
+    my $indexFile = $self->IndexFileOf($type);
+
+    my $startIndexPage =
+
+        '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" '
+            . '"http://www.w3.org/TR/REC-html40/loose.dtd">' . "\n\n"
+
+        . '<html><head>'
+
+            . (NaturalDocs::Settings->CharSet() ?
+                '<meta http-equiv="Content-Type" content="text/html; charset=' . NaturalDocs::Settings->CharSet() . '">' : '')
+
+            . '<title>';
+
+            if (defined NaturalDocs::Menu->Title())
+                {  $startIndexPage .= $self->StringToHTML(NaturalDocs::Menu->Title()) . ' - ';  };
+
+                $startIndexPage .=
+                $indexTitle
+            . '</title>'
+
+            . '<link rel="stylesheet" type="text/css" href="' . $self->MakeRelativeURL($indexFile, $self->MainCSSFile(), 1) . '">'
+
+            . '<script language=JavaScript src="' . $self->MakeRelativeURL($indexFile, $self->MainJavaScriptFile(), 1) . '"></script>'
+
+        . '</head><body class="FramedIndexPage" onLoad="NDOnLoad()">'
+            . $self->OpeningBrowserStyles()
+
+            . "\n\n\n"
+                . $self->StandardComments()
+            . "\n\n\n"
+                . '<div id=Index>'
+                    . '<div class=IPageTitle>'
+                        . $indexTitle
+                    . '</div>';
+
+
+    my $endIndexPage =
+
+                    '</div><!--Index-->'
+                . "\n\n\n"
+
+                . $self->ClosingBrowserStyles()
+
+       . '</body></html>';
+
+    my $startSearchResultsPage =
+
+        '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" '
+            . '"http://www.w3.org/TR/REC-html40/loose.dtd">' . "\n\n"
+
+        . '<html><head>'
+
+            . (NaturalDocs::Settings->CharSet() ?
+                '<meta http-equiv="Content-Type" content="text/html; charset=' . NaturalDocs::Settings->CharSet() . '">' : '')
+
+            . '<link rel="stylesheet" type="text/css" href="' . $self->MakeRelativeURL($indexFile, $self->MainCSSFile(), 1) . '">'
+
+            . '<script language=JavaScript src="' . $self->MakeRelativeURL($indexFile, $self->MainJavaScriptFile(), 1) . '"></script>'
+            . '<script language=JavaScript src="' . $self->MakeRelativeURL($indexFile, $self->SearchDataJavaScriptFile(), 1) . '">'
+                . '</script>'
+
+        . '</head><body class="FramedSearchResultsPage" onLoad="NDOnLoad()">'
+            . $self->OpeningBrowserStyles()
+
+            . "\n\n\n"
+                . $self->StandardComments()
+            . "\n\n\n"
+
+                . '<div id=Index>'
+                    . '<div class=IPageTitle>'
+                        . 'Search Results'
+                    . '</div>';
+
+    my $endSearchResultsPage =
+
+                    '</div><!--Index-->'
+                . "\n\n\n"
+
+                . $self->ClosingBrowserStyles()
+
+       . '</body></html>';
+
+    my $indexContent = NaturalDocs::SymbolTable->Index($type);
+    my $pageCount = $self->BuildIndexPages($type, $indexContent, $startIndexPage, $endIndexPage,
+                                                                  $startSearchResultsPage, $endSearchResultsPage);
+    $self->PurgeIndexFiles($type, $indexContent, $pageCount + 1);
+    };
+
+
+#
+#   Function: UpdateMenu
+#
+#   Builds the menu file.  Also generates index.html.
+#
+sub UpdateMenu
+    {
+    my $self = shift;
+
+    my $outputDirectory = NaturalDocs::Settings->OutputDirectoryOf($self);
+    my $outputFile = NaturalDocs::File->JoinPaths($outputDirectory, 'menu.html');
+
+
+    open(OUTPUTFILEHANDLE, '>' . $outputFile)
+        or die "Couldn't create output file " . $outputFile . "\n";
+
+    my $title = 'Menu';
+    if (defined $title)
+        {  $title .= ' - ' . NaturalDocs::Menu->Title();  };
+
+    $title = $self->StringToHTML($title);
+
+
+    print OUTPUTFILEHANDLE
+
+
+        '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" '
+            . '"http://www.w3.org/TR/REC-html40/loose.dtd">' . "\n\n"
+
+        . '<html><head>'
+
+            . (NaturalDocs::Settings->CharSet() ?
+                '<meta http-equiv="Content-Type" content="text/html; charset=' . NaturalDocs::Settings->CharSet() . '">' : '')
+
+            . '<title>'
+                . $title
+            . '</title>'
+
+            . '<base target="Content">'
+
+            . '<link rel="stylesheet" type="text/css" href="' . $self->MakeRelativeURL($outputFile, $self->MainCSSFile(), 1) . '">'
+
+            . '<script language=JavaScript src="' . $self->MakeRelativeURL($outputFile, $self->MainJavaScriptFile(), 1) . '"></script>'
+            . '<script language=JavaScript src="' . $self->MakeRelativeURL($outputFile, $self->SearchDataJavaScriptFile(), 1) . '">'
+                . '</script>'
+
+        . '</head><body class="FramedMenuPage" onLoad="NDOnLoad()">'
+            . $self->OpeningBrowserStyles()
+
+            . $self->StandardComments()
+
+            . "\n\n\n"
+                . $self->BuildMenu(undef, undef)
+            . "\n\n\n"
+                . $self->BuildFooter(1)
+            . "\n\n\n"
+
+            . $self->ClosingBrowserStyles()
+        . '</body></html>';
+
+
+    close(OUTPUTFILEHANDLE);
+
+
+    # Update index.html
+
+    my $firstMenuEntry = $self->FindFirstFile();
+    my $indexFile = NaturalDocs::File->JoinPaths( NaturalDocs::Settings->OutputDirectoryOf($self), 'index.html' );
+
+    # We have to check because it's possible that there may be no files with Natural Docs content and thus no files on the menu.
+    if (defined $firstMenuEntry)
+        {
+        open(INDEXFILEHANDLE, '>' . $indexFile)
+            or die "Couldn't create output file " . $indexFile . ".\n";
+
+        print INDEXFILEHANDLE
+
+            '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN" '
+                . '"http://www.w3.org/TR/REC-html40/frameset.dtd">'
+
+            . '<html>'
+
+                . '<head>'
+
+                    . (NaturalDocs::Settings->CharSet() ?
+                        '<meta http-equiv="Content-Type" content="text/html; charset=' . NaturalDocs::Settings->CharSet() . '">' : '')
+
+                    . '<title>'
+                        . $self->StringToHTML(NaturalDocs::Menu->Title())
+                    . '</title>'
+
+                . '</head>'
+
+                . $self->StandardComments()
+
+                . '<frameset cols="185,*">'
+                    . '<frame name=Menu src="menu.html">'
+                    . '<frame name=Content src="'
+                        . $self->MakeRelativeURL($indexFile, $self->OutputFileOf($firstMenuEntry->Target()), 1) . '">'
+                . '</frameset>'
+
+                . '<noframes>'
+                    . 'This documentation was designed for use with frames.  However, you can still use it by '
+                    . '<a href="menu.html">starting from the menu page</a>.'
+                    . "<script language=JavaScript><!--\n"
+                        . 'location.href="menu.html";'
+                    . "\n// --></script>"
+                . '</noframes>'
+
+            . '</html>';
+
+        close INDEXFILEHANDLE;
+        }
+
+    elsif (-e $indexFile)
+        {
+        unlink($indexFile);
+        };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Builder/HTML.pm b/docs/tool/Modules/NaturalDocs/Builder/HTML.pm
new file mode 100644
index 00000000..95f31b5a
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Builder/HTML.pm
@@ -0,0 +1,398 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Builder::HTML
+#
+###############################################################################
+#
+#   A package that generates output in HTML.
+#
+#   All functions are called with Package->Function() notation.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+
+use strict;
+use integer;
+
+package NaturalDocs::Builder::HTML;
+
+use base 'NaturalDocs::Builder::HTMLBase';
+
+
+###############################################################################
+# Group: Implemented Interface Functions
+
+
+#
+#   Function: INIT
+#
+#   Registers the package with <NaturalDocs::Builder>.
+#
+sub INIT
+    {
+    NaturalDocs::Builder->Add(__PACKAGE__);
+    };
+
+
+#
+#   Function: CommandLineOption
+#
+#   Returns the option to follow -o to use this package.  In this case, "html".
+#
+sub CommandLineOption
+    {
+    return 'HTML';
+    };
+
+
+#
+#   Function: BuildFile
+#
+#   Builds the output file from the parsed source file.
+#
+#   Parameters:
+#
+#       sourcefile       - The <FileName> of the source file.
+#       parsedFile      - An arrayref of the source file as <NaturalDocs::Parser::ParsedTopic> objects.
+#
+sub BuildFile #(sourceFile, parsedFile)
+    {
+    my ($self, $sourceFile, $parsedFile) = @_;
+
+    my $outputFile = $self->OutputFileOf($sourceFile);
+
+
+    # 99.99% of the time the output directory will already exist, so this will actually be more efficient.  It only won't exist
+    # if a new file was added in a new subdirectory and this is the first time that file was ever parsed.
+    if (!open(OUTPUTFILEHANDLE, '>' . $outputFile))
+        {
+        NaturalDocs::File->CreatePath( NaturalDocs::File->NoFileName($outputFile) );
+
+        open(OUTPUTFILEHANDLE, '>' . $outputFile)
+            or die "Couldn't create output file " . $outputFile . "\n";
+        };
+
+    print OUTPUTFILEHANDLE
+
+
+        '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" '
+            . '"http://www.w3.org/TR/REC-html40/strict.dtd">' . "\n\n"
+
+        . '<html><head>'
+
+            . (NaturalDocs::Settings->CharSet() ?
+                '<meta http-equiv="Content-Type" content="text/html; charset=' . NaturalDocs::Settings->CharSet() . '">' : '')
+
+            . '<title>'
+                . $self->BuildTitle($sourceFile)
+            . '</title>'
+
+            . '<link rel="stylesheet" type="text/css" href="' . $self->MakeRelativeURL($outputFile, $self->MainCSSFile(), 1) . '">'
+
+            . '<script language=JavaScript src="' . $self->MakeRelativeURL($outputFile, $self->MainJavaScriptFile(), 1) . '">'
+                . '</script>'
+            . '<script language=JavaScript src="' . $self->MakeRelativeURL($outputFile, $self->SearchDataJavaScriptFile(), 1) . '">'
+                . '</script>'
+
+        . '</head><body class="ContentPage" onLoad="NDOnLoad()">'
+            . $self->OpeningBrowserStyles()
+
+            . $self->StandardComments()
+
+            . "\n\n\n"
+                . $self->BuildContent($sourceFile, $parsedFile)
+            . "\n\n\n"
+                . $self->BuildFooter()
+            . "\n\n\n"
+                . $self->BuildMenu($sourceFile, undef)
+            . "\n\n\n"
+                . $self->BuildToolTips()
+            . "\n\n\n"
+                . '<div id=MSearchResultsWindow>'
+                    . '<iframe src="" frameborder=0 name=MSearchResults id=MSearchResults></iframe>'
+                    . '<a href="javascript:searchPanel.CloseResultsWindow()" id=MSearchResultsWindowClose>Close</a>'
+                . '</div>'
+            . "\n\n\n"
+
+            . $self->ClosingBrowserStyles()
+        . '</body></html>';
+
+
+    close(OUTPUTFILEHANDLE);
+    };
+
+
+#
+#   Function: BuildIndex
+#
+#   Builds an index for the passed type.
+#
+#   Parameters:
+#
+#       type  - The <TopicType> to limit the index to, or undef if none.
+#
+sub BuildIndex #(type)
+    {
+    my ($self, $type) = @_;
+
+    my $indexTitle = $self->IndexTitleOf($type);
+
+    my $startIndexPage =
+
+        '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" '
+            . '"http://www.w3.org/TR/REC-html40/strict.dtd">' . "\n\n"
+
+        . '<html><head>'
+
+            . (NaturalDocs::Settings->CharSet() ?
+                '<meta http-equiv="Content-Type" content="text/html; charset=' . NaturalDocs::Settings->CharSet() . '">' : '')
+
+            . '<title>'
+                . $indexTitle;
+
+                if (defined NaturalDocs::Menu->Title())
+                    {  $startIndexPage .= ' - ' . $self->StringToHTML(NaturalDocs::Menu->Title());  };
+
+            $startIndexPage .=
+            '</title>'
+
+            . '<link rel="stylesheet" type="text/css" href="' . $self->MakeRelativeURL($self->IndexDirectory(),
+                                                                                                                       $self->MainCSSFile()) . '">'
+
+            . '<script language=JavaScript src="' . $self->MakeRelativeURL($self->IndexDirectory(),
+                                                                                                        $self->MainJavaScriptFile()) . '"></script>'
+            . '<script language=JavaScript src="' . $self->MakeRelativeURL($self->IndexDirectory(),
+                                                                                                        $self->SearchDataJavaScriptFile()) . '">'
+                . '</script>'
+
+        . '</head><body class="IndexPage" onLoad="NDOnLoad()">'
+            . $self->OpeningBrowserStyles()
+
+        . $self->StandardComments()
+
+        . "\n\n\n"
+
+        . '<div id=Index>'
+            . '<div class=IPageTitle>'
+                . $indexTitle
+            . '</div>';
+
+    my $endIndexPage =
+            '</div><!--Index-->'
+
+            . "\n\n\n"
+                . $self->BuildFooter()
+            . "\n\n\n"
+                . $self->BuildMenu(undef, $type)
+            . "\n\n\n"
+                . '<div id=MSearchResultsWindow>'
+                    . '<iframe src="" frameborder=0 name=MSearchResults id=MSearchResults></iframe>'
+                    . '<a href="javascript:searchPanel.CloseResultsWindow()" id=MSearchResultsWindowClose>Close</a>'
+                . '</div>'
+            . "\n\n\n"
+
+            . $self->ClosingBrowserStyles()
+        . '</body></html>';
+
+
+    my $startSearchResultsPage =
+
+        '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" '
+            . '"http://www.w3.org/TR/REC-html40/strict.dtd">' . "\n\n"
+
+        . '<html><head>'
+
+            . (NaturalDocs::Settings->CharSet() ?
+                '<meta http-equiv="Content-Type" content="text/html; charset=' . NaturalDocs::Settings->CharSet() . '">' : '')
+
+            . '<link rel="stylesheet" type="text/css" href="' . $self->MakeRelativeURL($self->SearchResultsDirectory(),
+                                                                                                                       $self->MainCSSFile()) . '">'
+
+            . '<script language=JavaScript src="' . $self->MakeRelativeURL($self->SearchResultsDirectory(),
+                                                                                                        $self->MainJavaScriptFile()) . '"></script>'
+
+        . '</head><body class="PopupSearchResultsPage" onLoad="NDOnLoad()">'
+            . $self->OpeningBrowserStyles()
+
+        . $self->StandardComments()
+
+        . "\n\n\n"
+
+        . '<div id=Index>';
+
+
+    my $endSearchResultsPage =
+        '</div>'
+        . $self->ClosingBrowserStyles()
+   . '</body></html>';
+
+    my $indexContent = NaturalDocs::SymbolTable->Index($type);
+    my $pageCount = $self->BuildIndexPages($type, $indexContent, $startIndexPage, $endIndexPage,
+                                                                  $startSearchResultsPage, $endSearchResultsPage);
+    $self->PurgeIndexFiles($type, $indexContent, $pageCount + 1);
+    };
+
+
+#
+#   Function: UpdateMenu
+#
+#   Updates the menu in all the output files that weren't rebuilt.  Also generates index.html.
+#
+sub UpdateMenu
+    {
+    my $self = shift;
+
+
+    # Update the menu on unbuilt files.
+
+    my $filesToUpdate = NaturalDocs::Project->UnbuiltFilesWithContent();
+
+    foreach my $sourceFile (keys %$filesToUpdate)
+        {
+        $self->UpdateFile($sourceFile);
+        };
+
+
+    # Update the menu on unchanged index files.
+
+    my $indexes = NaturalDocs::Menu->Indexes();
+
+    foreach my $index (keys %$indexes)
+        {
+        if (!NaturalDocs::SymbolTable->IndexChanged($index))
+            {
+            $self->UpdateIndex($index);
+            };
+        };
+
+
+    # Update index.html
+
+    my $firstMenuEntry = $self->FindFirstFile();
+    my $indexFile = NaturalDocs::File->JoinPaths( NaturalDocs::Settings->OutputDirectoryOf($self), 'index.html' );
+
+    # We have to check because it's possible that there may be no files with Natural Docs content and thus no files on the menu.
+    if (defined $firstMenuEntry)
+        {
+        open(INDEXFILEHANDLE, '>' . $indexFile)
+            or die "Couldn't create output file " . $indexFile . ".\n";
+
+        print INDEXFILEHANDLE
+        '<html><head>'
+             . '<meta http-equiv="Refresh" CONTENT="0; URL='
+                 . $self->MakeRelativeURL( NaturalDocs::File->JoinPaths( NaturalDocs::Settings->OutputDirectoryOf($self), 'index.html'),
+                                                        $self->OutputFileOf($firstMenuEntry->Target()), 1 ) . '">'
+        . '</head></html>';
+
+        close INDEXFILEHANDLE;
+        }
+
+    elsif (-e $indexFile)
+        {
+        unlink($indexFile);
+        };
+    };
+
+
+
+###############################################################################
+# Group: Support Functions
+
+
+#
+#   Function: UpdateFile
+#
+#   Updates an output file.  Replaces the menu, HTML title, and footer.  It opens the output file, makes the changes, and saves it
+#   back to disk, which is much quicker than rebuilding the file from scratch if these were the only things that changed.
+#
+#   Parameters:
+#
+#       sourceFile - The source <FileName>.
+#
+#   Dependencies:
+#
+#       - Requires <Builder::BuildMenu()> to surround its content with the exact strings "<div id=Menu>" and "</div><!--Menu-->".
+#       - Requires <Builder::BuildFooter()> to surround its content with the exact strings "<div id=Footer>" and
+#         "</div><!--Footer-->".
+#
+sub UpdateFile #(sourceFile)
+    {
+    my ($self, $sourceFile) = @_;
+
+    my $outputFile = $self->OutputFileOf($sourceFile);
+
+    if (open(OUTPUTFILEHANDLE, '<' . $outputFile))
+        {
+        my $content;
+
+        read(OUTPUTFILEHANDLE, $content, -s OUTPUTFILEHANDLE);
+        close(OUTPUTFILEHANDLE);
+
+
+        $content =~ s{<title>[^<]*<\/title>}{'<title>' . $self->BuildTitle($sourceFile) . '</title>'}e;
+
+        $content =~ s/<div id=Menu>.*?<\/div><!--Menu-->/$self->BuildMenu($sourceFile, undef)/es;
+
+        $content =~ s/<div id=Footer>.*?<\/div><!--Footer-->/$self->BuildFooter()/e;
+
+
+        open(OUTPUTFILEHANDLE, '>' . $outputFile);
+        print OUTPUTFILEHANDLE $content;
+        close(OUTPUTFILEHANDLE);
+        };
+    };
+
+
+#
+#   Function: UpdateIndex
+#
+#   Updates an index's output file.  Replaces the menu and footer.  It opens the output file, makes the changes, and saves it
+#   back to disk, which is much quicker than rebuilding the file from scratch if these were the only things that changed.
+#
+#   Parameters:
+#
+#       type - The index <TopicType>, or undef if none.
+#
+sub UpdateIndex #(type)
+    {
+    my ($self, $type) = @_;
+
+    my $page = 1;
+
+    my $outputFile = $self->IndexFileOf($type, $page);
+
+    my $newMenu = $self->BuildMenu(undef, $type);
+    my $newFooter = $self->BuildFooter();
+
+    while (-e $outputFile)
+        {
+        open(OUTPUTFILEHANDLE, '<' . $outputFile)
+            or die "Couldn't open output file " . $outputFile . ".\n";
+
+        my $content;
+
+        read(OUTPUTFILEHANDLE, $content, -s OUTPUTFILEHANDLE);
+        close(OUTPUTFILEHANDLE);
+
+
+        $content =~ s/<div id=Menu>.*?<\/div><!--Menu-->/$newMenu/es;
+
+        $content =~ s/<div id=Footer>.*<\/div><!--Footer-->/$newFooter/e;
+
+
+        open(OUTPUTFILEHANDLE, '>' . $outputFile)
+            or die "Couldn't save output file " . $outputFile . ".\n";
+
+        print OUTPUTFILEHANDLE $content;
+        close(OUTPUTFILEHANDLE);
+
+        $page++;
+        $outputFile = $self->IndexFileOf($type, $page);
+        };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Builder/HTMLBase.pm b/docs/tool/Modules/NaturalDocs/Builder/HTMLBase.pm
new file mode 100644
index 00000000..d943622b
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Builder/HTMLBase.pm
@@ -0,0 +1,3693 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Builder::HTMLBase
+#
+###############################################################################
+#
+#   A base package for all the shared functionality in <NaturalDocs::Builder::HTML> and
+#   <NaturalDocs::Builder::FramedHTML>.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+
+use Tie::RefHash;
+
+use strict;
+use integer;
+
+package NaturalDocs::Builder::HTMLBase;
+
+use base 'NaturalDocs::Builder::Base';
+
+use NaturalDocs::DefineMembers 'MADE_EMPTY_SEARCH_RESULTS_PAGE', 'MadeEmptySearchResultsPage()',
+                                                 'SetMadeEmptySearchResultsPage()';
+
+
+
+###############################################################################
+# Group: Object Variables
+
+
+#
+#   Constants: Members
+#
+#   The object is implemented as a blessed arrayref, with the follow constants as indexes.
+#
+#   MADE_EMPTY_SEARCH_RESULTS_PAGE - Whether the search results page for searches with no results was generated.
+#
+
+#
+#   Constants: NDMarkupToHTML Styles
+#
+#   These are the styles used with <NDMarkupToHTML()>.
+#
+#   NDMARKUPTOHTML_GENERAL - General style.
+#   NDMARKUPTOHTML_SUMMARY - For summaries.
+#   NDMARKUPTOHTML_TOOLTIP - For tooltips.
+#
+use constant NDMARKUPTOHTML_GENERAL => undef;
+use constant NDMARKUPTOHTML_SUMMARY => 1;
+use constant NDMARKUPTOHTML_TOOLTIP => 2;
+
+
+
+###############################################################################
+# Group: Package Variables
+# These variables are shared by all instances of the package so don't change them.
+
+
+#
+#   handle: FH_CSS_FILE
+#
+#   The file handle to use when updating CSS files.
+#
+
+
+#
+#   Hash: abbreviations
+#
+#   An existence hash of acceptable abbreviations.  These are words that <AddDoubleSpaces()> won't put a second space
+#   after when followed by period-whitespace-capital letter.  Yes, this is seriously over-engineered.
+#
+my %abbreviations = ( mr => 1, mrs => 1, ms => 1, dr => 1,
+                                  rev => 1, fr => 1, 'i.e' => 1,
+                                  maj => 1, gen => 1, pres => 1, sen => 1, rep => 1,
+                                  n => 1, s => 1, e => 1, w => 1, ne => 1, se => 1, nw => 1, sw => 1 );
+
+#
+#   array: indexHeadings
+#
+#   An array of the headings of all the index sections.  First is for symbols, second for numbers, and the rest for each letter.
+#
+my @indexHeadings = ( '$#!', '0-9', 'A' .. 'Z' );
+
+#
+#   array: indexAnchors
+#
+#   An array of the HTML anchors of all the index sections.  First is for symbols, second for numbers, and the rest for each letter.
+#
+my @indexAnchors = ( 'Symbols', 'Numbers', 'A' .. 'Z' );
+
+#
+#   array: searchExtensions
+#
+#   An array of the search file name extensions for all the index sections.  First is for symbols, second for numbers, and the rest
+#   for each letter.
+#
+my @searchExtensions = ( 'Symbols', 'Numbers', 'A' .. 'Z' );
+
+#
+#   bool: saidUpdatingCSSFile
+#
+#   Whether the status message "Updating CSS file..." has been displayed.  We only want to print it once, no matter how many
+#   HTML-based targets we are building.
+#
+my $saidUpdatingCSSFile;
+
+#
+#   constant: ADD_HIDDEN_BREAKS
+#
+#   Just a synonym for "1" so that setting the flag on <StringToHTML()> is clearer in the calling code.
+#
+use constant ADD_HIDDEN_BREAKS => 1;
+
+
+###############################################################################
+# Group: ToolTip Package Variables
+#
+#   These variables are for the tooltip generation functions only.  Since they're reset on every call to <BuildContent()> and
+#   <BuildIndexSections()>, and are only used by them and their support functions, they can be shared by all instances of the
+#   package.
+
+#
+#   int: tooltipLinkNumber
+#
+#   A number used as part of the ID for each link that has a tooltip.  Should be incremented whenever one is made.
+#
+my $tooltipLinkNumber;
+
+#
+#   int: tooltipNumber
+#
+#   A number used as part of the ID for each tooltip.  Should be incremented whenever one is made.
+#
+my $tooltipNumber;
+
+#
+#   hash: tooltipSymbolsToNumbers
+#
+#   A hash that maps the tooltip symbols to their assigned numbers.
+#
+my %tooltipSymbolsToNumbers;
+
+#
+#   string: tooltipHTML
+#
+#   The generated tooltip HTML.
+#
+my $tooltipHTML;
+
+
+###############################################################################
+# Group: Menu Package Variables
+#
+#   These variables are for the menu generation functions only.  Since they're reset on every call to <BuildMenu()> and are
+#   only used by it and its support functions, they can be shared by all instances of the package.
+#
+
+
+#
+#   hash: prebuiltMenus
+#
+#   A hash that maps output directonies to menu HTML already built for it.  There will be no selection or JavaScript in the menus.
+#
+my %prebuiltMenus;
+
+
+#
+#   bool: menuNumbersAndLengthsDone
+#
+#   Set when the variables that only need to be calculated for the menu once are done.  This includes <menuGroupNumber>,
+#   <menuLength>, <menuGroupLengths>, and <menuGroupNumbers>, and <menuRootLength>.
+#
+my $menuNumbersAndLengthsDone;
+
+
+#
+#   int: menuGroupNumber
+#
+#   The current menu group number.  Each time a group is created, this is incremented so that each one will be unique.
+#
+my $menuGroupNumber;
+
+
+#
+#   int: menuLength
+#
+#   The length of the entire menu, fully expanded.  The value is computed from the <Menu Length Constants>.
+#
+my $menuLength;
+
+
+#
+#   hash: menuGroupLengths
+#
+#   A hash of the length of each group, *not* including any subgroup contents.  The keys are references to each groups'
+#   <NaturalDocs::Menu::Entry> object, and the values are their lengths computed from the <Menu Length Constants>.
+#
+my %menuGroupLengths;
+tie %menuGroupLengths, 'Tie::RefHash';
+
+
+#
+#   hash: menuGroupNumbers
+#
+#   A hash of the number of each group, as managed by <menuGroupNumber>.  The keys are references to each groups'
+#   <NaturalDocs::Menu::Entry> object, and the values are the number.
+#
+my %menuGroupNumbers;
+tie %menuGroupNumbers, 'Tie::RefHash';
+
+
+#
+#   int: menuRootLength
+#
+#   The length of the top-level menu entries without expansion.  The value is computed from the <Menu Length Constants>.
+#
+my $menuRootLength;
+
+
+#
+#   constants: Menu Length Constants
+#
+#   Constants used to approximate the lengths of the menu or its groups.
+#
+#   MENU_TITLE_LENGTH       - The length of the title.
+#   MENU_SUBTITLE_LENGTH - The length of the subtitle.
+#   MENU_FILE_LENGTH         - The length of one file entry.
+#   MENU_GROUP_LENGTH     - The length of one group entry.
+#   MENU_TEXT_LENGTH        - The length of one text entry.
+#   MENU_LINK_LENGTH        - The length of one link entry.
+#
+#   MENU_LENGTH_LIMIT    - The limit of the menu's length.  If the total length surpasses this limit, groups that aren't required
+#                                       to be open to show the selection will default to closed on browsers that support it.
+#
+use constant MENU_TITLE_LENGTH => 3;
+use constant MENU_SUBTITLE_LENGTH => 1;
+use constant MENU_FILE_LENGTH => 1;
+use constant MENU_GROUP_LENGTH => 2; # because it's a line and a blank space
+use constant MENU_TEXT_LENGTH => 1;
+use constant MENU_LINK_LENGTH => 1;
+use constant MENU_INDEX_LENGTH => 1;
+
+use constant MENU_LENGTH_LIMIT => 35;
+
+
+###############################################################################
+# Group: Image Package Variables
+#
+#   These variables are for the image generation functions only.  Since they're reset on every call to <BuildContent()>,
+#   and are only used by it and its support functions, they can be shared by all instances of thepackage.
+
+
+#
+#   var: imageAnchorNumber
+#   Incremented for each image link in the file that requires an anchor.
+#
+my $imageAnchorNumber;
+
+
+#
+#   var: imageContent
+#
+#   The actual embedded image HTML for all image links.  When generating an image link, the link HTML is returned and
+#   the HTML for the target image is added here.  Periodically, such as after the end of the paragraph, this content should
+#   be added to the page and the variable set to undef.
+#
+my $imageContent;
+
+
+
+###############################################################################
+# Group: Search Package Variables
+#
+#   These variables are for the search generation functions only.  Since they're reset on every call to <BuildIndexSections()> and
+#   are only used by them and their support functions, they can be shared by all instances of the package.
+
+
+#
+#   hash: searchResultIDs
+#
+#   A hash mapping lowercase-only search result IDs to the number of times they've been used.  This is to work around an IE
+#   bug where it won't correctly reference IDs if they differ only in case.
+#
+my %searchResultIDs;
+
+
+
+###############################################################################
+# Group: Object Functions
+
+
+#
+#   Function: New
+#   Creates and returns a new object.
+#
+sub New
+    {
+    my $class = shift;
+
+    my $object = $class->SUPER::New();
+    $object->SetMadeEmptySearchResultsPage(0);
+
+    return $object;
+    };
+
+
+# Function: MadeEmptySearchResultsPage
+# Returns whether the empty search results page was created or not.
+
+# Function: SetMadeEmptySearchResultsPage
+# Sets whether the empty search results page was created or not.
+
+
+
+###############################################################################
+# Group: Implemented Interface Functions
+#
+#   The behavior of these functions is shared between HTML output formats.
+#
+
+
+#
+#   Function: UpdateImage
+#
+#   Define this function to add or update the passed image in the output.
+#
+#   Parameters:
+#
+#       file - The image <FileName>
+#
+sub UpdateImage #(file)
+    {
+    my ($self, $file) = @_;
+
+    my $outputFile = $self->OutputImageOf($file);
+    my $outputDirectory = NaturalDocs::File->NoFileName($outputFile);
+
+    if (!-d $outputDirectory)
+        {  NaturalDocs::File->CreatePath($outputDirectory);  };
+
+    NaturalDocs::File->Copy($file, $outputFile);
+    };
+
+
+#
+#   Function: PurgeFiles
+#
+#   Deletes the output files associated with the purged source files.
+#
+sub PurgeFiles #(filesToPurge)
+    {
+    my ($self, $filesToPurge) = @_;
+
+    # Combine directories into a hash to remove duplicate work.
+    my %directoriesToPurge;
+
+    foreach my $file (keys %$filesToPurge)
+        {
+        # It's possible that there may be files there that aren't in a valid input directory anymore.  They won't generate an output
+        # file name so we need to check for undef.
+        my $outputFile = $self->OutputFileOf($file);
+        if (defined $outputFile)
+            {
+            unlink($outputFile);
+            $directoriesToPurge{ NaturalDocs::File->NoFileName($outputFile) } = 1;
+            };
+        };
+
+    foreach my $directory (keys %directoriesToPurge)
+        {
+        NaturalDocs::File->RemoveEmptyTree($directory, NaturalDocs::Settings->OutputDirectoryOf($self));
+        };
+    };
+
+
+#
+#   Function: PurgeIndexes
+#
+#   Deletes the output files associated with the purged source files.
+#
+#   Parameters:
+#
+#       indexes  - An existence hashref of the index types to purge.  The keys are the <TopicTypes> or * for the general index.
+#
+sub PurgeIndexes #(indexes)
+    {
+    my ($self, $indexes) = @_;
+
+    foreach my $index (keys %$indexes)
+        {
+        $self->PurgeIndexFiles($index, undef, undef);
+        };
+    };
+
+
+#
+#   Function: PurgeImages
+#
+#   Define this function to make the package remove all output related to the passed image files.  These files are no longer used
+#   by the documentation.
+#
+#   Parameters:
+#
+#       files - An existence hashref of the image <FileNames> to purge.
+#
+sub PurgeImages #(files)
+    {
+    my ($self, $filesToPurge) = @_;
+
+    # Combine directories into a hash to remove duplicate work.
+    my %directoriesToPurge;
+
+    foreach my $file (keys %$filesToPurge)
+        {
+        # It's possible that there may be files there that aren't in a valid input directory anymore.  They won't generate an output
+        # file name so we need to check for undef.
+        my $outputFile = $self->OutputImageOf($file);
+        if (defined $outputFile)
+            {
+            unlink($outputFile);
+            $directoriesToPurge{ NaturalDocs::File->NoFileName($outputFile) } = 1;
+            };
+        };
+
+    foreach my $directory (keys %directoriesToPurge)
+        {
+        NaturalDocs::File->RemoveEmptyTree($directory, NaturalDocs::Settings->OutputDirectoryOf($self));
+        };
+    };
+
+
+#
+#   Function: BeginBuild
+#
+#   Creates the necessary subdirectories in the output directory.
+#
+sub BeginBuild #(hasChanged)
+    {
+    my ($self, $hasChanged) = @_;
+
+    foreach my $directory ( $self->JavaScriptDirectory(), $self->CSSDirectory(), $self->IndexDirectory(),
+                                       $self->SearchResultsDirectory() )
+        {
+        if (!-d $directory)
+            {  NaturalDocs::File->CreatePath($directory);  };
+        };
+    };
+
+
+#
+#   Function: EndBuild
+#
+#   Synchronizes the projects CSS and JavaScript files.  Also generates the search data JavaScript file.
+#
+sub EndBuild #(hasChanged)
+    {
+    my ($self, $hasChanged) = @_;
+
+
+    # Update the style sheets.
+
+    my $styles = NaturalDocs::Settings->Styles();
+    my $changed;
+
+    my $cssDirectory = $self->CSSDirectory();
+    my $mainCSSFile = $self->MainCSSFile();
+
+    for (my $i = 0; $i < scalar @$styles; $i++)
+        {
+        my $outputCSSFile;
+
+        if (scalar @$styles == 1)
+            {  $outputCSSFile = $mainCSSFile;  }
+        else
+            {  $outputCSSFile = NaturalDocs::File->JoinPaths($cssDirectory, ($i + 1) . '.css');  };
+
+
+        my $masterCSSFile = NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDirectory(), $styles->[$i] . '.css' );
+
+        if (! -e $masterCSSFile)
+            {  $masterCSSFile = NaturalDocs::File->JoinPaths( NaturalDocs::Settings->StyleDirectory(), $styles->[$i] . '.css' );  };
+
+        # We check both the date and the size in case the user switches between two styles which just happen to have the same
+        # date.  Should rarely happen, but it might.
+        if (! -e $outputCSSFile ||
+            (stat($masterCSSFile))[9] != (stat($outputCSSFile))[9] ||
+             -s $masterCSSFile != -s $outputCSSFile)
+            {
+            if (!NaturalDocs::Settings->IsQuiet() && !$saidUpdatingCSSFile)
+                {
+                print "Updating CSS file...\n";
+                $saidUpdatingCSSFile = 1;
+                };
+
+            NaturalDocs::File->Copy($masterCSSFile, $outputCSSFile);
+
+            $changed = 1;
+            };
+        };
+
+
+    my $deleteFrom;
+
+    if (scalar @$styles == 1)
+        {  $deleteFrom = 1;  }
+    else
+        {  $deleteFrom = scalar @$styles + 1;  };
+
+    for (;;)
+        {
+        my $file = NaturalDocs::File->JoinPaths($cssDirectory, $deleteFrom . '.css');
+
+        if (! -e $file)
+            {  last;  };
+
+        unlink ($file);
+        $deleteFrom++;
+
+        $changed = 1;
+        };
+
+
+    if ($changed)
+        {
+        if (scalar @$styles > 1)
+            {
+            open(FH_CSS_FILE, '>' . $mainCSSFile);
+
+            for (my $i = 0; $i < scalar @$styles; $i++)
+                {
+                print FH_CSS_FILE '@import URL("' . ($i + 1) . '.css");' . "\n";
+                };
+
+            close(FH_CSS_FILE);
+            };
+        };
+
+
+
+    # Update the JavaScript files
+
+    my $jsMaster = NaturalDocs::File->JoinPaths( NaturalDocs::Settings->JavaScriptDirectory(), 'NaturalDocs.js' );
+    my $jsOutput = $self->MainJavaScriptFile();
+
+    # We check both the date and the size in case the user switches between two styles which just happen to have the same
+    # date.  Should rarely happen, but it might.
+    if (! -e $jsOutput ||
+        (stat($jsMaster))[9] != (stat($jsOutput))[9] ||
+         -s $jsMaster != -s $jsOutput)
+        {
+        NaturalDocs::File->Copy($jsMaster, $jsOutput);
+        };
+
+
+    my @indexes = keys %{NaturalDocs::Menu->Indexes()};
+
+    open(FH_INDEXINFOJS, '>' . NaturalDocs::File->JoinPaths( $self->JavaScriptDirectory(), 'searchdata.js'));
+
+    print FH_INDEXINFOJS
+    "var indexSectionsWithContent = {\n";
+
+    for (my $i = 0; $i < scalar @indexes; $i++)
+        {
+        if ($i != 0)
+            {  print FH_INDEXINFOJS ",\n";  };
+
+        print FH_INDEXINFOJS '   "' . NaturalDocs::Topics->NameOfType($indexes[$i], 1, 1) . "\": {\n";
+
+        my $content = NaturalDocs::SymbolTable->IndexSectionsWithContent($indexes[$i]);
+        for (my $contentIndex = 0; $contentIndex < 28; $contentIndex++)
+            {
+            if ($contentIndex != 0)
+                {  print FH_INDEXINFOJS ",\n";  };
+
+            print FH_INDEXINFOJS '      "' . $searchExtensions[$contentIndex] . '": ' . ($content->[$contentIndex] ? 'true' : 'false');
+            };
+
+        print FH_INDEXINFOJS "\n      }";
+        };
+
+    print FH_INDEXINFOJS
+    "\n   }";
+
+    close(FH_INDEXINFOJS);
+    };
+
+
+
+###############################################################################
+# Group: Section Functions
+
+
+#
+#   Function: BuildTitle
+#
+#   Builds and returns the HTML page title of a file.
+#
+#   Parameters:
+#
+#       sourceFile - The source <FileName> to build the title of.
+#
+#   Returns:
+#
+#       The source file's title in HTML.
+#
+sub BuildTitle #(sourceFile)
+    {
+    my ($self, $sourceFile) = @_;
+
+    # If we have a menu title, the page title is [menu title] - [file title].  Otherwise it is just [file title].
+
+    my $title = NaturalDocs::Project->DefaultMenuTitleOf($sourceFile);
+
+    my $menuTitle = NaturalDocs::Menu->Title();
+    if (defined $menuTitle && $menuTitle ne $title)
+        {  $title .= ' - ' . $menuTitle;  };
+
+    $title = $self->StringToHTML($title);
+
+    return $title;
+    };
+
+#
+#   Function: BuildMenu
+#
+#   Builds and returns the side menu of a file.
+#
+#   Parameters:
+#
+#       sourceFile - The source <FileName> to use if you're looking for a source file.
+#       indexType - The index <TopicType> to use if you're looking for an index.
+#
+#       Both sourceFile and indexType may be undef.
+#
+#   Returns:
+#
+#       The side menu in HTML.
+#
+#   Dependencies:
+#
+#       - <Builder::HTML::UpdateFile()> and <Builder::HTML::UpdateIndex()> require this section to be surrounded with the exact
+#         strings "<div id=Menu>" and "</div><!--Menu-->".
+#       - This function depends on the way <BuildMenuSegment()> formats file and index entries.
+#
+sub BuildMenu #(FileName sourceFile, TopicType indexType) -> string htmlMenu
+    {
+    my ($self, $sourceFile, $indexType) = @_;
+
+    if (!$menuNumbersAndLengthsDone)
+        {
+        $menuGroupNumber = 1;
+        $menuLength = 0;
+        %menuGroupLengths = ( );
+        %menuGroupNumbers = ( );
+        $menuRootLength = 0;
+        };
+
+    my $outputDirectory;
+
+    if ($sourceFile)
+        {  $outputDirectory = NaturalDocs::File->NoFileName( $self->OutputFileOf($sourceFile) );  }
+    elsif ($indexType)
+        {  $outputDirectory = NaturalDocs::File->NoFileName( $self->IndexFileOf($indexType) );  }
+    else
+        {  $outputDirectory = NaturalDocs::Settings->OutputDirectoryOf($self);  };
+
+
+    # Comment needed for UpdateFile().
+    my $output = '<div id=Menu>';
+
+
+    if (!exists $prebuiltMenus{$outputDirectory})
+        {
+        my $segmentOutput;
+
+        ($segmentOutput, $menuRootLength) =
+            $self->BuildMenuSegment($outputDirectory, NaturalDocs::Menu->Content(), 1);
+
+        my $titleOutput;
+
+        my $menuTitle = NaturalDocs::Menu->Title();
+        if (defined $menuTitle)
+            {
+            if (!$menuNumbersAndLengthsDone)
+                {  $menuLength += MENU_TITLE_LENGTH;  };
+
+            $menuRootLength += MENU_TITLE_LENGTH;
+
+            $titleOutput .=
+            '<div class=MTitle>'
+                . $self->StringToHTML($menuTitle);
+
+            my $menuSubTitle = NaturalDocs::Menu->SubTitle();
+            if (defined $menuSubTitle)
+                {
+                if (!$menuNumbersAndLengthsDone)
+                    {  $menuLength += MENU_SUBTITLE_LENGTH;  };
+
+                $menuRootLength += MENU_SUBTITLE_LENGTH;
+
+                $titleOutput .=
+                '<div class=MSubTitle>'
+                    . $self->StringToHTML($menuSubTitle)
+                . '</div>';
+                };
+
+            $titleOutput .=
+            '</div>';
+            };
+
+        my $searchOutput;
+
+        if (scalar keys %{NaturalDocs::Menu->Indexes()})
+            {
+            $searchOutput =
+            '<script type="text/javascript"><!--' . "\n"
+                . 'var searchPanel = new SearchPanel("searchPanel", "' . $self->CommandLineOption() . '", '
+                    . '"' . $self->MakeRelativeURL($outputDirectory, $self->SearchResultsDirectory()) . '");' . "\n"
+            . '--></script>'
+
+            . '<div id=MSearchPanel class=MSearchPanelInactive>'
+                . '<input type=text id=MSearchField value=Search '
+                    . 'onFocus="searchPanel.OnSearchFieldFocus(true)" onBlur="searchPanel.OnSearchFieldFocus(false)" '
+                    . 'onKeyUp="searchPanel.OnSearchFieldChange()">'
+                . '<select id=MSearchType '
+                    . 'onFocus="searchPanel.OnSearchTypeFocus(true)" onBlur="searchPanel.OnSearchTypeFocus(false)" '
+                    . 'onChange="searchPanel.OnSearchTypeChange()">';
+
+                my @indexes = keys %{NaturalDocs::Menu->Indexes()};
+                @indexes = sort
+                    {
+                    if ($a eq ::TOPIC_GENERAL())  {  return -1;  }
+                    elsif ($b eq ::TOPIC_GENERAL())  {  return 1;  }
+                    else  {  return (NaturalDocs::Topics->NameOfType($a, 1) cmp NaturalDocs::Topics->NameOfType($b, 1))  };
+                    }  @indexes;
+
+                foreach my $index (@indexes)
+                    {
+                    my ($name, $extra);
+                    if ($index eq ::TOPIC_GENERAL())
+                        {
+                        $name = 'Everything';
+                        $extra = ' id=MSearchEverything selected ';
+                        }
+                    else
+                        {  $name = $self->ConvertAmpChars(NaturalDocs::Topics->NameOfType($index, 1));  }
+
+                    $searchOutput .=
+                    '<option ' . $extra . 'value="' . NaturalDocs::Topics->NameOfType($index, 1, 1) . '">'
+                        . $name
+                    . '</option>';
+                    };
+
+                $searchOutput .=
+                '</select>'
+            . '</div>';
+            };
+
+        $prebuiltMenus{$outputDirectory} = $titleOutput . $segmentOutput . $searchOutput;
+        $output .= $titleOutput . $segmentOutput . $searchOutput;
+        }
+    else
+        {  $output .= $prebuiltMenus{$outputDirectory};  };
+
+
+    # Highlight the menu selection.
+
+    if ($sourceFile)
+        {
+        # Dependency: This depends on how BuildMenuSegment() formats file entries.
+        my $outputFile = $self->OutputFileOf($sourceFile);
+        my $tag = '<div class=MFile><a href="' . $self->MakeRelativeURL($outputDirectory, $outputFile) . '">';
+        my $tagIndex = index($output, $tag);
+
+        if ($tagIndex != -1)
+            {
+            my $endIndex = index($output, '</a>', $tagIndex);
+
+            substr($output, $endIndex, 4, '');
+            substr($output, $tagIndex, length($tag), '<div class=MFile id=MSelected>');
+            };
+        }
+    elsif ($indexType)
+        {
+        # Dependency: This depends on how BuildMenuSegment() formats index entries.
+        my $outputFile = $self->IndexFileOf($indexType);
+        my $tag = '<div class=MIndex><a href="' . $self->MakeRelativeURL($outputDirectory, $outputFile) . '">';
+        my $tagIndex = index($output, $tag);
+
+        if ($tagIndex != -1)
+            {
+            my $endIndex = index($output, '</a>', $tagIndex);
+
+            substr($output, $endIndex, 4, '');
+            substr($output, $tagIndex, length($tag), '<div class=MIndex id=MSelected>');
+            };
+        };
+
+
+    # If the completely expanded menu is too long, collapse all the groups that aren't in the selection hierarchy or near the
+    # selection.  By doing this instead of having them default to closed via CSS, any browser that doesn't support changing this at
+    # runtime will keep the menu entirely open so that its still usable.
+
+    if ($menuLength > MENU_LENGTH_LIMIT())
+        {
+        my $menuSelectionHierarchy = $self->GetMenuSelectionHierarchy($sourceFile, $indexType);
+
+        my $toExpand = $self->ExpandMenu($sourceFile, $indexType, $menuSelectionHierarchy, $menuRootLength);
+
+        $output .=
+
+        '<script language=JavaScript><!--' . "\n"
+
+        . 'HideAllBut([' . join(', ', @$toExpand) . '], ' . $menuGroupNumber . ');'
+
+        . '// --></script>';
+        };
+
+    $output .= '</div><!--Menu-->';
+
+    $menuNumbersAndLengthsDone = 1;
+
+    return $output;
+    };
+
+
+#
+#   Function: BuildMenuSegment
+#
+#   A recursive function to build a segment of the menu.  *Remember to reset the <Menu Package Variables> before calling this
+#   for the first time.*
+#
+#   Parameters:
+#
+#       outputDirectory - The output directory the menu is being built for.
+#       menuSegment - An arrayref specifying the segment of the menu to build.  Either pass the menu itself or the contents
+#                               of a group.
+#       topLevel - Whether the passed segment is the top level segment or not.
+#
+#   Returns:
+#
+#       The array ( menuHTML, length ).
+#
+#       menuHTML - The menu segment in HTML.
+#       groupLength - The length of the group, *not* including the contents of any subgroups, as computed from the
+#                            <Menu Length Constants>.
+#
+#   Dependencies:
+#
+#       - <BuildMenu()> depends on the way this function formats file and index entries.
+#
+sub BuildMenuSegment #(outputDirectory, menuSegment, topLevel)
+    {
+    my ($self, $outputDirectory, $menuSegment, $topLevel) = @_;
+
+    my $output;
+    my $groupLength = 0;
+
+    foreach my $entry (@$menuSegment)
+        {
+        if ($entry->Type() == ::MENU_GROUP())
+            {
+            my ($entryOutput, $entryLength) =
+                $self->BuildMenuSegment($outputDirectory, $entry->GroupContent());
+
+            my $entryNumber;
+
+            if (!$menuNumbersAndLengthsDone)
+                {
+                $entryNumber = $menuGroupNumber;
+                $menuGroupNumber++;
+
+                $menuGroupLengths{$entry} = $entryLength;
+                $menuGroupNumbers{$entry} = $entryNumber;
+                }
+            else
+                {  $entryNumber = $menuGroupNumbers{$entry};  };
+
+            $output .=
+            '<div class=MEntry>'
+                . '<div class=MGroup>'
+
+                    . '<a href="javascript:ToggleMenu(\'MGroupContent' . $entryNumber . '\')"'
+                         . ($self->CommandLineOption() eq 'FramedHTML' ? ' target="_self"' : '') . '>'
+                        . $self->StringToHTML($entry->Title())
+                    . '</a>'
+
+                    . '<div class=MGroupContent id=MGroupContent' . $entryNumber . '>'
+                        . $entryOutput
+                    . '</div>'
+
+                . '</div>'
+            . '</div>';
+
+            $groupLength += MENU_GROUP_LENGTH;
+            }
+
+        elsif ($entry->Type() == ::MENU_FILE())
+            {
+            my $targetOutputFile = $self->OutputFileOf($entry->Target());
+
+        # Dependency: BuildMenu() depends on how this formats file entries.
+            $output .=
+            '<div class=MEntry>'
+                . '<div class=MFile>'
+                    . '<a href="' . $self->MakeRelativeURL($outputDirectory, $targetOutputFile) . '">'
+                        . $self->StringToHTML( $entry->Title(), ADD_HIDDEN_BREAKS)
+                    . '</a>'
+                . '</div>'
+            . '</div>';
+
+            $groupLength += MENU_FILE_LENGTH;
+            }
+
+        elsif ($entry->Type() == ::MENU_TEXT())
+            {
+            $output .=
+            '<div class=MEntry>'
+                . '<div class=MText>'
+                    . $self->StringToHTML( $entry->Title() )
+                . '</div>'
+            . '</div>';
+
+            $groupLength += MENU_TEXT_LENGTH;
+            }
+
+        elsif ($entry->Type() == ::MENU_LINK())
+            {
+            $output .=
+            '<div class=MEntry>'
+                . '<div class=MLink>'
+                    . '<a href="' . $entry->Target() . '"' . ($self->CommandLineOption() eq 'FramedHTML' ? ' target="_top"' : '') . '>'
+                        . $self->StringToHTML( $entry->Title() )
+                    . '</a>'
+                . '</div>'
+            . '</div>';
+
+            $groupLength += MENU_LINK_LENGTH;
+            }
+
+        elsif ($entry->Type() == ::MENU_INDEX())
+            {
+            my $indexFile = $self->IndexFileOf($entry->Target);
+
+        # Dependency: BuildMenu() depends on how this formats index entries.
+            $output .=
+            '<div class=MEntry>'
+                . '<div class=MIndex>'
+                    . '<a href="' . $self->MakeRelativeURL( $outputDirectory, $self->IndexFileOf($entry->Target()) ) . '">'
+                        . $self->StringToHTML( $entry->Title() )
+                    . '</a>'
+                . '</div>'
+            . '</div>';
+
+            $groupLength += MENU_INDEX_LENGTH;
+            };
+        };
+
+
+    if (!$menuNumbersAndLengthsDone)
+        {  $menuLength += $groupLength;  };
+
+    return ($output, $groupLength);
+    };
+
+
+#
+#   Function: BuildContent
+#
+#   Builds and returns the main page content.
+#
+#   Parameters:
+#
+#       sourceFile - The source <FileName>.
+#       parsedFile - The parsed source file as an arrayref of <NaturalDocs::Parser::ParsedTopic> objects.
+#
+#   Returns:
+#
+#       The page content in HTML.
+#
+sub BuildContent #(sourceFile, parsedFile)
+    {
+    my ($self, $sourceFile, $parsedFile) = @_;
+
+    $self->ResetToolTips();
+    $imageAnchorNumber = 1;
+    $imageContent = undef;
+
+    my $output = '<div id=Content>';
+    my $i = 0;
+
+    while ($i < scalar @$parsedFile)
+        {
+        my $anchor = $self->SymbolToHTMLSymbol($parsedFile->[$i]->Symbol());
+
+        my $scope = NaturalDocs::Topics->TypeInfo($parsedFile->[$i]->Type())->Scope();
+
+
+        # The anchors are closed, but not around the text, so the :hover CSS style won't accidentally kick in.
+
+        my $headerType;
+
+        if ($i == 0)
+            {  $headerType = 'h1';  }
+        elsif ($scope == ::SCOPE_START() || $scope == ::SCOPE_END())
+            {  $headerType = 'h2';  }
+        else
+            {  $headerType = 'h3';  };
+
+        $output .=
+
+        '<div class="C' . NaturalDocs::Topics->NameOfType($parsedFile->[$i]->Type(), 0, 1) . '">'
+            . '<div class=CTopic' . ($i == 0 ? ' id=MainTopic' : '') . '>'
+
+                . '<' . $headerType . ' class=CTitle>'
+                    . '<a name="' . $anchor . '"></a>'
+                    . $self->StringToHTML( $parsedFile->[$i]->Title(), ADD_HIDDEN_BREAKS)
+                . '</' . $headerType . '>';
+
+
+        my $hierarchy;
+        if (NaturalDocs::Topics->TypeInfo( $parsedFile->[$i]->Type() )->ClassHierarchy())
+            {
+            $hierarchy = $self->BuildClassHierarchy($sourceFile, $parsedFile->[$i]->Symbol());
+            };
+
+        my $summary;
+        if ($i == 0 || $scope == ::SCOPE_START() || $scope == ::SCOPE_END())
+            {
+            $summary .= $self->BuildSummary($sourceFile, $parsedFile, $i);
+            };
+
+        my $hasBody;
+        if (defined $hierarchy || defined $summary ||
+            defined $parsedFile->[$i]->Prototype() || defined $parsedFile->[$i]->Body())
+            {
+            $output .= '<div class=CBody>';
+            $hasBody = 1;
+            };
+
+        $output .= $hierarchy;
+
+        if (defined $parsedFile->[$i]->Prototype())
+            {
+            $output .= $self->BuildPrototype($parsedFile->[$i]->Type(), $parsedFile->[$i]->Prototype(), $sourceFile);
+            };
+
+        if (defined $parsedFile->[$i]->Body())
+            {
+            $output .= $self->NDMarkupToHTML( $sourceFile, $parsedFile->[$i]->Body(), $parsedFile->[$i]->Symbol(),
+                                                                  $parsedFile->[$i]->Package(), $parsedFile->[$i]->Type(),
+                                                                  $parsedFile->[$i]->Using() );
+            };
+
+        $output .= $summary;
+
+
+        if ($hasBody)
+            {  $output .= '</div>';  };
+
+        $output .=
+            '</div>' # CTopic
+        . '</div>' # CType
+        . "\n\n";
+
+        $i++;
+        };
+
+    $output .= '</div><!--Content-->';
+
+    return $output;
+    };
+
+
+#
+#   Function: BuildSummary
+#
+#   Builds a summary, either for the entire file or the current class/section.
+#
+#   Parameters:
+#
+#       sourceFile - The source <FileName> the summary appears in.
+#
+#       parsedFile - A reference to the parsed source file.
+#
+#       index   - The index into the parsed file to start at.  If undef or zero, it builds a summary for the entire file.  If it's the
+#                    index of a <TopicType> that starts or ends a scope, it builds a summary for that scope
+#
+#   Returns:
+#
+#       The summary in HTML.
+#
+sub BuildSummary #(sourceFile, parsedFile, index)
+    {
+    my ($self, $sourceFile, $parsedFile, $index) = @_;
+    my $completeSummary;
+
+    if (!defined $index || $index == 0)
+        {
+        $index = 0;
+        $completeSummary = 1;
+        }
+    else
+        {
+        # Skip the scope entry.
+        $index++;
+        };
+
+    if ($index + 1 >= scalar @$parsedFile)
+        {  return undef;  };
+
+
+    my $scope = NaturalDocs::Topics->TypeInfo($parsedFile->[$index]->Type())->Scope();
+
+    # Return nothing if there's only one entry.
+    if (!$completeSummary && ($scope == ::SCOPE_START() || $scope == ::SCOPE_END()) )
+        {  return undef;  };
+
+
+    my $indent = 0;
+    my $inGroup;
+
+    my $isMarked = 0;
+
+    my $output =
+    '<!--START_ND_SUMMARY-->'
+    . '<div class=Summary><div class=STitle>Summary</div>'
+
+        # Not all browsers get table padding right, so we need a div to apply the border.
+        . '<div class=SBorder>'
+        . '<table border=0 cellspacing=0 cellpadding=0 class=STable>';
+
+        while ($index < scalar @$parsedFile)
+            {
+            my $topic = $parsedFile->[$index];
+            my $scope = NaturalDocs::Topics->TypeInfo($topic->Type())->Scope();
+
+            if (!$completeSummary && ($scope == ::SCOPE_START() || $scope == ::SCOPE_END()) )
+                {  last;  };
+
+
+            # Remove modifiers as appropriate for the current entry.
+
+            if ($scope == ::SCOPE_START() || $scope == ::SCOPE_END())
+                {
+                $indent = 0;
+                $inGroup = 0;
+                $isMarked = 0;
+                }
+            elsif ($topic->Type() eq ::TOPIC_GROUP())
+                {
+                if ($inGroup)
+                    {  $indent--;  };
+
+                $inGroup = 0;
+                $isMarked = 0;
+                };
+
+
+            $output .=
+             '<tr class="S' . ($index == 0 ? 'Main' : NaturalDocs::Topics->NameOfType($topic->Type(), 0, 1))
+                . ($indent ? ' SIndent' . $indent : '') . ($isMarked ? ' SMarked' : '') .'">'
+                . '<td class=SEntry>';
+
+           # Add the entry itself.
+
+            my $toolTipProperties;
+
+            # We only want a tooltip here if there's a protoype.  Otherwise it's redundant.
+
+            if (defined $topic->Prototype())
+                {
+                my $tooltipID = $self->BuildToolTip($topic->Symbol(), $sourceFile, $topic->Type(),
+                                                                     $topic->Prototype(), $topic->Summary());
+                $toolTipProperties = $self->BuildToolTipLinkProperties($tooltipID);
+                };
+
+            $output .=
+            '<a href="#' . $self->SymbolToHTMLSymbol($parsedFile->[$index]->Symbol()) . '" ' . $toolTipProperties . '>'
+                . $self->StringToHTML( $parsedFile->[$index]->Title(), ADD_HIDDEN_BREAKS)
+            . '</a>';
+
+
+            $output .=
+            '</td><td class=SDescription>';
+
+            if (defined $parsedFile->[$index]->Body())
+                {
+                $output .= $self->NDMarkupToHTML($sourceFile, $parsedFile->[$index]->Summary(),
+                                                                     $parsedFile->[$index]->Symbol(), $parsedFile->[$index]->Package(),
+                                                                     $parsedFile->[$index]->Type(), $parsedFile->[$index]->Using(),
+                                                                     NDMARKUPTOHTML_SUMMARY);
+                };
+
+
+            $output .=
+            '</td></tr>';
+
+
+            # Prepare the modifiers for the next entry.
+
+            if ($scope == ::SCOPE_START() || $scope == ::SCOPE_END())
+                {
+                $indent = 1;
+                $inGroup = 0;
+                }
+            elsif ($topic->Type() eq ::TOPIC_GROUP())
+                {
+                if (!$inGroup)
+                    {
+                    $indent++;
+                    $inGroup = 1;
+                    };
+                };
+
+            $isMarked ^= 1;
+            $index++;
+            };
+
+        $output .=
+        '</table>'
+    . '</div>' # Body
+    . '</div>' # Summary
+    . "<!--END_ND_SUMMARY-->";
+
+    return $output;
+    };
+
+
+#
+#   Function: BuildPrototype
+#
+#   Builds and returns the prototype as HTML.
+#
+#   Parameters:
+#
+#       type - The <TopicType> the prototype is from.
+#       prototype - The prototype to format.
+#       file - The <FileName> the prototype was defined in.
+#
+#   Returns:
+#
+#       The prototype in HTML.
+#
+sub BuildPrototype #(type, prototype, file)
+    {
+    my ($self, $type, $prototype, $file) = @_;
+
+    my $language = NaturalDocs::Languages->LanguageOf($file);
+    my $prototypeObject = $language->ParsePrototype($type, $prototype);
+
+    my $output;
+
+    if ($prototypeObject->OnlyBeforeParameters())
+        {
+        $output =
+        # A blockquote to scroll it if it's too long.
+        '<blockquote>'
+            # A surrounding table as a hack to make the div form-fit.
+            . '<table border=0 cellspacing=0 cellpadding=0 class=Prototype><tr><td>'
+                . $self->ConvertAmpChars($prototypeObject->BeforeParameters())
+            . '</td></tr></table>'
+        . '</blockquote>';
+        }
+
+    else
+        {
+        my $params = $prototypeObject->Parameters();
+        my $beforeParams = $prototypeObject->BeforeParameters();
+        my $afterParams = $prototypeObject->AfterParameters();
+
+
+        # Determine what features the prototype has and its length.
+
+        my ($hasType, $hasTypePrefix, $hasNamePrefix, $hasDefaultValue, $hasDefaultValuePrefix);
+        my $maxParamLength = 0;
+
+        foreach my $param (@$params)
+            {
+            my $paramLength = length($param->Name());
+
+            if ($param->Type())
+                {
+                $hasType = 1;
+                $paramLength += length($param->Type()) + 1;
+                };
+            if ($param->TypePrefix())
+                {
+                $hasTypePrefix = 1;
+                $paramLength += length($param->TypePrefix()) + 1;
+                };
+            if ($param->NamePrefix())
+                {
+                $hasNamePrefix = 1;
+                $paramLength += length($param->NamePrefix());
+                };
+            if ($param->DefaultValue())
+                {
+                $hasDefaultValue = 1;
+
+                # The length of the default value part is either the longest word, or 1/3 the total, whichever is longer.  We do this
+                # because we don't want parameter lines wrapping to more than three lines, and there's no guarantee that the line will
+                # wrap at all.  There's a small possibility that it could still wrap to four lines with this code, but we don't need to go
+                # crazy(er) here.
+
+                my $thirdLength = length($param->DefaultValue()) / 3;
+
+                my @words = split(/ +/, $param->DefaultValue());
+                my $maxWordLength = 0;
+
+                foreach my $word (@words)
+                    {
+                    if (length($word) > $maxWordLength)
+                        {  $maxWordLength = length($word);  };
+                    };
+
+                $paramLength += ($maxWordLength > $thirdLength ? $maxWordLength : $thirdLength) + 1;
+                };
+            if ($param->DefaultValuePrefix())
+                {
+                $hasDefaultValuePrefix = 1;
+                $paramLength += length($param->DefaultValuePrefix()) + 1;
+                };
+
+            if ($paramLength > $maxParamLength)
+                {  $maxParamLength = $paramLength;  };
+            };
+
+        my $useCondensed = (length($beforeParams) + $maxParamLength + length($afterParams) > 80 ? 1 : 0);
+        my $parameterColumns = 1 + $hasType + $hasTypePrefix + $hasNamePrefix +
+                                               $hasDefaultValue + $hasDefaultValuePrefix + $useCondensed;
+
+        $output =
+        '<blockquote><table border=0 cellspacing=0 cellpadding=0 class=Prototype><tr><td>'
+
+            # Stupid hack to get it to work right in IE.
+            . '<table border=0 cellspacing=0 cellpadding=0><tr>'
+
+            . '<td class=PBeforeParameters ' . ($useCondensed ? 'colspan=' . $parameterColumns : 'nowrap') . '>'
+                . $self->ConvertAmpChars($beforeParams);
+
+                if ($beforeParams && $beforeParams !~ /[\(\[\{\<]$/)
+                    {  $output .= '&nbsp;';  };
+
+            $output .=
+            '</td>';
+
+            for (my $i = 0; $i < scalar @$params; $i++)
+                {
+                if ($useCondensed)
+                    {
+                    $output .= '</tr><tr><td>&nbsp;&nbsp;&nbsp;</td>';
+                    }
+                elsif ($i > 0)
+                    {
+                    # Go to the next row and and skip the BeforeParameters cell.
+                    $output .= '</tr><tr><td></td>';
+                    };
+
+                if ($language->TypeBeforeParameter())
+                    {
+                    if ($hasTypePrefix)
+                        {
+                        my $htmlTypePrefix = $self->ConvertAmpChars($params->[$i]->TypePrefix());
+                        $htmlTypePrefix =~ s/ $/&nbsp;/;
+
+                        $output .=
+                        '<td class=PTypePrefix nowrap>'
+                            . $htmlTypePrefix
+                        . '</td>';
+                        };
+
+                    if ($hasType)
+                        {
+                        $output .=
+                        '<td class=PType nowrap>'
+                            . $self->ConvertAmpChars($params->[$i]->Type()) . '&nbsp;'
+                        . '</td>';
+                        };
+
+                    if ($hasNamePrefix)
+                        {
+                        $output .=
+                        '<td class=PParameterPrefix nowrap>'
+                            . $self->ConvertAmpChars($params->[$i]->NamePrefix())
+                        . '</td>';
+                        };
+
+                    $output .=
+                    '<td class=PParameter nowrap' . ($useCondensed && !$hasDefaultValue ? ' width=100%' : '') . '>'
+                        . $self->ConvertAmpChars($params->[$i]->Name())
+                    . '</td>';
+                    }
+
+                else # !$language->TypeBeforeParameter()
+                    {
+                    $output .=
+                    '<td class=PParameter nowrap>'
+                        . $self->ConvertAmpChars( $params->[$i]->NamePrefix() . $params->[$i]->Name() )
+                    . '</td>';
+
+                    if ($hasType || $hasTypePrefix)
+                        {
+                        my $typePrefix = $params->[$i]->TypePrefix();
+                        if ($typePrefix)
+                            {  $typePrefix .= ' ';  };
+
+                        $output .=
+                        '<td class=PType nowrap' . ($useCondensed && !$hasDefaultValue ? ' width=100%' : '') . '>'
+                            . '&nbsp;' . $self->ConvertAmpChars( $typePrefix . $params->[$i]->Type() )
+                        . '</td>';
+                        };
+                    };
+
+                if ($hasDefaultValuePrefix)
+                    {
+                    $output .=
+                    '<td class=PDefaultValuePrefix>'
+
+                       . '&nbsp;' . $self->ConvertAmpChars( $params->[$i]->DefaultValuePrefix() ) . '&nbsp;'
+                    . '</td>';
+                    };
+
+                if ($hasDefaultValue)
+                    {
+                    $output .=
+                    '<td class=PDefaultValue width=100%>'
+                        . ($hasDefaultValuePrefix ? '' : '&nbsp;') . $self->ConvertAmpChars( $params->[$i]->DefaultValue() )
+                    . '</td>';
+                    };
+                };
+
+            if ($useCondensed)
+                {  $output .= '</tr><tr>';  };
+
+            $output .=
+            '<td class=PAfterParameters ' . ($useCondensed ? 'colspan=' . $parameterColumns : 'nowrap') . '>'
+                 . $self->ConvertAmpChars($afterParams);
+
+                if ($afterParams && $afterParams !~ /^[\)\]\}\>]/)
+                    {  $output .= '&nbsp;';  };
+
+            $output .=
+            '</td>'
+        . '</tr></table>'
+
+        # Hack.
+        . '</td></tr></table></blockquote>';
+       };
+
+    return $output;
+    };
+
+
+#
+#   Function: BuildFooter
+#
+#   Builds and returns the HTML footer for the page.
+#
+#   Parameters:
+#
+#       multiline - Whether it should be formatted on multiple lines or not.
+#
+#   Dependencies:
+#
+#       <Builder::HTML::UpdateFile()> and <Builder::HTML::UpdateIndex()> require this section to be surrounded with the exact
+#       strings "<div id=Footer>" and "</div><!--Footer-->".
+#
+sub BuildFooter #(bool multiline)
+    {
+    my ($self, $multiline) = @_;
+
+    my $footer = NaturalDocs::Menu->Footer();
+    my $timestamp = NaturalDocs::Menu->TimeStamp();
+    my $divider;
+
+    if ($multiline)
+        {  $divider = '</p><p>';  }
+    else
+        {  $divider = '&nbsp; &middot;&nbsp; ';  };
+
+
+    my $output = '<div id=Footer>';
+    if ($multiline)
+        {  $output .= '<p>';  };
+
+    if (defined $footer)
+        {
+        $footer =~ s/\(c\)/&copy;/gi;
+        $footer =~ s/\(tm\)/&trade;/gi;
+        $footer =~ s/\(r\)/&reg;/gi;
+
+        $output .= $footer . $divider;
+        };
+
+    if (defined $timestamp)
+        {
+        $output .= $timestamp . $divider;
+        };
+
+    $output .=
+    '<a href="' . NaturalDocs::Settings->AppURL() . '">'
+        . 'Generated by Natural Docs'
+    . '</a>';
+
+    if ($multiline)
+        {  $output .= '</p>';  };
+
+    $output .=
+    '</div><!--Footer-->';
+
+    return $output;
+    };
+
+
+#
+#   Function: BuildToolTip
+#
+#   Builds the HTML for a symbol's tooltip and stores it in <tooltipHTML>.
+#
+#   Parameters:
+#
+#       symbol - The target <SymbolString>.
+#       file - The <FileName> the target's defined in.
+#       type - The symbol <TopicType>.
+#       prototype - The target prototype, or undef for none.
+#       summary - The target summary, or undef for none.
+#
+#   Returns:
+#
+#       If a tooltip is necessary for the link, returns the tooltip ID.  If not, returns undef.
+#
+sub BuildToolTip #(symbol, file, type, prototype, summary)
+    {
+    my ($self, $symbol, $file, $type, $prototype, $summary) = @_;
+
+    if (defined $prototype || defined $summary)
+        {
+        my $htmlSymbol = $self->SymbolToHTMLSymbol($symbol);
+        my $number = $tooltipSymbolsToNumbers{$htmlSymbol};
+
+        if (!defined $number)
+            {
+            $number = $tooltipNumber;
+            $tooltipNumber++;
+
+            $tooltipSymbolsToNumbers{$htmlSymbol} = $number;
+
+            $tooltipHTML .=
+            '<div class=CToolTip id="tt' . $number . '">'
+                . '<div class=C' . NaturalDocs::Topics->NameOfType($type, 0, 1) . '>';
+
+            if (defined $prototype)
+                {
+                $tooltipHTML .= $self->BuildPrototype($type, $prototype, $file);
+                };
+
+            if (defined $summary)
+                {
+                # The fact that we don't have scope or using shouldn't matter because links shouldn't be included in the style anyway.
+                $summary = $self->NDMarkupToHTML($file, $summary, undef, undef, $type, undef, NDMARKUPTOHTML_TOOLTIP);
+                $tooltipHTML .= $summary;
+                };
+
+            $tooltipHTML .=
+                '</div>'
+            . '</div>';
+            };
+
+        return 'tt' . $number;
+        }
+    else
+        {  return undef;  };
+    };
+
+#
+#   Function: BuildToolTips
+#
+#   Builds and returns the tooltips for the page in HTML.
+#
+sub BuildToolTips
+    {
+    my $self = shift;
+    return "\n<!--START_ND_TOOLTIPS-->\n" . $tooltipHTML . "<!--END_ND_TOOLTIPS-->\n\n";
+    };
+
+#
+#   Function: BuildClassHierarchy
+#
+#   Builds and returns a class hierarchy diagram for the passed class, if applicable.
+#
+#   Parameters:
+#
+#       file - The source <FileName>.
+#       class - The class <SymbolString> to build the hierarchy of.
+#
+sub BuildClassHierarchy #(file, symbol)
+    {
+    my ($self, $file, $symbol) = @_;
+
+    my @parents = NaturalDocs::ClassHierarchy->ParentsOf($symbol);
+    @parents = sort { ::StringCompare($a, $b) } @parents;
+
+    my @children = NaturalDocs::ClassHierarchy->ChildrenOf($symbol);
+    @children = sort { ::StringCompare($a, $b) } @children;
+
+    if (!scalar @parents && !scalar @children)
+        {  return undef;  };
+
+    my $output =
+    '<div class=ClassHierarchy>';
+
+    if (scalar @parents)
+        {
+        $output .='<table border=0 cellspacing=0 cellpadding=0><tr><td>';
+
+        foreach my $parent (@parents)
+            {
+            $output .= $self->BuildClassHierarchyEntry($file, $parent, 'CHParent', 1);
+            };
+
+        $output .= '</td></tr></table><div class=CHIndent>';
+        };
+
+    $output .=
+    '<table border=0 cellspacing=0 cellpadding=0><tr><td>'
+        . $self->BuildClassHierarchyEntry($file, $symbol, 'CHCurrent', undef)
+    . '</td></tr></table>';
+
+    if (scalar @children)
+        {
+        $output .=
+        '<div class=CHIndent>'
+            . '<table border=0 cellspacing=0 cellpadding=0><tr><td>';
+
+        if (scalar @children <= 5)
+            {
+            for (my $i = 0; $i < scalar @children; $i++)
+                {  $output .= $self->BuildClassHierarchyEntry($file, $children[$i], 'CHChild', 1);  };
+            }
+        else
+            {
+            for (my $i = 0; $i < 4; $i++)
+                {  $output .= $self->BuildClassHierarchyEntry($file, $children[$i], 'CHChild', 1);  };
+
+           $output .= '<div class=CHChildNote><div class=CHEntry>' . (scalar @children - 4) . ' other children</div></div>';
+            };
+
+        $output .=
+        '</td></tr></table>'
+        . '</div>';
+        };
+
+    if (scalar @parents)
+        {  $output .= '</div>';  };
+
+    $output .=
+    '</div>';
+
+    return $output;
+    };
+
+
+#
+#   Function: BuildClassHierarchyEntry
+#
+#   Builds and returns a single class hierarchy entry.
+#
+#   Parameters:
+#
+#       file - The source <FileName>.
+#       symbol - The class <SymbolString> whose entry is getting built.
+#       style - The style to apply to the entry, such as <CHParent>.
+#       link - Whether to build a link for this class or not.  When building the selected class' entry, this should be false.  It will
+#               automatically handle whether the symbol is defined or not.
+#
+sub BuildClassHierarchyEntry #(file, symbol, style, link)
+    {
+    my ($self, $file, $symbol, $style, $link) = @_;
+
+    my @identifiers = NaturalDocs::SymbolString->IdentifiersOf($symbol);
+    my $name = join(NaturalDocs::Languages->LanguageOf($file)->PackageSeparator(), @identifiers);
+    $name = $self->StringToHTML($name);
+
+    my $output = '<div class=' . $style . '><div class=CHEntry>';
+
+    if ($link)
+        {
+        my $target = NaturalDocs::SymbolTable->Lookup($symbol, $file);
+
+        if (defined $target)
+            {
+            my $targetFile;
+
+            if ($target->File() ne $file)
+                {  $targetFile = $self->MakeRelativeURL( $self->OutputFileOf($file), $self->OutputFileOf($target->File()), 1 );  };
+            # else leave it undef
+
+            my $targetTooltipID = $self->BuildToolTip($symbol, $target->File(), $target->Type(),
+                                                                          $target->Prototype(), $target->Summary());
+
+            my $toolTipProperties = $self->BuildToolTipLinkProperties($targetTooltipID);
+
+            $output .= '<a href="' . $targetFile . '#' . $self->SymbolToHTMLSymbol($symbol) . '" '
+                            . 'class=L' . NaturalDocs::Topics->NameOfType($target->Type(), 0, 1) . ' ' . $toolTipProperties . '>'
+                            . $name . '</a>';
+            }
+        else
+            {  $output .= $name;  };
+        }
+    else
+        {  $output .= $name;  };
+
+    $output .= '</div></div>';
+    return $output;
+    };
+
+
+#
+#   Function: OpeningBrowserStyles
+#
+#   Returns the JavaScript that will add opening browser styles if necessary.
+#
+sub OpeningBrowserStyles
+    {
+    my $self = shift;
+
+    return
+
+    '<script language=JavaScript><!--' . "\n"
+
+        # IE 4 and 5 don't understand 'undefined', so you can't say '!= undefined'.
+        . 'if (browserType) {'
+            . 'document.write("<div class=" + browserType + ">");'
+            . 'if (browserVer) {'
+                . 'document.write("<div class=" + browserVer + ">"); }'
+            . '}'
+
+    . '// --></script>';
+    };
+
+
+#
+#   Function: ClosingBrowserStyles
+#
+#   Returns the JavaScript that will close browser styles if necessary.
+#
+sub ClosingBrowserStyles
+    {
+    my $self = shift;
+
+    return
+
+    '<script language=JavaScript><!--' . "\n"
+
+        . 'if (browserType) {'
+            . 'if (browserVer) {'
+                . 'document.write("</div>"); }'
+            . 'document.write("</div>");'
+            . '}'
+
+    . '// --></script>';
+    };
+
+
+#
+#   Function: StandardComments
+#
+#   Returns the standard HTML comments that should be included in every generated file.  This includes <IEWebMark()>, so this
+#   really is required for proper functionality.
+#
+sub StandardComments
+    {
+    my $self = shift;
+
+    return "\n\n"
+
+        . '<!--  Generated by Natural Docs, version ' . NaturalDocs::Settings->TextAppVersion() . ' -->' . "\n"
+        . '<!--  ' . NaturalDocs::Settings->AppURL() . '  -->' . "\n\n"
+        . $self->IEWebMark() . "\n\n";
+    };
+
+
+#
+#   Function: IEWebMark
+#
+#   Returns the HTML comment necessary to get around the security warnings in IE starting with Windows XP Service Pack 2.
+#
+#   With this mark, the HTML page is treated as if it were in the Internet security zone instead of the Local Machine zone.  This
+#   prevents the lockdown on scripting that causes an error message to appear with each page.
+#
+#   More Information:
+#
+#       - http://www.microsoft.com/technet/prodtechnol/winxppro/maintain/sp2brows.mspx#EHAA
+#       - http://www.phdcc.com/xpsp2.htm#markoftheweb
+#
+sub IEWebMark
+    {
+    my $self = shift;
+
+    return '<!-- saved from url=(0026)http://www.naturaldocs.org -->';
+    };
+
+
+
+###############################################################################
+# Group: Index Functions
+
+
+#
+#   Function: BuildIndexPages
+#
+#   Builds an index file or files.
+#
+#   Parameters:
+#
+#       type - The <TopicType> the index is limited to, or undef for none.
+#       indexSections  - An arrayref of sections, each section being an arrayref <NaturalDocs::SymbolTable::IndexElement>
+#                               objects.  The first section is for symbols, the second for numbers, and the rest for A through Z.
+#       beginIndexPage - All the content of the HTML page up to where the index content should appear.
+#       endIndexPage - All the content of the HTML page past where the index should appear.
+#       beginSearchResultsPage - All the content of the HTML page up to where the search results content should appear.
+#       endSearchResultsPage - All the content of the HTML page past where the search results content should appear.
+#
+#   Returns:
+#
+#       The number of pages in the index.
+#
+sub BuildIndexPages #(TopicType type, NaturalDocs::SymbolTable::IndexElement[] indexSections, string beginIndexPage, string endIndexPage, string beginSearchResultsPage, string endSearchResultsPage) => int
+    {
+    my ($self, $type, $indexSections, $beginIndexPage, $endIndexPage, $beginSearchResultsPage, $endSearchResultsPage) = @_;
+
+
+    # Build the content.
+
+    my ($indexHTMLSections, $tooltipHTMLSections, $searchResultsHTMLSections) = $self->BuildIndexSections($indexSections);
+
+
+    # Generate the search result pages.
+
+   for (my $i = 0; $i < 28; $i++)
+        {
+        if ($searchResultsHTMLSections->[$i])
+            {
+            my $searchResultsFileName = $self->SearchResultsFileOf($type, $searchExtensions[$i]);
+
+            open(INDEXFILEHANDLE, '>' . $searchResultsFileName)
+                or die "Couldn't create output file " . $searchResultsFileName . ".\n";
+
+            print INDEXFILEHANDLE
+
+            $beginSearchResultsPage
+
+            . '<div class=SRStatus id=Loading>Loading...</div>'
+
+            . '<table border=0 cellspacing=0 cellpadding=0>'
+                . $searchResultsHTMLSections->[$i]
+            . '</table>'
+
+            . '<div class=SRStatus id=Searching>Searching...</div>'
+            . '<div class=SRStatus id=NoMatches>No Matches</div>'
+
+            . '<script type="text/javascript"><!--' . "\n"
+                . 'document.getElementById("Loading").style.display="none";' . "\n"
+                . 'document.getElementById("NoMatches").style.display="none";' . "\n"
+
+                . 'var searchResults = new SearchResults("searchResults", "' . $self->CommandLineOption() . '");' . "\n"
+                . 'searchResults.Search();' . "\n"
+            . '--></script>'
+
+            . $endSearchResultsPage;
+
+            close(INDEXFILEHANDLE);
+            };
+        };
+
+    if (!$self->MadeEmptySearchResultsPage())
+        {
+        my $emptySearchResultsFileName = NaturalDocs::File->JoinPaths( $self->SearchResultsDirectory(), 'NoResults.html' );
+
+        open(INDEXFILEHANDLE, '>' . $emptySearchResultsFileName)
+            or die "Couldn't create output file " . $emptySearchResultsFileName . ".\n";
+
+        print INDEXFILEHANDLE
+
+        $beginSearchResultsPage
+        . '<div class=SRStatus id=NoMatches>No Matches</div>'
+        . $endSearchResultsPage;
+
+        close(INDEXFILEHANDLE);
+
+        $self->SetMadeEmptySearchResultsPage(1);
+        };
+
+
+    # Generate the index pages.
+
+    my $page = 1;
+    my $pageSize = 0;
+    my @pageLocations;
+
+    # The maximum page size acceptable before starting a new page.  Note that this doesn't include beginPage and endPage,
+    # because we don't want something like a large menu screwing up the calculations.
+    use constant PAGESIZE_LIMIT => 50000;
+
+
+    # File the pages.
+
+    for (my $i = 0; $i < scalar @$indexHTMLSections; $i++)
+        {
+        if (!defined $indexHTMLSections->[$i])
+            {  next;  };
+
+        $pageSize += length($indexHTMLSections->[$i]) + length($tooltipHTMLSections->[$i]);
+        $pageLocations[$i] = $page;
+
+        if ($pageSize + length($indexHTMLSections->[$i+1]) + length($tooltipHTMLSections->[$i+1]) > PAGESIZE_LIMIT)
+            {
+            $page++;
+            $pageSize = 0;
+            };
+        };
+
+
+    # Build the pages.
+
+    my $indexFileName;
+    $page = -1;
+    my $oldPage = -1;
+    my $tooltips;
+    my $firstHeading;
+
+    for (my $i = 0; $i < scalar @$indexHTMLSections; $i++)
+        {
+        if (!defined $indexHTMLSections->[$i])
+            {  next;  };
+
+        $page = $pageLocations[$i];
+
+        # Switch files if we need to.
+
+        if ($page != $oldPage)
+            {
+            if ($oldPage != -1)
+                {
+                print INDEXFILEHANDLE '</table>' . $tooltips . $endIndexPage;
+                close(INDEXFILEHANDLE);
+                $tooltips = undef;
+                };
+
+            $indexFileName = $self->IndexFileOf($type, $page);
+
+            open(INDEXFILEHANDLE, '>' . $indexFileName)
+                or die "Couldn't create output file " . $indexFileName . ".\n";
+
+            print INDEXFILEHANDLE $beginIndexPage . $self->BuildIndexNavigationBar($type, $page, \@pageLocations)
+                                              . '<table border=0 cellspacing=0 cellpadding=0>';
+
+            $oldPage = $page;
+            $firstHeading = 1;
+            };
+
+        print INDEXFILEHANDLE
+        '<tr>'
+            . '<td class=IHeading' . ($firstHeading ? ' id=IFirstHeading' : '') . '>'
+                . '<a name="' . $indexAnchors[$i] . '"></a>'
+                 . $indexHeadings[$i]
+            . '</td>'
+            . '<td></td>'
+        . '</tr>'
+
+        . $indexHTMLSections->[$i];
+
+        $firstHeading = 0;
+        $tooltips .= $tooltipHTMLSections->[$i];
+        };
+
+    if ($page != -1)
+        {
+        print INDEXFILEHANDLE '</table>' . $tooltips . $endIndexPage;
+        close(INDEXFILEHANDLE);
+        }
+
+    # Build a dummy page so there's something at least.
+    else
+        {
+        $indexFileName = $self->IndexFileOf($type, 1);
+
+        open(INDEXFILEHANDLE, '>' . $indexFileName)
+            or die "Couldn't create output file " . $indexFileName . ".\n";
+
+        print INDEXFILEHANDLE
+            $beginIndexPage
+            . $self->BuildIndexNavigationBar($type, 1, \@pageLocations)
+            . 'There are no entries in the ' . lc( NaturalDocs::Topics->NameOfType($type) ) . ' index.'
+            . $endIndexPage;
+
+        close(INDEXFILEHANDLE);
+        };
+
+
+    return $page;
+    };
+
+
+#
+#   Function: BuildIndexSections
+#
+#   Builds and returns the index and search results sections in HTML.
+#
+#   Parameters:
+#
+#       index  - An arrayref of sections, each section being an arrayref <NaturalDocs::SymbolTable::IndexElement> objects.
+#                   The first section is for symbols, the second for numbers, and the rest for A through Z.
+#
+#   Returns:
+#
+#       The arrayref ( indexSections, tooltipSections, searchResultsSections ).
+#
+#       Index 0 is the symbols, index 1 is the numbers, and each following index is A through Z.  The content of each section
+#       is its HTML, or undef if there is nothing for that section.
+#
+sub BuildIndexSections #(NaturalDocs::SymbolTable::IndexElement[] index) => ( string[], string[], string[] )
+    {
+    my ($self, $indexSections) = @_;
+
+    $self->ResetToolTips();
+    %searchResultIDs = ( );
+
+    my $contentSections = [ ];
+    my $tooltipSections = [ ];
+    my $searchResultsSections = [ ];
+
+    for (my $section = 0; $section < scalar @$indexSections; $section++)
+        {
+        if (defined $indexSections->[$section])
+            {
+            my $total = scalar @{$indexSections->[$section]};
+
+            for (my $i = 0; $i < $total; $i++)
+                {
+                my $id;
+
+                if ($i == 0)
+                    {
+                    if ($total == 1)
+                        {  $id = 'IOnlySymbolPrefix';  }
+                    else
+                        {  $id = 'IFirstSymbolPrefix';  };
+                    }
+                elsif ($i == $total - 1)
+                    {  $id = 'ILastSymbolPrefix';  };
+
+                my ($content, $searchResult) = $self->BuildIndexElement($indexSections->[$section]->[$i], $id);
+                $contentSections->[$section] .= $content;
+                $searchResultsSections->[$section] .= $searchResult;
+                };
+
+            $tooltipSections->[$section] .= $self->BuildToolTips();
+            $self->ResetToolTips(1);
+            };
+        };
+
+
+    return ( $contentSections, $tooltipSections, $searchResultsSections );
+    };
+
+
+#
+#   Function: BuildIndexElement
+#
+#   Converts a <NaturalDocs::SymbolTable::IndexElement> to HTML and returns it.  It will handle all sub-elements automatically.
+#
+#   Parameters:
+#
+#       element - The <NaturalDocs::SymbolTable::IndexElement> to build.
+#       cssID - The CSS ID to apply to the prefix.
+#
+#   Recursion-Only Parameters:
+#
+#       These parameters are used internally for recursion, and should not be set.
+#
+#       symbol - If the element is below symbol level, the <SymbolString> to use.
+#       package - If the element is below package level, the package <SymbolString> to use.
+#       hasPackage - Whether the element is below package level.  Is necessary because package may need to be undef.
+#
+#   Returns:
+#
+#       The array ( indexHTML, searchResultHTML ) which is the element in the respective HTML forms.
+#
+sub BuildIndexElement #(NaturalDocs::SymbolTable::IndexElement element, string cssID, SymbolString symbol, SymbolString package, bool hasPackage) => ( string, string )
+    {
+    my ($self, $element, $cssID, $symbol, $package, $hasPackage) = @_;
+
+
+    # If we're doing a file sub-index entry...
+
+    if ($hasPackage)
+        {
+        my ($inputDirectory, $relativePath) = NaturalDocs::Settings->SplitFromInputDirectory($element->File());
+
+        return $self->BuildIndexLink($self->StringToHTML($relativePath, ADD_HIDDEN_BREAKS), $symbol,
+                                                                                 $package, $element->File(), $element->Type(),
+                                                                                 $element->Prototype(), $element->Summary(), 'IFile');
+        }
+
+
+    # If we're doing a package sub-index entry...
+
+    elsif (defined $symbol)
+
+        {
+        my $text;
+
+        if ($element->Package())
+            {
+            $text = NaturalDocs::SymbolString->ToText($element->Package(), $element->PackageSeparator());
+            $text = $self->StringToHTML($text, ADD_HIDDEN_BREAKS);
+            }
+        else
+            {  $text = 'Global';  };
+
+        if (!$element->HasMultipleFiles())
+            {
+            return $self->BuildIndexLink($text, $symbol, $element->Package(), $element->File(), $element->Type(),
+                                                      $element->Prototype(), $element->Summary(), 'IParent');
+            }
+
+        else
+            {
+            my $indexHTML =
+            '<span class=IParent>'
+                . $text
+            . '</span>'
+            . '<div class=ISubIndex>';
+
+            my $searchResultHTML = $indexHTML;
+
+            my $fileElements = $element->File();
+            foreach my $fileElement (@$fileElements)
+                {
+                my ($i, $s) = $self->BuildIndexElement($fileElement, $cssID, $symbol, $element->Package(), 1);
+                $indexHTML .= $i;
+                $searchResultHTML .= $s;
+                };
+
+            $indexHTML .= '</div>';
+            $searchResultHTML .= '</div>';
+
+            return ($indexHTML, $searchResultHTML);
+            };
+        }
+
+
+    # If we're doing a top-level symbol entry...
+
+    else
+        {
+        my $symbolText = $self->StringToHTML($element->SortableSymbol(), ADD_HIDDEN_BREAKS);
+        my $symbolPrefix = $self->StringToHTML($element->IgnoredPrefix());
+        my $searchResultID = $self->StringToSearchResultID($element->SortableSymbol());
+
+        my $indexHTML =
+        '<tr>'
+            . '<td class=ISymbolPrefix' . ($cssID ? ' id=' . $cssID : '') . '>'
+                . ($symbolPrefix || '&nbsp;')
+            . '</td><td class=IEntry>';
+
+        my $searchResultsHTML =
+        '<div class=SRResult id=' . $searchResultID . '><div class=IEntry>';
+
+            if ($symbolPrefix)
+                {  $searchResultsHTML .= '<span class=ISymbolPrefix>' . $symbolPrefix . '</span>';  };
+
+        if (!$element->HasMultiplePackages())
+            {
+            my $packageText;
+
+            if (defined $element->Package())
+                {
+                $packageText = NaturalDocs::SymbolString->ToText($element->Package(), $element->PackageSeparator());
+                $packageText = $self->StringToHTML($packageText, ADD_HIDDEN_BREAKS);
+                };
+
+            if (!$element->HasMultipleFiles())
+                {
+                my ($i, $s) =
+                    $self->BuildIndexLink($symbolText, $element->Symbol(), $element->Package(), $element->File(),
+                                                     $element->Type(), $element->Prototype(), $element->Summary(), 'ISymbol');
+                $indexHTML .= $i;
+                $searchResultsHTML .= $s;
+
+                if (defined $packageText)
+                    {
+                    $indexHTML .=
+                    ', <span class=IParent>'
+                        . $packageText
+                    . '</span>';
+
+                    $searchResultsHTML .=
+                    ', <span class=IParent>'
+                        . $packageText
+                    . '</span>';
+                    };
+                }
+            else # hasMultipleFiles but not multiplePackages
+                {
+                $indexHTML .=
+                '<span class=ISymbol>'
+                    . $symbolText
+                . '</span>';
+
+                $searchResultsHTML .=
+                q{<a href="javascript:searchResults.Toggle('} . $searchResultID . q{')" class=ISymbol>}
+                    . $symbolText
+                . '</a>';
+
+                my $output;
+
+                if (defined $packageText)
+                    {
+                    $output .=
+                    ', <span class=IParent>'
+                        . $packageText
+                    . '</span>';
+                    };
+
+                $output .=
+                '<div class=ISubIndex>';
+
+                $indexHTML .= $output;
+                $searchResultsHTML .= $output;
+
+                my $fileElements = $element->File();
+                foreach my $fileElement (@$fileElements)
+                    {
+                    my ($i, $s) = $self->BuildIndexElement($fileElement, $cssID, $element->Symbol(), $element->Package(), 1);
+                    $indexHTML .= $i;
+                    $searchResultsHTML .= $s;
+                    };
+
+                $indexHTML .= '</div>';
+                $searchResultsHTML .= '</div>';
+                };
+            }
+
+        else # hasMultiplePackages
+            {
+            $indexHTML .=
+            '<span class=ISymbol>'
+                . $symbolText
+            . '</span>'
+            . '<div class=ISubIndex>';
+
+            $searchResultsHTML .=
+            q{<a href="javascript:searchResults.Toggle('} . $searchResultID . q{')" class=ISymbol>}
+                . $symbolText
+            . '</a>'
+            . '<div class=ISubIndex>';
+
+            my $packageElements = $element->Package();
+            foreach my $packageElement (@$packageElements)
+                {
+                my ($i, $s) = $self->BuildIndexElement($packageElement, $cssID, $element->Symbol());
+                $indexHTML .= $i;
+                $searchResultsHTML .= $s;
+                };
+
+            $indexHTML .= '</div>';
+            $searchResultsHTML .= '</div>';
+            };
+
+        $indexHTML .= '</td></tr>';
+        $searchResultsHTML .= '</div></div>';
+
+        return ($indexHTML, $searchResultsHTML);
+        };
+    };
+
+
+#
+#   Function: BuildIndexLink
+#
+#   Builds and returns the HTML associated with an index link.  The HTML will be the a href tag, the text, and the closing tag.
+#
+#   Parameters:
+#
+#       text - The text of the link *in HTML*.  Use <IndexSymbolToHTML()> if necessary.
+#       symbol - The partial <SymbolString> to link to.
+#       package - The package <SymbolString> of the symbol.
+#       file - The <FileName> the symbol is defined in.
+#       type - The <TopicType> of the symbol.
+#       prototype - The prototype of the symbol, or undef if none.
+#       summary - The summary of the symbol, or undef if none.
+#       style - The CSS style to apply to the link.
+#
+#   Returns:
+#
+#       The array ( indexHTML, searchResultHTML ) which is the link in the respective forms.
+#
+sub BuildIndexLink #(string text, SymbolString symbol, SymbolString package, FileName file, TopicType type, string prototype, string summary, string style) => ( string, string )
+    {
+    my ($self, $text, $symbol, $package, $file, $type, $prototype, $summary, $style) = @_;
+
+    $symbol = NaturalDocs::SymbolString->Join($package, $symbol);
+
+    my $targetTooltipID = $self->BuildToolTip($symbol, $file, $type, $prototype, $summary);
+    my $toolTipProperties = $self->BuildToolTipLinkProperties($targetTooltipID);
+
+    my $indexHTML = '<a href="' . $self->MakeRelativeURL( $self->IndexDirectory(), $self->OutputFileOf($file) )
+                                         . '#' . $self->SymbolToHTMLSymbol($symbol) . '" ' . $toolTipProperties . ' '
+                                . 'class=' . $style . '>' . $text . '</a>';
+    my $searchResultHTML = '<a href="' . $self->MakeRelativeURL( $self->SearchResultsDirectory(), $self->OutputFileOf($file) )
+                                         . '#' . $self->SymbolToHTMLSymbol($symbol) . '" '
+                                         . ($self->CommandLineOption eq 'HTML' ? 'target=_parent ' : '')
+                                . 'class=' . $style . '>' . $text . '</a>';
+
+    return ($indexHTML, $searchResultHTML);
+    };
+
+
+#
+#   Function: BuildIndexNavigationBar
+#
+#   Builds a navigation bar for a page of the index.
+#
+#   Parameters:
+#
+#       type - The <TopicType> of the index, or undef for general.
+#       page - The page of the index the navigation bar is for.
+#       locations - An arrayref of the locations of each section.  Index 0 is for the symbols, index 1 for the numbers, and the rest
+#                       for each letter.  The values are the page numbers where the sections are located.
+#
+sub BuildIndexNavigationBar #(type, page, locations)
+    {
+    my ($self, $type, $page, $locations) = @_;
+
+    my $output = '<div class=INavigationBar>';
+
+    for (my $i = 0; $i < scalar @indexHeadings; $i++)
+        {
+        if ($i != 0)
+            {  $output .= ' &middot; ';  };
+
+        if (defined $locations->[$i])
+            {
+            $output .= '<a href="';
+
+            if ($locations->[$i] != $page)
+                {  $output .= $self->RelativeIndexFileOf($type, $locations->[$i]);  };
+
+            $output .= '#' . $indexAnchors[$i] . '">' . $indexHeadings[$i] . '</a>';
+            }
+        else
+            {
+            $output .= $indexHeadings[$i];
+            };
+        };
+
+    $output .= '</div>';
+
+    return $output;
+    };
+
+
+
+###############################################################################
+# Group: File Functions
+
+
+#
+#   Function: PurgeIndexFiles
+#
+#   Removes all or some of the output files for an index.
+#
+#   Parameters:
+#
+#       type  - The index <TopicType>.
+#       indexSections  - An arrayref of sections, each section being an arrayref <NaturalDocs::SymbolTable::IndexElement>
+#                               objects.  The first section is for symbols, the second for numbers, and the rest for A through Z.  May be
+#                               undef.
+#       startingPage - If defined, only pages starting with this number will be removed.  Otherwise all pages will be removed.
+#
+sub PurgeIndexFiles #(TopicType type, optional NaturalDocs::SymbolTable::IndexElement[] indexSections, optional int startingPage)
+    {
+    my ($self, $type, $indexSections, $page) = @_;
+
+    # First the regular index pages.
+
+    if (!defined $page)
+        {  $page = 1;  };
+
+    for (;;)
+        {
+        my $file = $self->IndexFileOf($type, $page);
+
+        if (-e $file)
+            {
+            unlink($file);
+            $page++;
+            }
+        else
+            {
+            last;
+            };
+        };
+
+
+    # Next the search results.
+
+    for (my $i = 0; $i < 28; $i++)
+        {
+        if (!$indexSections || !$indexSections->[$i])
+            {
+            my $file = $self->SearchResultsFileOf($type, $searchExtensions[$i]);
+
+            if (-e $file)
+                {  unlink($file);  };
+            };
+        };
+    };
+
+
+#
+#   Function: OutputFileOf
+#
+#   Returns the output file name of the source file.  Will be undef if it is not a file from a valid input directory.
+#
+sub OutputFileOf #(sourceFile)
+    {
+    my ($self, $sourceFile) = @_;
+
+    my ($inputDirectory, $relativeSourceFile) = NaturalDocs::Settings->SplitFromInputDirectory($sourceFile);
+    if (!defined $inputDirectory)
+        {  return undef;  };
+
+    my $outputDirectory = NaturalDocs::Settings->OutputDirectoryOf($self);
+    my $inputDirectoryName = NaturalDocs::Settings->InputDirectoryNameOf($inputDirectory);
+
+    $outputDirectory = NaturalDocs::File->JoinPaths( $outputDirectory,
+                                                                            'files' . ($inputDirectoryName != 1 ? $inputDirectoryName : ''), 1 );
+
+    # We need to change any extensions to dashes because Apache will think file.pl.html is a script.
+    # We also need to add a dash if the file doesn't have an extension so there'd be no conflicts with index.html,
+    # FunctionIndex.html, etc.
+
+    if (!($relativeSourceFile =~ tr/./-/))
+        {  $relativeSourceFile .= '-';  };
+
+    $relativeSourceFile =~ tr/ &?(){};/_/;
+    $relativeSourceFile .= '.html';
+
+    return NaturalDocs::File->JoinPaths($outputDirectory, $relativeSourceFile);
+    };
+
+
+#
+#   Function: OutputImageOf
+#
+#   Returns the output image file name of the source image file.  Will be undef if it is not a file from a valid input directory.
+#
+sub OutputImageOf #(sourceImageFile)
+    {
+    my ($self, $sourceImageFile) = @_;
+
+    my $outputDirectory = NaturalDocs::Settings->OutputDirectoryOf($self);
+    my $topLevelDirectory;
+
+    my ($inputDirectory, $relativeImageFile) = NaturalDocs::Settings->SplitFromInputDirectory($sourceImageFile);
+
+    if (defined $inputDirectory)
+        {
+        my $inputDirectoryName = NaturalDocs::Settings->InputDirectoryNameOf($inputDirectory);
+        $topLevelDirectory = 'files' . ($inputDirectoryName != 1 ? $inputDirectoryName : '');
+        }
+    else
+        {
+        ($inputDirectory, $relativeImageFile) = NaturalDocs::Settings->SplitFromImageDirectory($sourceImageFile);
+
+        if (!defined $inputDirectory)
+            {  return undef;  };
+
+        my $inputDirectoryName = NaturalDocs::Settings->ImageDirectoryNameOf($inputDirectory);
+        $topLevelDirectory = 'images' . ($inputDirectoryName != 1 ? $inputDirectoryName : '');
+        }
+
+
+    $outputDirectory = NaturalDocs::File->JoinPaths($outputDirectory, $topLevelDirectory, 1);
+
+    $relativeImageFile =~ tr/ /_/;
+
+    return NaturalDocs::File->JoinPaths($outputDirectory, $relativeImageFile);
+    };
+
+
+#
+#   Function: IndexDirectory
+#
+#   Returns the directory of the index files.
+#
+sub IndexDirectory
+    {
+    my $self = shift;
+    return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->OutputDirectoryOf($self), 'index', 1);
+    };
+
+
+#
+#   Function: IndexFileOf
+#
+#   Returns the output file name of the index file.
+#
+#   Parameters:
+#
+#       type  - The <TopicType> of the index.
+#       page  - The page number.  Undef is the same as one.
+#
+sub IndexFileOf #(type, page)
+    {
+    my ($self, $type, $page) = @_;
+    return NaturalDocs::File->JoinPaths( $self->IndexDirectory(), $self->RelativeIndexFileOf($type, $page) );
+    };
+
+
+#
+#   Function: RelativeIndexFileOf
+#
+#   Returns the output file name of the index file, relative to other index files.
+#
+#   Parameters:
+#
+#       type  - The <TopicType> of the index.
+#       page  - The page number.  Undef is the same as one.
+#
+sub RelativeIndexFileOf #(type, page)
+    {
+    my ($self, $type, $page) = @_;
+    return NaturalDocs::Topics->NameOfType($type, 1, 1) . (defined $page && $page != 1 ? $page : '') . '.html';
+    };
+
+
+#
+#   Function: SearchResultsDirectory
+#
+#   Returns the directory of the search results files.
+#
+sub SearchResultsDirectory
+    {
+    my $self = shift;
+    return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->OutputDirectoryOf($self), 'search', 1);
+    };
+
+
+#
+#   Function: SearchResultsFileOf
+#
+#   Returns the output file name of the search result file.
+#
+#   Parameters:
+#
+#       type  - The <TopicType> of the index.
+#       extra - The string to add to the end of the file name, such as "A" or "Symbols".
+#
+sub SearchResultsFileOf #(TopicType type, string extra)
+    {
+    my ($self, $type, $extra) = @_;
+
+    my $fileName = NaturalDocs::Topics->NameOfType($type, 1, 1) . $extra . '.html';
+
+    return NaturalDocs::File->JoinPaths( $self->SearchResultsDirectory(), $fileName );
+    };
+
+
+#
+#   Function: CSSDirectory
+#
+#   Returns the directory of the CSS files.
+#
+sub CSSDirectory
+    {
+    my $self = shift;
+    return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->OutputDirectoryOf($self), 'styles', 1);
+    };
+
+
+#
+#   Function: MainCSSFile
+#
+#   Returns the location of the main CSS file.
+#
+sub MainCSSFile
+    {
+    my $self = shift;
+    return NaturalDocs::File->JoinPaths( $self->CSSDirectory(), 'main.css' );
+    };
+
+
+#
+#   Function: JavaScriptDirectory
+#
+#   Returns the directory of the JavaScript files.
+#
+sub JavaScriptDirectory
+    {
+    my $self = shift;
+    return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->OutputDirectoryOf($self), 'javascript', 1);
+    };
+
+
+#
+#   Function: MainJavaScriptFile
+#
+#   Returns the location of the main JavaScript file.
+#
+sub MainJavaScriptFile
+    {
+    my $self = shift;
+    return NaturalDocs::File->JoinPaths( $self->JavaScriptDirectory(), 'main.js' );
+    };
+
+
+#
+#   Function: SearchDataJavaScriptFile
+#
+#   Returns the location of the search data JavaScript file.
+#
+sub SearchDataJavaScriptFile
+    {
+    my $self = shift;
+    return NaturalDocs::File->JoinPaths( $self->JavaScriptDirectory(), 'searchdata.js' );
+    };
+
+
+
+###############################################################################
+# Group: Support Functions
+
+
+#
+#   Function: IndexTitleOf
+#
+#   Returns the page title of the index file.
+#
+#   Parameters:
+#
+#       type  - The type of index.
+#
+sub IndexTitleOf #(type)
+    {
+    my ($self, $type) = @_;
+
+    return ($type eq ::TOPIC_GENERAL() ? '' : NaturalDocs::Topics->NameOfType($type) . ' ') . 'Index';
+    };
+
+#
+#   Function: MakeRelativeURL
+#
+#   Returns a relative path between two files in the output tree and returns it in URL format.
+#
+#   Parameters:
+#
+#       baseFile    - The base <FileName> in local format, *not* in URL format.
+#       targetFile  - The target <FileName> of the link in local format, *not* in URL format.
+#       baseHasFileName - Whether baseFile has a file name attached or is just a path.
+#
+#   Returns:
+#
+#       The relative URL to the target.
+#
+sub MakeRelativeURL #(FileName baseFile, FileName targetFile, bool baseHasFileName) -> string relativeURL
+    {
+    my ($self, $baseFile, $targetFile, $baseHasFileName) = @_;
+
+    if ($baseHasFileName)
+        {  $baseFile = NaturalDocs::File->NoFileName($baseFile)  };
+
+    my $relativePath = NaturalDocs::File->MakeRelativePath($baseFile, $targetFile);
+
+    return $self->ConvertAmpChars( NaturalDocs::File->ConvertToURL($relativePath) );
+    };
+
+#
+#   Function: StringToHTML
+#
+#   Converts a text string to HTML.  Does not apply paragraph tags or accept formatting tags.
+#
+#   Parameters:
+#
+#       string - The string to convert.
+#       addHiddenBreaks - Whether to add hidden breaks to the string.  You can use <ADD_HIDDEN_BREAKS> for this parameter
+#                                   if you want to make the calling code clearer.
+#
+#   Returns:
+#
+#       The string in HTML.
+#
+sub StringToHTML #(string, addHiddenBreaks)
+    {
+    my ($self, $string, $addHiddenBreaks) = @_;
+
+    $string =~ s/&/&amp;/g;
+    $string =~ s/</&lt;/g;
+    $string =~ s/>/&gt;/g;
+
+    # Me likey the fancy quotes.  They work in IE 4+, Mozilla, and Opera 5+.  We've already abandoned NS4 with the CSS
+    # styles, so might as well.
+    $string =~ s/^\'/&lsquo;/gm;
+    $string =~ s/([\ \(\[\{])\'/$1&lsquo;/g;
+    $string =~ s/\'/&rsquo;/g;
+
+    $string =~ s/^\"/&ldquo;/gm;
+    $string =~ s/([\ \(\[\{])\"/$1&ldquo;/g;
+    $string =~ s/\"/&rdquo;/g;
+
+    # Me likey the double spaces too.  As you can probably tell, I like print-formatting better than web-formatting.  The indented
+    # paragraphs without blank lines in between them do become readable when you have fancy quotes and double spaces too.
+    $string = $self->AddDoubleSpaces($string);
+
+    if ($addHiddenBreaks)
+        {  $string = $self->AddHiddenBreaks($string);  };
+
+    return $string;
+    };
+
+
+#
+#   Function: SymbolToHTMLSymbol
+#
+#   Converts a <SymbolString> to a HTML symbol, meaning one that is safe to include in anchor and link tags.  You don't need
+#   to pass the result to <ConvertAmpChars()>.
+#
+sub SymbolToHTMLSymbol #(symbol)
+    {
+    my ($self, $symbol) = @_;
+
+    my @identifiers = NaturalDocs::SymbolString->IdentifiersOf($symbol);
+    my $htmlSymbol = join('.', @identifiers);
+
+    # If only Mozilla was nice about putting special characters in URLs like IE and Opera are, I could leave spaces in and replace
+    # "<>& with their amp chars.  But alas, Mozilla shows them as %20, etc. instead.  It would have made for nice looking URLs.
+    $htmlSymbol =~ tr/ \"<>\?&%/_/d;
+
+    return $htmlSymbol;
+    };
+
+
+#
+#   Function: StringToSearchResultID
+#
+#   Takes a text string and translates it into something that can be used as a CSS ID.
+#
+#   Parameters:
+#
+#       string - The string to convert
+#       dontIncrement - If set, it reuses the last generated ID.  Otherwise it generates a new one if it matches a previously
+#                               generated one in a case-insensitive way.
+#
+sub StringToSearchResultID #(string string, bool dontIncrement = 0) => string
+    {
+    my ($self, $string, $dontIncrement) = @_;
+
+    $string =~ s/\_/_und/g;
+    $string =~ s/ +/_spc/g;
+
+    my %translation = ( '~' => '_til', '!' => '_exc', '@' => '_att', '#' => '_num', '$' => '_dol', '%' => '_pct', '^' => '_car',
+                                  '&' => '_amp', '*' => '_ast', '(' => '_lpa', ')' => '_rpa', '-' => '_min', '+' => '_plu', '=' => '_equ',
+                                  '{' => '_lbc', '}' => '_rbc', '[' => '_lbk', ']' => '_rbk', ':' => '_col', ';' => '_sco', '"' => '_quo',
+                                  '\'' => '_apo', '<' => '_lan', '>' => '_ran', ',' => '_com', '.' => '_per', '?' => '_que', '/' => '_sla' );
+
+    $string =~ s/([\~\!\@\#\$\%\^\&\*\(\)\-\+\=\{\}\[\]\:\;\"\'\<\>\,\.\?\/])/$translation{$1}/ge;
+    $string =~ s/[^a-z0-9_]/_zzz/gi;
+
+    my $number = $searchResultIDs{lc($string)};
+
+    if (!$number)
+        {  $number = 1;  }
+    elsif (!$dontIncrement)
+        {  $number++;  };
+
+    $searchResultIDs{lc($string)} = $number;
+
+    return 'SR' . ($number == 1 ? '' : $number) . '_' . $string;
+    };
+
+
+#
+#   Function: NDMarkupToHTML
+#
+#   Converts a block of <NDMarkup> to HTML.
+#
+#   Parameters:
+#
+#       sourceFile - The source <FileName> the <NDMarkup> appears in.
+#       text    - The <NDMarkup> text to convert.
+#       symbol - The topic <SymbolString> the <NDMarkup> appears in.
+#       package  - The package <SymbolString> the <NDMarkup> appears in.
+#       type - The <TopicType> the <NDMarkup> appears in.
+#       using - An arrayref of scope <SymbolStrings> the <NDMarkup> also has access to, or undef if none.
+#       style - Set to one of the <NDMarkupToHTML Styles> or leave undef for general.
+#
+#   Returns:
+#
+#       The text in HTML.
+#
+sub NDMarkupToHTML #(sourceFile, text, symbol, package, type, using, style)
+    {
+    my ($self, $sourceFile, $text, $symbol, $package, $type, $using, $style) = @_;
+
+    my $dlSymbolBehavior;
+
+    if ($type == ::TOPIC_ENUMERATION())
+        {  $dlSymbolBehavior = NaturalDocs::Languages->LanguageOf($sourceFile)->EnumValues();  }
+    elsif (NaturalDocs::Topics->TypeInfo($type)->Scope() == ::SCOPE_ALWAYS_GLOBAL())
+        {  $dlSymbolBehavior = ::ENUM_GLOBAL();  }
+    else
+        {  $dlSymbolBehavior = ::ENUM_UNDER_PARENT();  };
+
+    my $output;
+    my $inCode;
+
+    my @splitText = split(/(<\/?code>)/, $text);
+
+    while (scalar @splitText)
+        {
+        $text = shift @splitText;
+
+        if ($text eq '<code>')
+            {
+            $output .= '<blockquote><pre>';
+            $inCode = 1;
+            }
+        elsif ($text eq '</code>')
+            {
+            $output .= '</pre></blockquote>';
+            $inCode = undef;
+            }
+        elsif ($inCode)
+            {
+            # Leave line breaks in.
+            $output .= $text;
+            }
+        else
+            {
+            # Format non-code text.
+
+            # Convert linked images.
+            if ($text =~ /<img mode=\"link\"/)
+                {
+                if ($style == NDMARKUPTOHTML_GENERAL)
+                    {
+                    # Split by tags we would want to see the linked images appear after.  For example, an image link appearing in
+                    # the middle of a paragraph would appear after the end of that paragraph.
+                    my @imageBlocks = split(/(<p>.*?<\/p>|<dl>.*?<\/dl>|<ul>.*?<\/ul>)/, $text);
+                    $text = undef;
+
+                    foreach my $imageBlock (@imageBlocks)
+                        {
+                        $imageBlock =~ s{<img mode=\"link\" target=\"([^\"]*)\" original=\"([^\"]*)\">}
+                                                {$self->BuildImage($sourceFile, 'link', $1, $2)}ge;
+
+                        $text .= $imageBlock . $imageContent;
+                        $imageContent = undef;
+                        };
+                    }
+
+                # Use only the text for tooltips and summaries.
+                else
+                    {
+                    $text =~ s{<img mode=\"link\" target=\"[^\"]*\" original=\"([^\"]*)\">}{$1}g;
+                    };
+                };
+
+            # Convert quotes to fancy quotes.  This has to be done before links because some of them may have JavaScript
+            # attributes that use the apostrophe character.
+            $text =~ s/^\'/&lsquo;/gm;
+            $text =~ s/([\ \(\[\{])\'/$1&lsquo;/g;
+            $text =~ s/\'/&rsquo;/g;
+
+            $text =~ s/^&quot;/&ldquo;/gm;
+            $text =~ s/([\ \(\[\{])&quot;/$1&ldquo;/g;
+            $text =~ s/&quot;/&rdquo;/g;
+
+            # Resolve and convert links, except for tooltips.
+            if ($style != NDMARKUPTOHTML_TOOLTIP)
+                {
+                $text =~ s{<link target=\"([^\"]*)\" name=\"([^\"]*)\" original=\"([^\"]*)\">}
+                               {$self->BuildTextLink($1, $2, $3, $package, $using, $sourceFile)}ge;
+                $text =~ s/<url target=\"([^\"]*)\" name=\"([^\"]*)\">/$self->BuildURLLink($1, $2)/ge;
+                }
+            else
+                {
+                $text =~ s{<link target=\"[^\"]*\" name=\"([^\"]*)\" original=\"[^\"]*\">}{$1}g;
+                $text =~ s{<url target=\"[^\"]*\" name=\"([^\"]*)\">}{$1}g;
+                };
+
+            # We do full e-mail links anyway just so the obfuscation remains.
+            $text =~ s/<email target=\"([^\"]*)\" name=\"([^\"]*)\">/$self->BuildEMailLink($1, $2)/ge;
+
+
+            # Convert inline images, but only for the general style.
+            if ($style == NDMARKUPTOHTML_GENERAL)
+                {
+                $text =~ s{<img mode=\"inline\" target=\"([^\"]*)\" original=\"([^\"]*)\">}
+                               {$self->BuildImage($sourceFile, 'inline', $1, $2)}ge;
+                }
+            else
+                {
+                $text =~ s{<img mode=\"inline\" target=\"[^\"]*\" original=\"([^\"]*)\">}{$1}g;
+                };
+
+            # Copyright symbols.  Prevent conversion when part of (a), (b), (c) lists.
+            if ($text !~ /\(a\)/i)
+                {  $text =~ s/\(c\)/&copy;/gi;  };
+
+            # Trademark symbols.
+            $text =~ s/\(tm\)/&trade;/gi;
+            $text =~ s/\(r\)/&reg;/gi;
+
+            # Add double spaces too.
+            $text = $self->AddDoubleSpaces($text);
+
+            # Headings
+            $text =~ s/<h>/<h4 class=CHeading>/g;
+            $text =~ s/<\/h>/<\/h4>/g;
+
+            # Description Lists
+            $text =~ s/<dl>/<table border=0 cellspacing=0 cellpadding=0 class=CDescriptionList>/g;
+            $text =~ s/<\/dl>/<\/table>/g;
+
+            $text =~ s/<de>/<tr><td class=CDLEntry>/g;
+            $text =~ s/<\/de>/<\/td>/g;
+
+            if ($dlSymbolBehavior == ::ENUM_GLOBAL())
+                {  $text =~ s/<ds>([^<]+)<\/ds>/$self->MakeDescriptionListSymbol(undef, $1)/ge;  }
+            elsif ($dlSymbolBehavior == ::ENUM_UNDER_PARENT())
+                {  $text =~ s/<ds>([^<]+)<\/ds>/$self->MakeDescriptionListSymbol($package, $1)/ge;  }
+            else # ($dlSymbolBehavior == ::ENUM_UNDER_TYPE())
+                {  $text =~ s/<ds>([^<]+)<\/ds>/$self->MakeDescriptionListSymbol($symbol, $1)/ge;  }
+
+            sub MakeDescriptionListSymbol #(package, text)
+                {
+                my ($self, $package, $text) = @_;
+
+                $text = NaturalDocs::NDMarkup->RestoreAmpChars($text);
+                my $symbol = NaturalDocs::SymbolString->FromText($text);
+
+                if (defined $package)
+                    {  $symbol = NaturalDocs::SymbolString->Join($package, $symbol);  };
+
+                return
+                '<tr>'
+                    . '<td class=CDLEntry>'
+                        # The anchors are closed, but not around the text, to prevent the :hover CSS style from kicking in.
+                        . '<a name="' . $self->SymbolToHTMLSymbol($symbol) . '"></a>'
+                        . $text
+                    . '</td>';
+                };
+
+            $text =~ s/<dd>/<td class=CDLDescription>/g;
+            $text =~ s/<\/dd>/<\/td><\/tr>/g;
+
+            $output .= $text;
+            };
+        };
+
+    return $output;
+    };
+
+
+#
+#   Function: BuildTextLink
+#
+#   Creates a HTML link to a symbol, if it exists.
+#
+#   Parameters:
+#
+#       target  - The link text.
+#       name - The link name.
+#       original - The original text as it appears in the source.
+#       package  - The package <SymbolString> the link appears in, or undef if none.
+#       using - An arrayref of additional scope <SymbolStrings> the link has access to, or undef if none.
+#       sourceFile  - The <FileName> the link appears in.
+#
+#       Target, name, and original are assumed to still have <NDMarkup> amp chars.
+#
+#   Returns:
+#
+#       The link in HTML, including tags.  If the link doesn't resolve to anything, returns the HTML that should be substituted for it.
+#
+sub BuildTextLink #(target, name, original, package, using, sourceFile)
+    {
+    my ($self, $target, $name, $original, $package, $using, $sourceFile) = @_;
+
+    my $plainTarget = $self->RestoreAmpChars($target);
+
+    my $symbol = NaturalDocs::SymbolString->FromText($plainTarget);
+    my $symbolTarget = NaturalDocs::SymbolTable->References(::REFERENCE_TEXT(), $symbol, $package, $using, $sourceFile);
+
+    if (defined $symbolTarget)
+        {
+        my $symbolTargetFile;
+
+        if ($symbolTarget->File() ne $sourceFile)
+            {
+            $symbolTargetFile = $self->MakeRelativeURL( $self->OutputFileOf($sourceFile),
+                                                                               $self->OutputFileOf($symbolTarget->File()), 1 );
+            };
+        # else leave it undef
+
+        my $symbolTargetTooltipID = $self->BuildToolTip($symbolTarget->Symbol(), $sourceFile, $symbolTarget->Type(),
+                                                                                 $symbolTarget->Prototype(), $symbolTarget->Summary());
+
+        my $toolTipProperties = $self->BuildToolTipLinkProperties($symbolTargetTooltipID);
+
+        return '<a href="' . $symbolTargetFile . '#' . $self->SymbolToHTMLSymbol($symbolTarget->Symbol()) . '" '
+                    . 'class=L' . NaturalDocs::Topics->NameOfType($symbolTarget->Type(), 0, 1) . ' ' . $toolTipProperties . '>'
+                        . $name
+                    . '</a>';
+        }
+    else
+        {
+        return $original;
+        };
+    };
+
+
+#
+#   Function: BuildURLLink
+#
+#   Creates a HTML link to an external URL.  Long URLs will have hidden breaks to allow them to wrap.
+#
+#   Parameters:
+#
+#       target - The URL to link to.
+#       name - The label of the link.
+#
+#       Both are assumed to still have <NDMarkup> amp chars.
+#
+#   Returns:
+#
+#       The HTML link, complete with tags.
+#
+sub BuildURLLink #(target, name)
+    {
+    my ($self, $target, $name) = @_;
+
+    # Don't restore amp chars on the target.
+
+    if (length $name < 50 || $name ne $target)
+        {  return '<a href="' . $target . '" class=LURL target=_top>' . $name . '</a>';  };
+
+    my @segments = split(/([\,\/]|&amp;)/, $target);
+    my $output = '<a href="' . $target . '" class=LURL target=_top>';
+
+    # Get past the first batch of slashes, since we don't want to break on things like http://.
+
+    $output .= $segments[0];
+
+    my $i = 1;
+    while ($i < scalar @segments && ($segments[$i] eq '/' || !$segments[$i]))
+        {
+        $output .= $segments[$i];
+        $i++;
+        };
+
+    # Now break on each one of those symbols.
+
+    while ($i < scalar @segments)
+        {
+        if ($segments[$i] eq ',' || $segments[$i] eq '/' || $segments[$i] eq '&amp;')
+            {  $output .= '<wbr>';  };
+
+        $output .= $segments[$i];
+        $i++;
+        };
+
+    $output .= '</a>';
+    return $output;
+    };
+
+
+#
+#   Function: BuildEMailLink
+#
+#   Creates a HTML link to an e-mail address.  The address will be transparently munged to protect it (hopefully) from spambots.
+#
+#   Parameters:
+#
+#       target  - The e-mail address.
+#       name - The label of the link.
+#
+#       Both are assumed to still have <NDMarkup> amp chars.
+#
+#   Returns:
+#
+#       The HTML e-mail link, complete with tags.
+#
+sub BuildEMailLink #(target, name)
+    {
+    my ($self, $target, $name) = @_;
+    my @splitAddress;
+
+
+    # Hack the address up.  We want two user pieces and two host pieces.
+
+    my ($user, $host) = split(/\@/, $self->RestoreAmpChars($target));
+
+    my $userSplit = length($user) / 2;
+
+    push @splitAddress, NaturalDocs::NDMarkup->ConvertAmpChars( substr($user, 0, $userSplit) );
+    push @splitAddress, NaturalDocs::NDMarkup->ConvertAmpChars( substr($user, $userSplit) );
+
+    push @splitAddress, '@';
+
+    my $hostSplit = length($host) / 2;
+
+    push @splitAddress, NaturalDocs::NDMarkup->ConvertAmpChars( substr($host, 0, $hostSplit) );
+    push @splitAddress, NaturalDocs::NDMarkup->ConvertAmpChars( substr($host, $hostSplit) );
+
+
+    # Now put it back together again.  We'll use spans to split the text transparently and JavaScript to split and join the link.
+
+    my $output =
+    "<a href=\"#\" onClick=\"location.href='mai' + 'lto:' + '" . join("' + '", @splitAddress) . "'; return false;\" class=LEMail>";
+
+    if ($name eq $target)
+        {
+        $output .=
+        $splitAddress[0] . '<span style="display: none">.nosp@m.</span>' . $splitAddress[1]
+        . '<span>@</span>'
+        . $splitAddress[3] . '<span style="display: none">.nosp@m.</span>' . $splitAddress[4];
+        }
+    else
+        {  $output .= $name;  };
+
+    $output .= '</a>';
+    return $output;
+    };
+
+
+#
+#   Function: BuildImage
+#
+#   Builds the HTML for an image.
+#
+#   Parameters:
+#
+#       sourceFile - The source <FileName> this image appears in.
+#       mode - Either "inline" or "link".
+#       target - The target.
+#       original - The original text.
+#
+#       All are assumed to still have <NDMarkup> amp chars.
+#
+#   Returns:
+#
+#       The result in HTML.  If the mode was "link", the target image's HTML is added to <imageContent>.
+#
+sub BuildImage #(sourceFile, mode, target, original)
+    {
+    my ($self, $sourceFile, $mode, $target, $original) = @_;
+
+    my $targetNoAmp = $self->RestoreAmpChars($target);
+
+    my $image = NaturalDocs::ImageReferenceTable->GetReferenceTarget($sourceFile, $targetNoAmp);
+
+    if ($image)
+        {
+        my ($width, $height) = NaturalDocs::Project->ImageFileDimensions($image);
+
+        if ($mode eq 'inline')
+            {
+            return
+            '<img src="' . $self->MakeRelativeURL($self->OutputFileOf($sourceFile),
+                                                                       $self->OutputImageOf($image), 1) . '"'
+
+            . ($width && $height ? ' width="' . $width . '" height="' . $height . '"' : '')
+            . '>';
+            }
+        else # link
+            {
+            # Make the text a little more friendly in the output by removing any folders and file extensions.
+            # (see images/Table1.gif) will be turned into (see Table1).
+            my $originalNoAmp = $self->RestoreAmpChars($original);
+            my $targetIndex = index($originalNoAmp, $targetNoAmp);
+            my ($shortTarget, $shortTargetNoAmp, $shortOriginal);
+
+            if ($targetIndex != -1)
+                {
+                $shortTargetNoAmp = (NaturalDocs::File->SplitPath($targetNoAmp))[2];
+                $shortTargetNoAmp = NaturalDocs::File->NoExtension($shortTargetNoAmp);
+
+                substr($originalNoAmp, $targetIndex, length($targetNoAmp), $shortTargetNoAmp);
+
+                $shortOriginal = NaturalDocs::NDMarkup->ConvertAmpChars($originalNoAmp);
+                $shortTarget = NaturalDocs::NDMarkup->ConvertAmpChars($shortTargetNoAmp);
+                };
+
+            my $output =
+            '<a href="#Image' . $imageAnchorNumber . '" class=CImageLink>'
+                . ($shortOriginal || $original)
+            . '</a>';
+
+            $imageContent .=
+            '<blockquote>'
+            . '<div class=CImage>'
+                . '<a name="Image' . $imageAnchorNumber . '"></a>'
+                . '<div class=CImageCaption>' . ($shortTarget || $target) . '</div>'
+                . '<img src="' . $self->MakeRelativeURL($self->OutputFileOf($sourceFile),
+                                                                           $self->OutputImageOf($image), 1) . '"'
+
+                . ($width && $height ? ' width="' . $width . '" height="' . $height . '"' : '')
+                . '>'
+
+            . '</div></blockquote>';
+
+            $imageAnchorNumber++;
+            return $output;
+            };
+        }
+    else # !$image
+        {
+        if ($mode eq 'inline')
+            {  return '<p>' . $original . '</p>';  }
+        else #($mode eq 'link')
+            {  return $original;  };
+        };
+    };
+
+
+#
+#   Function: BuildToolTipLinkProperties
+#
+#   Returns the properties that should go in the link tag to add a tooltip to it.  Because the function accepts undef, you can
+#   call it without checking if <BuildToolTip()> returned undef or not.
+#
+#   Parameters:
+#
+#       toolTipID - The ID of the tooltip.  If undef, the function will return undef.
+#
+#   Returns:
+#
+#       The properties that should be put in the link tag, or undef if toolTipID wasn't specified.
+#
+sub BuildToolTipLinkProperties #(toolTipID)
+    {
+    my ($self, $toolTipID) = @_;
+
+    if (defined $toolTipID)
+        {
+        my $currentNumber = $tooltipLinkNumber;
+        $tooltipLinkNumber++;
+
+        return 'id=link' . $currentNumber . ' '
+                . 'onMouseOver="ShowTip(event, \'' . $toolTipID . '\', \'link' . $currentNumber . '\')" '
+                . 'onMouseOut="HideTip(\'' . $toolTipID . '\')"';
+        }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: AddDoubleSpaces
+#
+#   Adds second spaces after the appropriate punctuation with &nbsp; so they show up in HTML.  They don't occur if there isn't at
+#   least one space after the punctuation, so things like class.member notation won't be affected.
+#
+#   Parameters:
+#
+#       text - The text to convert.
+#
+#   Returns:
+#
+#       The text with double spaces as necessary.
+#
+sub AddDoubleSpaces #(text)
+    {
+    my ($self, $text) = @_;
+
+    # Question marks and exclamation points get double spaces unless followed by a lowercase letter.
+
+    $text =~ s/  ([^\ \t\r\n] [\!\?])  # Must appear after a non-whitespace character to apply.
+
+                      (&quot;|&[lr][sd]quo;|[\'\"\]\}\)]?)  # Tolerate closing quotes, parenthesis, etc.
+                      ((?:<[^>]+>)*)  # Tolerate tags
+
+                      \   # The space
+                      (?![a-z])  # Not followed by a lowercase character.
+
+                   /$1$2$3&nbsp;\ /gx;
+
+
+    # Periods get double spaces if it's not followed by a lowercase letter.  However, if it's followed by a capital letter and the
+    # preceding word is in the list of acceptable abbreviations, it won't get the double space.  Yes, I do realize I am seriously
+    # over-engineering this.
+
+    $text =~ s/  ([^\ \t\r\n]+)  # The word prior to the period.
+
+                      \.
+
+                      (&quot;|&[lr][sd]quo;|[\'\"\]\}\)]?)  # Tolerate closing quotes, parenthesis, etc.
+                      ((?:<[^>]+>)*)  # Tolerate tags
+
+                      \   # The space
+                      ([^a-z])   # The next character, if it's not a lowercase letter.
+
+                  /$1 . '.' . $2 . $3 . MaybeExpand($1, $4) . $4/gex;
+
+    sub MaybeExpand #(leadWord, nextLetter)
+        {
+        my ($leadWord, $nextLetter) = @_;
+
+        if ($nextLetter =~ /^[A-Z]$/ && exists $abbreviations{ lc($leadWord) } )
+            { return ' '; }
+        else
+            { return '&nbsp; '; };
+        };
+
+    return $text;
+    };
+
+
+#
+#   Function: ConvertAmpChars
+#
+#   Converts certain characters to their HTML amp char equivalents.
+#
+#   Parameters:
+#
+#       text - The text to convert.
+#
+#   Returns:
+#
+#       The converted text.
+#
+sub ConvertAmpChars #(text)
+    {
+    my ($self, $text) = @_;
+
+    $text =~ s/&/&amp;/g;
+    $text =~ s/\"/&quot;/g;
+    $text =~ s/</&lt;/g;
+    $text =~ s/>/&gt;/g;
+
+    return $text;
+    };
+
+
+#
+#   Function: RestoreAmpChars
+#
+#   Restores all amp characters to their original state.  This works with both <NDMarkup> amp chars and fancy quotes.
+#
+#   Parameters:
+#
+#       text - The text to convert.
+#
+#   Returns:
+#
+#       The converted text.
+#
+sub RestoreAmpChars #(text)
+    {
+    my ($self, $text) = @_;
+
+    $text = NaturalDocs::NDMarkup->RestoreAmpChars($text);
+    $text =~ s/&[lr]squo;/\'/g;
+    $text =~ s/&[lr]dquo;/\"/g;
+
+    return $text;
+    };
+
+
+#
+#   Function: AddHiddenBreaks
+#
+#   Adds hidden breaks to symbols.  Puts them after symbol and directory separators so long names won't screw up the layout.
+#
+#   Parameters:
+#
+#       string - The string to break.
+#
+#   Returns:
+#
+#       The string with hidden breaks.
+#
+sub AddHiddenBreaks #(string)
+    {
+    my ($self, $string) = @_;
+
+    # \.(?=.{5,}) instead of \. so file extensions don't get breaks.
+    # :+ instead of :: because Mac paths are separated by a : and we want to get those too.
+
+    $string =~ s/(\w(?:\.(?=.{5,})|:+|->|\\|\/))(\w)/$1 . '<wbr>' . $2/ge;
+
+    return $string;
+    };
+
+
+#
+#   Function: FindFirstFile
+#
+#   A function that finds and returns the first file entry in the menu, or undef if none.
+#
+sub FindFirstFile
+    {
+    # Hidden parameter: arrayref
+    # Used for recursion only.
+
+    my ($self, $arrayref) = @_;
+
+    if (!defined $arrayref)
+        {  $arrayref = NaturalDocs::Menu->Content();  };
+
+    foreach my $entry (@$arrayref)
+        {
+        if ($entry->Type() == ::MENU_FILE())
+            {
+            return $entry;
+            }
+        elsif ($entry->Type() == ::MENU_GROUP())
+            {
+            my $result = $self->FindFirstFile($entry->GroupContent());
+            if (defined $result)
+                {  return $result;  };
+            };
+        };
+
+    return undef;
+    };
+
+
+#
+#   Function: ExpandMenu
+#
+#   Determines which groups should be expanded.
+#
+#   Parameters:
+#
+#       sourceFile - The source <FileName> to use if you're looking for a source file.
+#       indexType - The index <TopicType> to use if you're looking for an index.
+#       selectionHierarchy - The <FileName> the menu is being built for.  Does not have to be on the menu itself.
+#       rootLength - The length of the menu's root group, *not* including the contents of subgroups.
+#
+#   Returns:
+#
+#       An arrayref of all the group numbers that should be expanded.  At minimum, it will contain the numbers of the groups
+#       present in <menuSelectionHierarchy>, though it may contain more.
+#
+sub ExpandMenu #(FileName sourceFile, TopicType indexType, NaturalDocs::Menu::Entry[] selectionHierarchy, int rootLength) -> int[] groupsToExpand
+    {
+    my ($self, $sourceFile, $indexType, $menuSelectionHierarchy, $rootLength) = @_;
+
+    my $toExpand = [ ];
+
+
+    # First expand everything in the selection hierarchy.
+
+    my $length = $rootLength;
+
+    foreach my $entry (@$menuSelectionHierarchy)
+        {
+        $length += $menuGroupLengths{$entry};
+        push @$toExpand, $menuGroupNumbers{$entry};
+        };
+
+
+    # Now do multiple passes of group expansion as necessary.  We start from bottomIndex and expand outwards.  We stop going
+    # in a direction if a group there is too long -- we do not skip over it and check later groups as well.  However, if one direction
+    # stops, the other can keep going.
+
+    my $pass = 1;
+    my $hasSubGroups;
+
+    while ($length < MENU_LENGTH_LIMIT)
+        {
+        my $content;
+        my $topIndex;
+        my $bottomIndex;
+
+
+        if ($pass == 1)
+            {
+            # First pass, we expand the selection's siblings.
+
+            if (scalar @$menuSelectionHierarchy)
+                {  $content = $menuSelectionHierarchy->[0]->GroupContent();  }
+            else
+                {  $content = NaturalDocs::Menu->Content();  };
+
+            $bottomIndex = 0;
+
+            while ($bottomIndex < scalar @$content &&
+                     !($content->[$bottomIndex]->Type() == ::MENU_FILE() &&
+                       $content->[$bottomIndex]->Target() eq $sourceFile) &&
+                     !($content->[$bottomIndex]->Type() != ::MENU_INDEX() &&
+                       $content->[$bottomIndex]->Target() eq $indexType) )
+                {  $bottomIndex++;  };
+
+            if ($bottomIndex == scalar @$content)
+                {  $bottomIndex = 0;  };
+            $topIndex = $bottomIndex - 1;
+            }
+
+        elsif ($pass == 2)
+            {
+            # If the section we just expanded had no sub-groups, do another pass trying to expand the parent's sub-groups.  The
+            # net effect is that groups won't collapse as much unnecessarily.  Someone can click on a file in a sub-group and the
+            # groups in the parent will stay open.
+
+            if (!$hasSubGroups && scalar @$menuSelectionHierarchy)
+                {
+                if (scalar @$menuSelectionHierarchy > 1)
+                    {  $content = $menuSelectionHierarchy->[1]->GroupContent();  }
+                else
+                    {  $content = NaturalDocs::Menu->Content();  };
+
+                $bottomIndex = 0;
+
+                while ($bottomIndex < scalar @$content &&
+                         $content->[$bottomIndex] != $menuSelectionHierarchy->[0])
+                    {  $bottomIndex++;  };
+
+                $topIndex = $bottomIndex - 1;
+                $bottomIndex++;  # Increment past our own group.
+                $hasSubGroups = undef;
+                }
+            else
+                {  last;  };
+            }
+
+        # No more passes.
+        else
+            {  last;  };
+
+
+        while ( ($topIndex >= 0 || $bottomIndex < scalar @$content) && $length < MENU_LENGTH_LIMIT)
+            {
+            # We do the bottom first.
+
+            while ($bottomIndex < scalar @$content && $content->[$bottomIndex]->Type() != ::MENU_GROUP())
+                {  $bottomIndex++;  };
+
+            if ($bottomIndex < scalar @$content)
+                {
+                my $bottomEntry = $content->[$bottomIndex];
+                $hasSubGroups = 1;
+
+                if ($length + $menuGroupLengths{$bottomEntry} <= MENU_LENGTH_LIMIT)
+                    {
+                    $length += $menuGroupLengths{$bottomEntry};
+                    push @$toExpand, $menuGroupNumbers{$bottomEntry};
+                    $bottomIndex++;
+                    }
+                else
+                    {  $bottomIndex = scalar @$content;  };
+                };
+
+            # Top next.
+
+            while ($topIndex >= 0 && $content->[$topIndex]->Type() != ::MENU_GROUP())
+                {  $topIndex--;  };
+
+            if ($topIndex >= 0)
+                {
+                my $topEntry = $content->[$topIndex];
+                $hasSubGroups = 1;
+
+                if ($length + $menuGroupLengths{$topEntry} <= MENU_LENGTH_LIMIT)
+                    {
+                    $length += $menuGroupLengths{$topEntry};
+                    push @$toExpand, $menuGroupNumbers{$topEntry};
+                    $topIndex--;
+                    }
+                else
+                    {  $topIndex = -1;  };
+                };
+            };
+
+
+        $pass++;
+        };
+
+    return $toExpand;
+    };
+
+
+#
+#   Function: GetMenuSelectionHierarchy
+#
+#   Finds the sequence of menu groups that contain the current selection.
+#
+#   Parameters:
+#
+#       sourceFile - The source <FileName> to use if you're looking for a source file.
+#       indexType - The index <TopicType> to use if you're looking for an index.
+#
+#   Returns:
+#
+#       An arrayref of the <NaturalDocs::Menu::Entry> objects of each group surrounding the selected menu item.  First entry is the
+#       group immediately encompassing it, and each subsequent entry works its way towards the outermost group.
+#
+sub GetMenuSelectionHierarchy #(FileName sourceFile, TopicType indexType) -> NaturalDocs::Menu::Entry[] selectionHierarchy
+    {
+    my ($self, $sourceFile, $indexType) = @_;
+
+    my $hierarchy = [ ];
+
+    $self->FindMenuSelection($sourceFile, $indexType, $hierarchy, NaturalDocs::Menu->Content());
+
+    return $hierarchy;
+    };
+
+
+#
+#   Function: FindMenuSelection
+#
+#   A recursive function that deterimes if it or any of its sub-groups has the menu selection.
+#
+#   Parameters:
+#
+#       sourceFile - The source <FileName> to use if you're looking for a source file.
+#       indexType - The index <TopicType> to use if you're looking for an index.
+#       hierarchyRef - A reference to the menu selection hierarchy.
+#       entries - An arrayref of <NaturalDocs::Menu::Entries> to search.
+#
+#   Returns:
+#
+#       Whether this group or any of its subgroups had the selection.  If true, it will add any subgroups to the menu selection
+#       hierarchy but not itself.  This prevents the topmost entry from being added.
+#
+sub FindMenuSelection #(FileName sourceFile, TopicType indexType, NaturalDocs::Menu::Entry[] hierarchyRef, NaturalDocs::Menu::Entry[] entries) -> bool hasSelection
+    {
+    my ($self, $sourceFile, $indexType, $hierarchyRef, $entries) = @_;
+
+    foreach my $entry (@$entries)
+        {
+        if ($entry->Type() == ::MENU_GROUP())
+            {
+            # If the subgroup has the selection...
+            if ( $self->FindMenuSelection($sourceFile, $indexType, $hierarchyRef, $entry->GroupContent()) )
+                {
+                push @$hierarchyRef, $entry;
+                return 1;
+                };
+            }
+
+        elsif ($entry->Type() == ::MENU_FILE())
+            {
+            if ($sourceFile eq $entry->Target())
+                {  return 1;  };
+            }
+
+        elsif ($entry->Type() == ::MENU_INDEX())
+            {
+            if ($indexType eq $entry->Target)
+                {  return 1;  };
+            };
+        };
+
+    return 0;
+    };
+
+
+#
+#   Function: ResetToolTips
+#
+#   Resets the <ToolTip Package Variables> for a new page.
+#
+#   Parameters:
+#
+#       samePage  - Set this flag if there's the possibility that the next batch of tooltips may be on the same page as the last.
+#
+sub ResetToolTips #(samePage)
+    {
+    my ($self, $samePage) = @_;
+
+    if (!$samePage)
+        {
+        $tooltipLinkNumber = 1;
+        $tooltipNumber = 1;
+        };
+
+    $tooltipHTML = undef;
+    %tooltipSymbolsToNumbers = ( );
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/ClassHierarchy.pm b/docs/tool/Modules/NaturalDocs/ClassHierarchy.pm
new file mode 100644
index 00000000..9f64cb47
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/ClassHierarchy.pm
@@ -0,0 +1,860 @@
+###############################################################################
+#
+#   Package: NaturalDocs::ClassHierarchy
+#
+###############################################################################
+#
+#   A package that handles all the gory details of managing the class hierarchy.  It handles the hierarchy itself, which files define
+#   them, rebuilding the files that are affected by changes, and loading and saving them to a file.
+#
+#   Usage and Dependencies:
+#
+#       - <NaturalDocs::Settings> and <NaturalDocs::Project> must be initialized before use.
+#
+#       - <NaturalDocs::SymbolTable> must be initialized before <Load()> is called.  It must reflect the state as of the last time
+#          Natural Docs was run.
+#
+#       - <Load()> must be called to initialize the package.  At this point, the <Information Functions> will return the state as
+#         of the last time Natural Docs was run.  You are free to resolve <NaturalDocs::SymbolTable()> afterwards.
+#
+#       - <Purge()> must be called, and then <NaturalDocs::Parser->ParseForInformation()> must be called on all files that
+#         have changed so it can fully resolve the hierarchy via the <Modification Functions()>.  Afterwards the
+#         <Information Functions> will reflect the current state of the code.
+#
+#       - <Save()> must be called to commit any changes to the symbol table back to disk.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+
+use strict;
+use integer;
+
+use NaturalDocs::ClassHierarchy::Class;
+use NaturalDocs::ClassHierarchy::File;
+
+package NaturalDocs::ClassHierarchy;
+
+
+###############################################################################
+# Group: Variables
+
+#
+#   handle: CLASS_HIERARCHY_FILEHANDLE
+#   The file handle used with <ClassHierarchy.nd>.
+#
+
+#
+#   hash: classes
+#
+#   A hash of all the classes.  The keys are the class <SymbolStrings> and the values are <NaturalDocs::ClassHierarchy::Classes>.
+#
+my %classes;
+
+#
+#   hash: files
+#
+#   A hash of the hierarchy information referenced by file.  The keys are the <FileNames>, and the values are
+#   <NaturalDocs::ClassHierarchy::File>s.
+#
+my %files;
+
+#
+#   hash: parentReferences
+#
+#   A hash of all the parent reference strings and what they resolve to.  The keys are the <ReferenceStrings> and the values are
+#   the class <SymbolStrings> that they resolve to.
+#
+my %parentReferences;
+
+#
+#   object: watchedFile
+#
+#   A <NaturalDocs::ClassHierarchy::File> object of the file being watched for changes.  This is compared to the version in <files>
+#   to see if anything was changed since the last parse.
+#
+my $watchedFile;
+
+#
+#   string: watchedFileName
+#
+#   The <FileName> of the watched file, if any.  If there is no watched file, this will be undef.
+#
+my $watchedFileName;
+
+#
+#   bool: dontRebuildFiles
+#
+#   A bool to set if you don't want changes in the hierarchy to cause files to be rebuilt.
+#
+my $dontRebuildFiles;
+
+
+
+###############################################################################
+# Group: Files
+
+
+#
+#   File: ClassHierarchy.nd
+#
+#   Stores the class hierarchy on disk.
+#
+#   Format:
+#
+#       > [BINARY_FORMAT]
+#       > [VersionInt: app version]
+#
+#       The standard <BINARY_FORMAT> and <VersionInt> header.
+#
+#       > [SymbolString: class or undef to end]
+#
+#       Next we begin a class segment with its <SymbolString>.  These continue until the end of the file.  Only defined classes are
+#       included.
+#
+#       > [UInt32: number of files]
+#       > [AString16: file] [AString16: file] ...
+#
+#       Next there is the number of files that define that class.  It's a UInt32, which seems like overkill, but I could imagine every
+#       file in a huge C++ project being under the same namespace, and thus contributing its own definition.  It's theoretically
+#       possible.
+#
+#       Following the number is that many file names.  You must remember the index of each file, as they will be important later.
+#       Indexes start at one because zero has a special meaning.
+#
+#       > [UInt8: number of parents]
+#       > ( [ReferenceString (no type): parent]
+#       >   [UInt32: file index] [UInt32: file index] ... [UInt32: 0] ) ...
+#
+#       Next there is the number of parents defined for this class.  For each one, we define a parent segment, which consists of
+#       its <ReferenceString>, and then a zero-terminated string of indexes of the files that define that parent as part of that class.
+#       The indexes start at one, and are into the list of files we saw previously.
+#
+#       Note that we do store class segments for classes without parents, but not for undefined classes.
+#
+#       This concludes a class segment.  These segments continue until an undef <SymbolString>.
+#
+#   See Also:
+#
+#       <File Format Conventions>
+#
+#   Revisions:
+#
+#       1.22:
+#
+#           - Classes and parents switched from AString16s to <SymbolStrings> and <ReferenceStrings>.
+#           - A ending undef <SymbolString> was added to the end.  Previously it stopped when the file ran out.
+#
+#       1.2:
+#
+#           - This file was introduced in 1.2.
+#
+
+
+###############################################################################
+# Group: File Functions
+
+
+#
+#   Function: Load
+#
+#   Loads the class hierarchy from disk.
+#
+sub Load
+    {
+    my ($self) = @_;
+
+    $dontRebuildFiles = 1;
+
+    my $fileIsOkay;
+    my $fileName = NaturalDocs::Project->DataFile('ClassHierarchy.nd');
+
+    if (!NaturalDocs::Settings->RebuildData() && open(CLASS_HIERARCHY_FILEHANDLE, '<' . $fileName))
+        {
+        # See if it's binary.
+        binmode(CLASS_HIERARCHY_FILEHANDLE);
+
+        my $firstChar;
+        read(CLASS_HIERARCHY_FILEHANDLE, $firstChar, 1);
+
+        if ($firstChar != ::BINARY_FORMAT())
+            {
+            close(CLASS_HIERARCHY_FILEHANDLE);
+            }
+        else
+            {
+            my $version = NaturalDocs::Version->FromBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE);
+
+            # Minor bugs were fixed in 1.33 that may affect the stored data.
+
+            if (NaturalDocs::Version->CheckFileFormat( $version, NaturalDocs::Version->FromString('1.33') ))
+                {  $fileIsOkay = 1;  }
+            else
+                {  close(CLASS_HIERARCHY_FILEHANDLE);  };
+            };
+        };
+
+
+    if (!$fileIsOkay)
+        {
+        NaturalDocs::Project->ReparseEverything();
+        }
+    else
+        {
+        my $raw;
+
+        for (;;)
+            {
+            # [SymbolString: class or undef to end]
+
+            my $class = NaturalDocs::SymbolString->FromBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE);
+
+            if (!defined $class)
+                {  last;  };
+
+            # [UInt32: number of files]
+
+            read(CLASS_HIERARCHY_FILEHANDLE, $raw, 4);
+            my $numberOfFiles = unpack('N', $raw);
+
+            my @files;
+
+            while ($numberOfFiles)
+                {
+                # [AString16: file]
+
+                read(CLASS_HIERARCHY_FILEHANDLE, $raw, 2);
+                my $fileLength = unpack('n', $raw);
+
+                my $file;
+                read(CLASS_HIERARCHY_FILEHANDLE, $file, $fileLength);
+
+                push @files, $file;
+                $self->AddClass($file, $class, NaturalDocs::Languages->LanguageOf($file)->Name());
+
+                $numberOfFiles--;
+                };
+
+            # [UInt8: number of parents]
+
+            read(CLASS_HIERARCHY_FILEHANDLE, $raw, 1);
+            my $numberOfParents = unpack('C', $raw);
+
+            while ($numberOfParents)
+                {
+                # [ReferenceString (no type): parent]
+
+                my $parent = NaturalDocs::ReferenceString->FromBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE,
+                                                                                                         ::BINARYREF_NOTYPE(),
+                                                                                                         ::REFERENCE_CH_PARENT());
+
+                for (;;)
+                    {
+                    # [UInt32: file index or 0]
+
+                    read(CLASS_HIERARCHY_FILEHANDLE, $raw, 4);
+                    my $fileIndex = unpack('N', $raw);
+
+                    if ($fileIndex == 0)
+                        {  last;  }
+
+                    $self->AddParentReference( $files[$fileIndex - 1], $class, $parent );
+                    };
+
+                $numberOfParents--;
+                };
+            };
+
+        close(CLASS_HIERARCHY_FILEHANDLE);
+        };
+
+    $dontRebuildFiles = undef;
+    };
+
+
+#
+#   Function: Save
+#
+#   Saves the class hierarchy to disk.
+#
+sub Save
+    {
+    my ($self) = @_;
+
+    open (CLASS_HIERARCHY_FILEHANDLE, '>' . NaturalDocs::Project->DataFile('ClassHierarchy.nd'))
+        or die "Couldn't save " . NaturalDocs::Project->DataFile('ClassHierarchy.nd') . ".\n";
+
+    binmode(CLASS_HIERARCHY_FILEHANDLE);
+
+    print CLASS_HIERARCHY_FILEHANDLE '' . ::BINARY_FORMAT();
+    NaturalDocs::Version->ToBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE, NaturalDocs::Settings->AppVersion());
+
+    while (my ($class, $classObject) = each %classes)
+        {
+        if ($classObject->IsDefined())
+            {
+            # [SymbolString: class or undef to end]
+
+            NaturalDocs::SymbolString->ToBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE, $class);
+
+            # [UInt32: number of files]
+
+            my @definitions = $classObject->Definitions();
+            my %definitionIndexes;
+
+            print CLASS_HIERARCHY_FILEHANDLE pack('N', scalar @definitions);
+
+            for (my $i = 0; $i < scalar @definitions; $i++)
+                {
+                # [AString16: file]
+                print CLASS_HIERARCHY_FILEHANDLE pack('nA*', length($definitions[$i]), $definitions[$i]);
+                $definitionIndexes{$definitions[$i]} = $i + 1;
+                };
+
+            # [UInt8: number of parents]
+
+            my @parents = $classObject->ParentReferences();
+            print CLASS_HIERARCHY_FILEHANDLE pack('C', scalar @parents);
+
+            foreach my $parent (@parents)
+                {
+                # [ReferenceString (no type): parent]
+
+                NaturalDocs::ReferenceString->ToBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE, $parent, ::BINARYREF_NOTYPE());
+
+                # [UInt32: file index]
+
+                my @parentDefinitions = $classObject->ParentReferenceDefinitions($parent);
+
+                foreach my $parentDefinition (@parentDefinitions)
+                    {
+                    print CLASS_HIERARCHY_FILEHANDLE pack('N', $definitionIndexes{$parentDefinition});
+                    };
+
+                # [UInt32: 0]
+                print CLASS_HIERARCHY_FILEHANDLE pack('N', 0);
+                };
+            };
+        };
+
+    # [SymbolString: class or undef to end]
+
+    NaturalDocs::SymbolString->ToBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE, undef);
+
+    close(CLASS_HIERARCHY_FILEHANDLE);
+    };
+
+
+#
+#   Function: Purge
+#
+#   Purges the hierarchy of files that no longer have Natural Docs content.
+#
+sub Purge
+    {
+    my ($self) = @_;
+
+    my $filesToPurge = NaturalDocs::Project->FilesToPurge();
+
+    foreach my $file (keys %$filesToPurge)
+        {
+        $self->DeleteFile($file);
+        };
+    };
+
+
+
+###############################################################################
+# Group: Interface Functions
+
+
+#
+#   Function: OnInterpretationChange
+#
+#   Called by <NaturalDocs::SymbolTable> whenever a class hierarchy reference's intepretation changes, meaning it switched
+#   from one symbol to another.
+#
+#       reference - The <ReferenceString> whose current interpretation changed.
+#
+sub OnInterpretationChange #(reference)
+    {
+    my ($self, $reference) = @_;
+
+    if (NaturalDocs::ReferenceString->TypeOf($reference) == ::REFERENCE_CH_PARENT())
+        {
+        # The approach here is simply to completely delete the reference and readd it.  This is less than optimal efficiency, since it's
+        # being removed and added from %files too, even though that isn't required.  However, the simpler code is worth it
+        # considering this will only happen when a parent reference becomes defined or undefined, or on the rare languages (like C#)
+        # that allow relative parent references.
+
+        my $oldTargetSymbol = $parentReferences{$reference};
+        my $oldTargetObject = $classes{$oldTargetSymbol};
+
+        my @classesWithReferenceParent = $oldTargetObject->Children();
+
+        # Each entry is an arrayref of file names.  Indexes are the same as classesWithReferenceParent's.
+        my @filesDefiningReferenceParent;
+
+        foreach my $classWithReferenceParent (@classesWithReferenceParent)
+            {
+            my $fileList = [ $classes{$classWithReferenceParent}->ParentReferenceDefinitions($reference) ];
+            push @filesDefiningReferenceParent, $fileList;
+
+            foreach my $fileDefiningReferenceParent (@$fileList)
+                {
+                $self->DeleteParentReference($fileDefiningReferenceParent, $classWithReferenceParent, $reference);
+                };
+            };
+
+
+        # This will force the reference to be reinterpreted on the next add.
+
+        delete $parentReferences{$reference};
+
+
+        # Now we can just readd it.
+
+        for (my $i = 0; $i < scalar @classesWithReferenceParent; $i++)
+            {
+            foreach my $file (@{$filesDefiningReferenceParent[$i]})
+                {
+                $self->AddParentReference($file, $classesWithReferenceParent[$i], $reference);
+                };
+            };
+        };
+
+    # The only way for a REFERENCE_CH_CLASS reference to change is if the symbol is deleted.  That will be handled by
+    # <AnalyzeChanges()>, so we don't need to do anything here.
+    };
+
+
+#
+#   Function: OnTargetSymbolChange
+#
+#   Called by <NaturalDocs::SymbolTable> whenever a class hierarchy reference's target symbol changes, but the reference
+#   still resolves to the same symbol.
+#
+#   Parameters:
+#
+#       reference - The <ReferenceString> that was affected by the change.
+#
+sub OnTargetSymbolChange #(reference)
+    {
+    my ($self, $reference) = @_;
+
+    my $type = NaturalDocs::ReferenceString->TypeOf($reference);
+    my $class;
+
+    if ($type == ::REFERENCE_CH_PARENT())
+        {  $class = $parentReferences{$reference};  }
+    else # ($type == ::REFERENCE_CH_CLASS())
+        {
+        # Class references are global absolute, so we can just yank the symbol.
+        (undef, $class, undef, undef, undef, undef) = NaturalDocs::ReferenceString->InformationOf($reference);
+        };
+
+    $self->RebuildFilesFor($class, 1, 0, 1);
+    };
+
+
+
+###############################################################################
+# Group: Modification Functions
+
+
+#
+#   Function: AddClass
+#
+#   Adds a class to the hierarchy.
+#
+#   Parameters:
+#
+#       file - The <FileName> the class was defined in.
+#       class - The class <SymbolString>.
+#       languageName - The name of the language this applies to.
+#
+#   Note:
+#
+#       The file parameter must be defined when using this function externally.  It may be undef for internal use only.
+#
+sub AddClass #(file, class, languageName)
+    {
+    my ($self, $file, $class, $languageName) = @_;
+
+    if (!exists $classes{$class})
+        {
+        $classes{$class} = NaturalDocs::ClassHierarchy::Class->New();
+        NaturalDocs::SymbolTable->AddReference($self->ClassReferenceOf($class, $languageName), $file)
+        };
+
+    if (defined $file)
+        {
+        # If this was the first definition for this class...
+        if ($classes{$class}->AddDefinition($file))
+            {  $self->RebuildFilesFor($class, 1, 1, 1);  };
+
+        if (!exists $files{$file})
+            {  $files{$file} = NaturalDocs::ClassHierarchy::File->New();  };
+
+        $files{$file}->AddClass($class);
+
+        if (defined $watchedFileName)
+            {  $watchedFile->AddClass($class);  };
+        };
+    };
+
+
+#
+#   Function: AddParentReference
+#
+#   Adds a class-parent relationship to the hierarchy.  The classes will be created if they don't already exist.
+#
+#   Parameters:
+#
+#       file - The <FileName> the reference was defined in.
+#       class - The class <SymbolString>.
+#       symbol - The parent class <SymbolString>.
+#       scope - The package <SymbolString> that the reference appeared in.
+#       using - An arrayref of package <SymbolStrings> that the reference has access to via "using" statements.
+#       resolvingFlags - Any <Resolving Flags> to be used when resolving the reference.
+#
+#   Alternate Parameters:
+#
+#       file - The <FileName> the reference was defined in.
+#       class - The class <SymbolString>.
+#       reference - The parent <ReferenceString>.
+#
+sub AddParentReference #(file, class, symbol, scope, using, resolvingFlags) or (file, class, reference)
+    {
+    my ($self, $file, $class, $symbol, $parentReference);
+
+    if (scalar @_ == 7)
+        {
+        my ($scope, $using, $resolvingFlags);
+        ($self, $file, $class, $symbol, $scope, $using, $resolvingFlags) = @_;
+
+        $parentReference = NaturalDocs::ReferenceString->MakeFrom(::REFERENCE_CH_PARENT(), $symbol,
+                                                                                                    NaturalDocs::Languages->LanguageOf($file)->Name(),
+                                                                                                    $scope, $using, $resolvingFlags);
+        }
+    else
+        {
+        ($self, $file, $class, $parentReference) = @_;
+        $symbol = (NaturalDocs::ReferenceString->InformationOf($parentReference))[1];
+        };
+
+
+    # In case it doesn't already exist.
+    $self->AddClass($file, $class);
+
+    my $parent;
+    if (exists $parentReferences{$parentReference})
+        {
+        $parent = $parentReferences{$parentReference};
+        }
+    else
+        {
+        NaturalDocs::SymbolTable->AddReference($parentReference, $file);
+        my $parentTarget = NaturalDocs::SymbolTable->References($parentReference);
+
+        if (defined $parentTarget)
+            {  $parent = $parentTarget->Symbol();  }
+        else
+            {  $parent = $symbol;  };
+
+        # In case it doesn't already exist.
+        $self->AddClass(undef, $parent);
+
+        $parentReferences{$parentReference} = $parent;
+        };
+
+
+    # If this defined a new parent...
+    if ($classes{$class}->AddParentReference($parentReference, $file, \%parentReferences))
+        {
+        $classes{$parent}->AddChild($class);
+
+        $self->RebuildFilesFor($class, 0, 1, 0);
+        $self->RebuildFilesFor($parent, 0, 1, 0);
+        };
+
+    $files{$file}->AddParentReference($class, $parentReference);
+
+    if (defined $watchedFileName)
+        {  $watchedFile->AddParentReference($class, $parentReference);  };
+    };
+
+
+#
+#   Function: WatchFileForChanges
+#
+#   Watches a file for changes, which can then be applied by <AnalyzeChanges()>.  Definitions are not deleted via a DeleteClass()
+#   function.  Instead, a file is watched for changes, reparsed, and then a comparison is made to look for definitions that
+#   disappeared and any other relevant changes.
+#
+#   Parameters:
+#
+#       file - The <FileName> to watch.
+#
+sub WatchFileForChanges #(file)
+    {
+    my ($self, $file) = @_;
+
+    $watchedFile = NaturalDocs::ClassHierarchy::File->New();
+    $watchedFileName = $file;
+    };
+
+
+#
+#   Function: AnalyzeChanges
+#
+#   Checks the watched file for any changes that occured since the last time is was parsed, and updates the hierarchy as
+#   necessary.  Also sends any files that are affected to <NaturalDocs::Project->RebuildFile()>.
+#
+sub AnalyzeChanges
+    {
+    my ($self) = @_;
+
+    # If the file didn't have any classes before, and it still doesn't, it wont be in %files.
+    if (exists $files{$watchedFileName})
+        {
+        my @originalClasses = $files{$watchedFileName}->Classes();
+
+        foreach my $originalClass (@originalClasses)
+            {
+            # If the class isn't there the second time around...
+            if (!$watchedFile->HasClass($originalClass))
+                {  $self->DeleteClass($watchedFileName, $originalClass);  }
+
+            else
+                {
+                my @originalParents = $files{$watchedFileName}->ParentReferencesOf($originalClass);
+
+                foreach my $originalParent (@originalParents)
+                    {
+                    # If the parent reference wasn't there the second time around...
+                    if (!$watchedFile->HasParentReference($originalClass, $originalParent))
+                        {  $self->DeleteParentReference($watchedFileName, $originalClass, $originalParent);  };
+                    };
+                };
+            };
+        };
+
+
+    $watchedFile = undef;
+    $watchedFileName = undef;
+    };
+
+
+
+###############################################################################
+# Group: Information Functions
+
+
+#
+#   Function: ParentsOf
+#   Returns a <SymbolString> array of the passed class' parents, or an empty array if none.  Note that not all of them may be
+#   defined.
+#
+sub ParentsOf #(class)
+    {
+    my ($self, $class) = @_;
+
+    if (exists $classes{$class})
+        {  return $classes{$class}->Parents();  }
+    else
+        {  return ( );  };
+    };
+
+#
+#   Function: ChildrenOf
+#   Returns a <SymbolString> array of the passed class' children, or an empty array if none.  Note that not all of them may be
+#   defined.
+#
+sub ChildrenOf #(class)
+    {
+    my ($self, $class) = @_;
+
+    if (exists $classes{$class})
+        {  return $classes{$class}->Children();  }
+    else
+        {  return ( );  };
+    };
+
+
+
+###############################################################################
+# Group: Support Functions
+
+
+#
+#   Function: DeleteFile
+#
+#   Deletes a file and everything defined in it.
+#
+#   Parameters:
+#
+#       file - The <FileName>.
+#
+sub DeleteFile #(file)
+    {
+    my ($self, $file) = @_;
+
+    if (!exists $files{$file})
+        {  return;  };
+
+    my @classes = $files{$file}->Classes();
+    foreach my $class (@classes)
+        {
+        $self->DeleteClass($file, $class);
+        };
+
+    delete $files{$file};
+    };
+
+#
+#   Function: DeleteClass
+#
+#   Deletes a class definition from a file.  Will also delete any parent references from this class and file.  Will rebuild any file
+#   affected unless <dontRebuildFiles> is set.
+#
+#   Parameters:
+#
+#       file - The <FileName> that defines the class.
+#       class - The class <SymbolString>.
+#
+sub DeleteClass #(file, class)
+    {
+    my ($self, $file, $class) = @_;
+
+    my @parents = $files{$file}->ParentReferencesOf($class);
+    foreach my $parent (@parents)
+        {
+        $self->DeleteParentReference($file, $class, $parent);
+        };
+
+    $files{$file}->DeleteClass($class);
+
+    # If we're deleting the last definition of this class.
+    if ($classes{$class}->DeleteDefinition($file))
+        {
+        if (!$classes{$class}->HasChildren())
+            {
+            delete $classes{$class};
+
+            if (!$dontRebuildFiles)
+                {  NaturalDocs::Project->RebuildFile($file);  };
+            }
+        else
+            {  $self->RebuildFilesFor($class, 0, 1, 1);  };
+
+        };
+    };
+
+
+#
+#   Function: DeleteParentReference
+#
+#   Deletes a class' parent reference and returns whether it resulted in the loss of a parent class.  Will rebuild any file affected
+#   unless <dontRebuildFiles> is set.
+#
+#   Parameters:
+#
+#       file - The <FileName> that defines the reference.
+#       class - The class <SymbolString>.
+#       reference - The parent <ReferenceString>.
+#
+#   Returns:
+#
+#       If the class lost a parent as a result of this, it will return its <SymbolString>.  It will return undef otherwise.
+#
+sub DeleteParentReference #(file, class, reference)
+    {
+    my ($self, $file, $class, $reference) = @_;
+
+    if (!exists $classes{$class})
+        {  return;  };
+
+    $files{$file}->DeleteParentReference($class, $reference);
+
+    my $deletedParent = $classes{$class}->DeleteParentReference($reference, $file, \%parentReferences);
+
+    if (defined $deletedParent)
+        {
+        my $deletedParentObject = $classes{$deletedParent};
+
+        $deletedParentObject->DeleteChild($class);
+
+        $self->RebuildFilesFor($deletedParent, 0, 1, 0);
+        $self->RebuildFilesFor($class, 0, 1, 0);
+
+        if (!$deletedParentObject->HasChildren() && !$deletedParentObject->IsDefined())
+            {
+            delete $classes{$deletedParent};
+            NaturalDocs::SymbolTable->DeleteReference(
+                $self->ClassReferenceOf($class, NaturalDocs::Languages->LanguageOf($file)->Name()) );
+            };
+
+        return $deletedParent;
+        };
+
+    return undef;
+    };
+
+
+#
+#   Function: ClassReferenceOf
+#
+#   Returns the <REFERENCE_CH_CLASS> <ReferenceString> of the passed class <SymbolString>.
+#
+sub ClassReferenceOf #(class, languageName)
+    {
+    my ($self, $class, $languageName) = @_;
+
+    return NaturalDocs::ReferenceString->MakeFrom(::REFERENCE_CH_CLASS(), $class, $languageName, undef, undef,
+                                                                            ::RESOLVE_ABSOLUTE() | ::RESOLVE_NOPLURAL());
+    };
+
+
+#
+#   Function: RebuildFilesFor
+#
+#   Calls <NaturalDocs::Project->RebuildFile()> for every file defining the passed class, its parents, and/or its children.
+#   Returns without doing anything if <dontRebuildFiles> is set.
+#
+#   Parameters:
+#
+#       class - The class <SymbolString>.
+#       rebuildParents - Whether to rebuild the class' parents.
+#       rebuildSelf - Whether to rebuild the class.
+#       rebuildChildren - Whether to rebuild the class' children.
+#
+sub RebuildFilesFor #(class, rebuildParents, rebuildSelf, rebuildChildren)
+    {
+    my ($self, $class, $rebuildParents, $rebuildSelf, $rebuildChildren) = @_;
+
+    if ($dontRebuildFiles)
+        {  return;  };
+
+    my @classesToBuild;
+
+    if ($rebuildParents)
+        {  @classesToBuild = $classes{$class}->Parents();  };
+    if ($rebuildSelf)
+        {  push @classesToBuild, $class;  };
+    if ($rebuildChildren)
+        {  push @classesToBuild, $classes{$class}->Children();  };
+
+    foreach my $classToBuild (@classesToBuild)
+        {
+        my @definitions = $classes{$classToBuild}->Definitions();
+
+        foreach my $definition (@definitions)
+            {  NaturalDocs::Project->RebuildFile($definition);  };
+        };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/ClassHierarchy/Class.pm b/docs/tool/Modules/NaturalDocs/ClassHierarchy/Class.pm
new file mode 100644
index 00000000..b6faedfc
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/ClassHierarchy/Class.pm
@@ -0,0 +1,412 @@
+###############################################################################
+#
+#   Class: NaturalDocs::ClassHierarchy::Class
+#
+###############################################################################
+#
+#   An object that stores information about a class in the hierarchy.  It does not store its <SymbolString>; it assumes that it will
+#   be stored in a hashref where the key is the <SymbolString>.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::ClassHierarchy::Class;
+
+
+#
+#   Constants: Members
+#
+#   The class is implemented as a blessed arrayref.  The keys are the constants below.
+#
+#   DEFINITIONS - An existence hashref of all the <FileNames> which define this class.  Undef if none.
+#   PARENTS - An existence hashref of the <SymbolStrings> of all the parents this class has.
+#   CHILDREN - An existence hashref of the <SymbolStrings> of all the children this class has.
+#   PARENT_REFERENCES - A hashref of the parent <ReferenceStrings> this class has.  The keys are the <ReferenceStrings>,
+#                                      and the values are existence hashrefs of all the <FileNames> that define them.  Undef if none.
+#
+use NaturalDocs::DefineMembers 'DEFINITIONS', 'PARENTS', 'CHILDREN', 'PARENT_REFERENCES';
+# Dependency: New() depends on the order of these constants, as well as the class not being derived from any other.
+
+
+###############################################################################
+# Group: Modification Functions
+
+
+#
+#   Function: New
+#
+#   Creates and returns a new class.
+#
+sub New
+    {
+    # Dependency: This function depends on the order of the constants, as well as the class not being derived from any other.
+    my ($package, $definitionFile) = @_;
+
+    my $object = [ undef, undef, undef, undef ];
+    bless $object, $package;
+
+    return $object;
+    };
+
+
+#
+#   Function: AddDefinition
+#
+#   Adds a rew definition of this class and returns if that was the first definition.
+#
+#   Parameters:
+#
+#       file - The <FileName> the definition appears in.
+#
+#   Returns:
+#
+#       Whether this was the first definition of this class.
+#
+sub AddDefinition #(file)
+    {
+    my ($self, $file) = @_;
+
+    my $wasFirst;
+
+    if (!defined $self->[DEFINITIONS])
+        {
+        $self->[DEFINITIONS] = { };
+        $wasFirst = 1;
+        };
+
+    $self->[DEFINITIONS]->{$file} = 1;
+
+    return $wasFirst;
+    };
+
+
+#
+#   Function: DeleteDefinition
+#
+#   Removes the definition of this class and returns if there are no more definitions.  Note that if there are no more
+#   definitions, you may still want to keep the object around if <HasChildren()> returns true.
+#
+#   Parameters:
+#
+#       file - The <FileName> the definition appears in.
+#
+#   Returns:
+#
+#       Whether this deleted the last definition of this class.
+#
+sub DeleteDefinition #(file)
+    {
+    my ($self, $file) = @_;
+
+    if (defined $self->[DEFINITIONS])
+        {
+        delete $self->[DEFINITIONS]->{$file};
+
+        if (!scalar keys %{$self->[DEFINITIONS]})
+            {
+            $self->[DEFINITIONS] = undef;
+            return 1;
+            };
+        };
+
+    return undef;
+    };
+
+
+#
+#   Function: AddParentReference
+#
+#   Adds a parent reference to the class and return whether it resulted in a new parent class.
+#
+#   Parameters:
+#
+#       reference - The <ReferenceString> used to determine the parent.
+#       file - The <FileName> the parent reference is in.
+#       referenceTranslations - A hashref of what each reference currently resolves to.  The keys are the
+#                                         <ReferenceStrings> and the values are class <SymbolStrings>.  It should include an entry for
+#                                         the reference parameter above.
+#
+#   Returns:
+#
+#       If the reference adds a new parent, it will return that parent's <SymbolString>.  Otherwise it will return undef.
+#
+sub AddParentReference #(reference, file, referenceTranslations)
+    {
+    my ($self, $reference, $file, $referenceTranslations) = @_;
+
+    if (!defined $self->[PARENT_REFERENCES])
+        {  $self->[PARENT_REFERENCES] = { };  };
+    if (!defined $self->[PARENTS])
+        {  $self->[PARENTS] = { };  };
+
+
+    if (!exists $self->[PARENT_REFERENCES]->{$reference})
+        {
+        $self->[PARENT_REFERENCES]->{$reference} = { $file => 1 };
+
+        my $symbol = $referenceTranslations->{$reference};
+
+        if (!exists $self->[PARENTS]->{$symbol})
+            {
+            $self->[PARENTS]->{$symbol} = 1;
+            return $symbol;
+            }
+        else
+            {  return undef;  };
+        }
+    else
+        {
+        $self->[PARENT_REFERENCES]->{$reference}->{$file} = 1;
+        return undef;
+        };
+    };
+
+#
+#   Function: DeleteParentReference
+#
+#   Deletes a parent reference from the class and return whether it resulted in a loss of a parent class.
+#
+#   Parameters:
+#
+#       reference - The <ReferenceString> used to determine the parent.
+#       file - The <FileName> the parent declaration is in.
+#       referenceTranslations - A hashref of what each reference currently resolves to.  The keys are the
+#                                         <ReferenceStrings> and the values are class <SymbolStrings>.  It should include an entry for
+#                                         the reference parameter above.
+#
+#   Returns:
+#
+#       If this causes a parent class to be lost, it will return that parent's <SymbolString>.  Otherwise it will return undef.
+#
+sub DeleteParentReference #(reference, file, referenceTranslations)
+    {
+    my ($self, $reference, $file, $referenceTranslations) = @_;
+
+    if (defined $self->[PARENT_REFERENCES] && exists $self->[PARENT_REFERENCES]->{$reference} &&
+        exists $self->[PARENT_REFERENCES]->{$reference}->{$file})
+        {
+        delete $self->[PARENT_REFERENCES]->{$reference}->{$file};
+
+        # Quit if there are other definitions of this reference.
+        if (scalar keys %{$self->[PARENT_REFERENCES]->{$reference}})
+            {  return undef;  };
+
+        delete $self->[PARENT_REFERENCES]->{$reference};
+
+        if (!scalar keys %{$self->[PARENT_REFERENCES]})
+            {  $self->[PARENT_REFERENCES] = undef;  };
+
+        my $parent = $referenceTranslations->{$reference};
+
+        # Check if any other references resolve to the same parent.
+        if (defined $self->[PARENT_REFERENCES])
+            {
+            foreach my $parentReference (keys %{$self->[PARENT_REFERENCES]})
+                {
+                if ($referenceTranslations->{$parentReference} eq $parent)
+                    {  return undef;  };
+                };
+            };
+
+        # If we got this far, no other parent references resolve to this symbol.
+
+        delete $self->[PARENTS]->{$parent};
+
+        if (!scalar keys %{$self->[PARENTS]})
+            {  $self->[PARENTS] = undef;  };
+
+        return $parent;
+        }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: AddChild
+#   Adds a child <SymbolString> to the class.  Unlike <AddParentReference()>, this does not keep track of anything other than
+#   whether it has it or not.
+#
+#   Parameters:
+#
+#       child - The <SymbolString> to add.
+#
+sub AddChild #(child)
+    {
+    my ($self, $child) = @_;
+
+    if (!defined $self->[CHILDREN])
+        {  $self->[CHILDREN] = { };  };
+
+    $self->[CHILDREN]->{$child} = 1;
+    };
+
+#
+#   Function: DeleteChild
+#   Deletes a child <SymbolString> from the class.  Unlike <DeleteParentReference()>, this does not keep track of anything other
+#   than whether it has it or not.
+#
+#   Parameters:
+#
+#       child - The <SymbolString> to delete.
+#
+sub DeleteChild #(child)
+    {
+    my ($self, $child) = @_;
+
+    if (defined $self->[CHILDREN])
+        {
+        delete $self->[CHILDREN]->{$child};
+
+        if (!scalar keys %{$self->[CHILDREN]})
+            {  $self->[CHILDREN] = undef;  };
+        };
+    };
+
+
+
+###############################################################################
+# Group: Information Functions
+
+#
+#   Function: Definitions
+#   Returns an array of the <FileNames> that define this class, or an empty array if none.
+#
+sub Definitions
+    {
+    my ($self) = @_;
+
+    if (defined $self->[DEFINITIONS])
+        {  return keys %{$self->[DEFINITIONS]};  }
+    else
+        {  return ( );  };
+    };
+
+#
+#   Function: IsDefinedIn
+#   Returns whether the class is defined in the passed <FileName>.
+#
+sub IsDefinedIn #(file)
+    {
+    my ($self, $file) = @_;
+
+    if (defined $self->[DEFINITIONS])
+        {  return exists $self->[DEFINITIONS]->{$file};  }
+    else
+        {  return 0;  };
+    };
+
+#
+#   Function: IsDefined
+#   Returns whether the class is defined in any files.
+#
+sub IsDefined
+    {
+    my ($self) = @_;
+    return defined $self->[DEFINITIONS];
+    };
+
+#
+#   Function: ParentReferences
+#   Returns an array of the parent <ReferenceStrings>, or an empty array if none.
+#
+sub ParentReferences
+    {
+    my ($self) = @_;
+
+    if (defined $self->[PARENT_REFERENCES])
+        {  return keys %{$self->[PARENT_REFERENCES]};  }
+    else
+        {  return ( );  };
+    };
+
+#
+#   Function: HasParentReference
+#   Returns whether the class has the passed parent <ReferenceString>.
+#
+sub HasParentReference #(reference)
+    {
+    my ($self, $reference) = @_;
+    return (defined $self->[PARENT_REFERENCES] && exists $self->[PARENT_REFERENCES]->{$reference});
+    };
+
+#
+#   Function: HasParentReferences
+#   Returns whether the class has any parent <ReferenceStrings>.
+#
+sub HasParentReferences
+    {
+    my ($self) = @_;
+    return defined $self->[PARENT_REFERENCES];
+    };
+
+#
+#   Function: Parents
+#   Returns an array of the parent <SymbolStrings>, or an empty array if none.
+#
+sub Parents
+    {
+    my ($self) = @_;
+
+    if (defined $self->[PARENTS])
+        {  return keys %{$self->[PARENTS]};  }
+    else
+        {  return ( );  };
+    };
+
+#
+#   Function: HasParents
+#   Returns whether the class has any parent <SymbolStrings> defined.
+#
+sub HasParents
+    {
+    my ($self) = @_;
+    return defined $self->[PARENTS];
+    };
+
+#
+#   Function: Children
+#   Returns an array of the child <SymbolStrings>, or an empty array if none.
+#
+sub Children
+    {
+    my ($self) = @_;
+
+    if (defined $self->[CHILDREN])
+        {  return keys %{$self->[CHILDREN]};  }
+    else
+        {  return ( );  };
+    };
+
+#
+#   Function: HasChildren
+#   Returns whether any child <SymbolStrings> are defined.
+#
+sub HasChildren
+    {
+    my ($self) = @_;
+    return defined $self->[CHILDREN];
+    };
+
+
+#
+#   Function: ParentReferenceDefinitions
+#   Returns an array of the <FileNames> which define the passed parent <ReferenceString>, or an empty array if none.
+#
+sub ParentReferenceDefinitions #(reference)
+    {
+    my ($self, $reference) = @_;
+
+    if (defined $self->[PARENT_REFERENCES] && exists $self->[PARENT_REFERENCES]->{$reference})
+        {  return keys %{$self->[PARENT_REFERENCES]->{$reference}};  }
+    else
+        {  return ( );  };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/ClassHierarchy/File.pm b/docs/tool/Modules/NaturalDocs/ClassHierarchy/File.pm
new file mode 100644
index 00000000..3c664acd
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/ClassHierarchy/File.pm
@@ -0,0 +1,157 @@
+###############################################################################
+#
+#   Class: NaturalDocs::ClassHierarchy::File
+#
+###############################################################################
+#
+#   An object that stores information about what hierarchy information is present in a file.  It does not store its <FileName>; it
+#   assumes that it will be stored in a hashref where the key is the <FileName>.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::ClassHierarchy::File;
+
+
+#
+#   Topic: Implementation
+#
+#   Since there's only one member in the class, and it's a hashref, the class is simply the hashref itself blessed as a class.
+#   The keys are the class <SymbolStrings> that are defined in the file, and the values are existence hashrefs of each class'
+#   parent <ReferenceStrings>, or undef if none.
+#
+
+
+###############################################################################
+# Group: Modification Functions
+
+
+#
+#   Function: New
+#
+#   Creates and returns a new class.
+#
+sub New
+    {
+    my ($package) = @_;
+
+    my $object = { };
+    bless $object, $package;
+
+    return $object;
+    };
+
+#
+#   Function: AddClass
+#   Adds a rew class <SymbolString> to the file.
+#
+sub AddClass #(class)
+    {
+    my ($self, $class) = @_;
+
+    if (!exists $self->{$class})
+        {  $self->{$class} = undef;  };
+    };
+
+#
+#   Function: DeleteClass
+#   Deletes a class <SymbolString> from the file.
+#
+sub DeleteClass #(class)
+    {
+    my ($self, $class) = @_;
+    delete $self->{$class};
+    };
+
+#
+#   Function: AddParentReference
+#   Adds a parent <ReferenceString> to a class <SymbolString>.
+#
+sub AddParentReference #(class, parentReference)
+    {
+    my ($self, $class, $parent) = @_;
+
+    if (!exists $self->{$class} || !defined $self->{$class})
+        {  $self->{$class} = { };  };
+
+    $self->{$class}->{$parent} = 1;
+    };
+
+#
+#   Function: DeleteParentReference
+#   Deletes a parent <ReferenceString> from a class <SymbolString>.
+#
+sub DeleteParentReference #(class, parent)
+    {
+    my ($self, $class, $parent) = @_;
+
+    if (exists $self->{$class})
+        {
+        delete $self->{$class}->{$parent};
+
+        if (!scalar keys %{$self->{$class}})
+            {  $self->{$class} = undef;  };
+        };
+    };
+
+
+
+###############################################################################
+# Group: Information Functions
+
+
+#
+#   Function: Classes
+#   Returns an array of the class <SymbolStrings> that are defined by this file, or an empty array if none.
+#
+sub Classes
+    {
+    my ($self) = @_;
+    return keys %{$self};
+    };
+
+#
+#   Function: HasClass
+#   Returns whether the file defines the passed class <SymbolString>.
+#
+sub HasClass #(class)
+    {
+    my ($self, $class) = @_;
+    return exists $self->{$class};
+    };
+
+#
+#   Function: ParentReferencesOf
+#   Returns an array of the parent <ReferenceStrings> that are defined by the class, or an empty array if none.
+#
+sub ParentReferencesOf #(class)
+    {
+    my ($self, $class) = @_;
+
+    if (!exists $self->{$class} || !defined $self->{$class})
+        {  return ( );  }
+    else
+        {  return keys %{$self->{$class}};  };
+    };
+
+#
+#   Function: HasParentReference
+#   Returns whether the file defines the passed class <SymbolString> and parent <ReferenceString>.
+#
+sub HasParentReference #(class, parent)
+    {
+    my ($self, $class, $parent) = @_;
+
+    if (!$self->HasClass($class))
+        {  return undef;  };
+
+    return exists $self->{$class}->{$parent};
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/ConfigFile.pm b/docs/tool/Modules/NaturalDocs/ConfigFile.pm
new file mode 100644
index 00000000..73bc1caa
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/ConfigFile.pm
@@ -0,0 +1,497 @@
+###############################################################################
+#
+#   Package: NaturalDocs::ConfigFile
+#
+###############################################################################
+#
+#   A package to manage Natural Docs' configuration files.
+#
+#   Usage:
+#
+#       - Only one configuration file can be managed with this package at a time.  You must close the file before opening another
+#         one.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::ConfigFile;
+
+
+
+#
+#   Topic: Format
+#
+#   All configuration files are text files.
+#
+#   > # [comment]
+#
+#   Comments start with the # character.
+#
+#   > Format: [version]
+#
+#   All configuration files *must* have a format line as its first line containing content.  Whitespace and comments are permitted
+#   ahead of it.
+#
+#   > [keyword]: [value]
+#
+#   Keywords can only contain <CFChars>.  Keywords are not case sensitive.  Values can be anything and run until the end of
+#   the line or a comment.
+#
+#   > [value]
+#
+#   Lines that don't start with a valid keyword format are considered to be all value.
+#
+#   > [line] { [line] } [line]
+#
+#   Files supporting brace groups (specified in <Open()>) may also have braces that can appear anywhere.  It allows more than
+#   one thing to appear per line, which isn't supported otherwise.  Consequently, values may not have braces.
+#
+
+
+#
+#   Type: CFChars
+#
+#   The characters that can appear in configuration file keywords and user-defined element names: letters, numbers, spaces,
+#   dashes, slashes, apostrophes, and periods.
+#
+#   Although the list above is exhaustive, it should be noted that you especially can *not* use colons (messes up keyword: value
+#   sequences) commas (messes up item, item, item list sequences) and hashes (messes up comment detection.)
+#
+#   You can search the source code for [CFChars] to find all the instances where this definition is used.
+#
+
+
+###############################################################################
+# Group: Variables
+
+#
+#   handle: CONFIG_FILEHANDLE
+#
+#   The file handle used for the configuration file.
+#
+
+
+#
+#   string: file
+#
+#   The <FileName> for the current configuration file being parsed.
+#
+my $file;
+
+
+#
+#   array: errors
+#
+#   An array of errors added by <AddError()>.  Every odd entry is the line number, and every even entry following is the
+#   error message.
+#
+my @errors;
+
+
+#
+#   var: lineNumber
+#
+#   The current line number for the configuration file.
+#
+my $lineNumber;
+
+
+#
+#   bool: hasBraceGroups
+#
+#   Whether the file has brace groups or not.
+#
+my $hasBraceGroups;
+
+
+#
+#   array: virtualLines
+#
+#   An array of virtual lines if a line from the file contained more than one.
+#
+#   Files with brace groups may have more than one virtual line per actual file line, such as "Group: A { Group: B".  When that
+#   happens, any extra virtual lines are put into here so they can be returned on the next call.
+#
+my @virtualLines;
+
+
+
+###############################################################################
+# Group: Functions
+
+
+#
+#   Function: Open
+#
+#   Opens a configuration file for parsing and returns the format <VersionInt>.
+#
+#   Parameters:
+#
+#       file - The <FileName> to parse.
+#       hasBraceGroups - Whether the file supports brace groups or not.  If so, lines with braces will be split apart behind the
+#                                  scenes.
+#
+#   Returns:
+#
+#       The <VersionInt> of the file, or undef if the file doesn't exist.
+#
+sub Open #(file, hasBraceGroups)
+    {
+    my $self;
+    ($self, $file, $hasBraceGroups) = @_;
+
+    @errors = ( );
+
+    # It will be incremented to one when the first line is read from the file.
+    $lineNumber = 0;
+
+    open(CONFIG_FILEHANDLE, '<' . $file) or return undef;
+
+
+    # Get the format line.
+
+    my ($keyword, $value, $comment) = $self->GetLine();
+
+    if ($keyword eq 'format')
+        {  return NaturalDocs::Version->FromString($value);  }
+    else
+        {  die "The first content line in " . $file . " must be the Format: line.\n";  };
+    };
+
+
+#
+#   Function: Close
+#
+#   Closes the current configuration file.
+#
+sub Close
+    {
+    my $self = shift;
+    close(CONFIG_FILEHANDLE);
+    };
+
+
+#
+#   Function: GetLine
+#
+#   Returns the next line containing content, or an empty array if none.
+#
+#   Returns:
+#
+#       Returns the array ( keyword, value, comment ), or an empty array if none.  All tabs will be converted to spaces, and all
+#       whitespace will be condensed into a single space.
+#
+#       keyword - The keyword part of the line, if any.  Is converted to lowercase and doesn't include the colon.  If the file supports
+#                       brace groups, opening and closing braces will be returned as keywords.
+#       value - The value part of the line, minus any whitespace.  Keeps its original case.
+#       comment - The comment following the line, if any.  This includes the # symbol and a leading space if there was
+#                       any whitespace, since it may be significant.  Otherwise undef.  Used for lines where the # character needs to be
+#                       accepted as part of the value.
+#
+sub GetLine
+    {
+    my $self = shift;
+
+    my ($line, $comment);
+
+
+    # Get the next line with content.
+
+    do
+        {
+        # Get the next line.
+
+        my $isFileLine;
+
+        if (scalar @virtualLines)
+            {
+            $line = shift @virtualLines;
+            $isFileLine = 0;
+            }
+        else
+            {
+            $line = <CONFIG_FILEHANDLE>;
+            $lineNumber++;
+
+            if (!defined $line)
+                {  return ( );  };
+
+            ::XChomp(\$line);
+
+            # Condense spaces and tabs into a single space.
+            $line =~ tr/\t /  /s;
+            $isFileLine = 1;
+            };
+
+
+        # Split off the comment.
+
+        if ($line =~ /^(.*?)( ?#.*)$/)
+            {  ($line, $comment) = ($1, $2);  }
+        else
+            {  $comment = undef;  };
+
+
+        # Split any brace groups.
+
+        if ($isFileLine && $hasBraceGroups && $line =~ /[\{\}]/)
+            {
+            ($line, @virtualLines) = split(/([\{\}])/, $line);
+
+            $virtualLines[-1] .= $comment;
+            $comment = undef;
+            };
+
+
+        # Remove whitespace.
+
+        $line =~ s/^ //;
+        $line =~ s/ $//;
+        $comment =~ s/ $//;
+        # We want to keep the leading space on a comment.
+        }
+    while (!$line);
+
+
+    # Process the line.
+
+    if ($hasBraceGroups && ($line eq '{' || $line eq '}'))
+        {
+        return ($line, undef, undef);
+        };
+
+
+    if ($line =~ /^([a-z0-9\ \'\/\.\-]+?) ?: ?(.*)$/i) # [CFChars]
+        {
+        my ($keyword, $value) = ($1, $2);
+        return (lc($keyword), $value, $comment);
+        }
+
+    else
+        {
+        return (undef, $line, $comment);
+        };
+    };
+
+
+#
+#   Function: LineNumber
+#
+#   Returns the line number for the line last returned by <GetLine()>.
+#
+sub LineNumber
+    {  return $lineNumber;  };
+
+
+
+###############################################################################
+# Group: Error Functions
+
+
+#
+#   Function: AddError
+#
+#   Stores an error for the current configuration file.  Will be attached to the last line read by <GetLine()>.
+#
+#   Parameters:
+#
+#       message - The error message.
+#       lineNumber - The line number to use.  If not specified, it will use the line number from the last call to <GetLine()>.
+#
+sub AddError #(message, lineNumber)
+    {
+    my ($self, $message, $messageLineNumber) = @_;
+
+    if (!defined $messageLineNumber)
+        {  $messageLineNumber = $lineNumber;  };
+
+    push @errors, $messageLineNumber, $message;
+    };
+
+
+#
+#   Function: ErrorCount
+#
+#   Returns how many errors the configuration file has.
+#
+sub ErrorCount
+    {
+    return (scalar @errors) / 2;
+    };
+
+
+#
+#   Function: PrintErrorsAndAnnotateFile
+#
+#   Prints the errors to STDERR in the standard GNU format and annotates the configuration file with them.  It does *not* end
+#   execution.  <Close()> *must* be called before this function.
+#
+sub PrintErrorsAndAnnotateFile
+    {
+    my ($self) = @_;
+
+    if (scalar @errors)
+        {
+        open(CONFIG_FILEHANDLE, '<' . $file);
+        my @lines = <CONFIG_FILEHANDLE>;
+        close(CONFIG_FILEHANDLE);
+
+        # We need to keep track of both the real and the original line numbers.  The original line numbers are for matching errors in
+        # the errors array, and don't include any comment lines added or deleted.  Line number is the current line number including
+        # those comment lines for sending to the display.
+        my $lineNumber = 1;
+        my $originalLineNumber = 1;
+
+        open(CONFIG_FILEHANDLE, '>' . $file);
+
+        # We don't want to keep the old error header, if present.
+        if ($lines[0] =~ /^\# There (?:is an error|are \d+ errors) in this file\./)
+            {
+            shift @lines;
+            $originalLineNumber++;
+
+            # We want to drop the blank line after it as well.
+            if ($lines[0] eq "\n")
+                {
+                shift @lines;
+                $originalLineNumber++;
+                };
+            };
+
+        if ($self->ErrorCount() == 1)
+            {
+            print CONFIG_FILEHANDLE
+            "# There is an error in this file.  Search for ERROR to find it.\n\n";
+            }
+        else
+            {
+            print CONFIG_FILEHANDLE
+            "# There are " . $self->ErrorCount() . " errors in this file.  Search for ERROR to find them.\n\n";
+            };
+
+        $lineNumber += 2;
+
+
+        foreach my $line (@lines)
+            {
+            while (scalar @errors && $originalLineNumber == $errors[0])
+                {
+                my $errorLine = shift @errors;
+                my $errorMessage = shift @errors;
+
+                print CONFIG_FILEHANDLE "# ERROR: " . $errorMessage . "\n";
+
+                # Use the GNU error format, which should make it easier to handle errors when Natural Docs is part of a build process.
+                # See http://www.gnu.org/prep/standards_15.html
+
+                $errorMessage = lcfirst($errorMessage);
+                $errorMessage =~ s/\.$//;
+
+                print STDERR 'NaturalDocs:' . $file . ':' . $lineNumber . ': ' . $errorMessage . "\n";
+
+                $lineNumber++;
+                };
+
+            # We want to remove error lines from previous runs.
+            if (substr($line, 0, 9) ne '# ERROR: ')
+                {
+                print CONFIG_FILEHANDLE $line;
+                $lineNumber++;
+                };
+
+            $originalLineNumber++;
+            };
+
+        # Clean up any remaining errors.
+        while (scalar @errors)
+            {
+            my $errorLine = shift @errors;
+            my $errorMessage = shift @errors;
+
+            print CONFIG_FILEHANDLE "# ERROR: " . $errorMessage . "\n";
+
+            # Use the GNU error format, which should make it easier to handle errors when Natural Docs is part of a build process.
+            # See http://www.gnu.org/prep/standards_15.html
+
+            $errorMessage = lcfirst($errorMessage);
+            $errorMessage =~ s/\.$//;
+
+            print STDERR 'NaturalDocs:' . $file . ':' . $lineNumber . ': ' . $errorMessage . "\n";
+            };
+
+        close(CONFIG_FILEHANDLE);
+        };
+    };
+
+
+
+###############################################################################
+# Group: Misc Functions
+
+
+#
+#   Function: HasOnlyCFChars
+#
+#   Returns whether the passed string contains only <CFChars>.
+#
+sub HasOnlyCFChars #(string)
+    {
+    my ($self, $string) = @_;
+    return ($string =~ /^[a-z0-9\ \.\-\/\']*$/i);  # [CFChars]
+    };
+
+
+#
+#   Function: CFCharNames
+#
+#   Returns a plain-english list of <CFChars> which can be embedded in a sentence.  For example, "You can only use
+#   [CFCharsList()] in the name.
+#
+sub CFCharNames
+    {
+    # [CFChars]
+    return 'letters, numbers, spaces, periods, dashes, slashes, and apostrophes';
+    };
+
+
+#
+#   Function: Obscure
+#
+#   Obscures the passed text so that it is not user editable and returns it.  The encoding method is not secure; it is just designed
+#   to be fast and to discourage user editing.
+#
+sub Obscure #(text)
+    {
+    my ($self, $text) = @_;
+
+    # ` is specifically chosen to encode to space because of its rarity.  We don't want a trailing one to get cut off before decoding.
+    $text =~ tr{a-zA-Z0-9\ \\\/\.\:\_\-\`}
+                    {pY9fGc\`R8lAoE\\uIdH6tN\/7sQjKx0B5mW\.vZ41PyFg\:CrLaO\_eUi2DhT\-nSqJkXb3MwVz\ };
+
+    return $text;
+    };
+
+
+#
+#   Function: Unobscure
+#
+#   Restores text encoded with <Obscure()> and returns it.
+#
+sub Unobscure #(text)
+    {
+    my ($self, $text) = @_;
+
+    $text =~ tr{pY9fGc\`R8lAoE\\uIdH6tN\/7sQjKx0B5mW\.vZ41PyFg\:CrLaO\_eUi2DhT\-nSqJkXb3MwVz\ }
+                    {a-zA-Z0-9\ \\\/\.\:\_\-\`};
+
+    return $text;
+    };
+
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Constants.pm b/docs/tool/Modules/NaturalDocs/Constants.pm
new file mode 100644
index 00000000..f9e272b3
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Constants.pm
@@ -0,0 +1,165 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Constants
+#
+###############################################################################
+#
+#   Constants that are used throughout the script.  All are exported by default.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Constants;
+
+use vars qw(@EXPORT @ISA);
+require Exporter;
+@ISA = qw(Exporter);
+
+@EXPORT = ('MENU_TITLE', 'MENU_SUBTITLE', 'MENU_FILE', 'MENU_GROUP', 'MENU_TEXT', 'MENU_LINK', 'MENU_FOOTER',
+                   'MENU_INDEX', 'MENU_FORMAT', 'MENU_ENDOFORIGINAL', 'MENU_DATA',
+
+                   'MENU_FILE_NOAUTOTITLE', 'MENU_GROUP_UPDATETITLES', 'MENU_GROUP_UPDATESTRUCTURE',
+                   'MENU_GROUP_UPDATEORDER', 'MENU_GROUP_HASENDOFORIGINAL',
+                   'MENU_GROUP_UNSORTED', 'MENU_GROUP_FILESSORTED',
+                   'MENU_GROUP_FILESANDGROUPSSORTED', 'MENU_GROUP_EVERYTHINGSORTED',
+                   'MENU_GROUP_ISINDEXGROUP',
+
+                   'FILE_NEW', 'FILE_CHANGED', 'FILE_SAME', 'FILE_DOESNTEXIST');
+
+#
+#   Topic: Assumptions
+#
+#   - No constant here will ever be zero.
+#   - All constants are exported by default.
+#
+
+
+###############################################################################
+# Group: Virtual Types
+# These are only groups of constants, but should be treated like typedefs or enums.  Each one represents a distinct type and
+# their values should only be one of their constants or undef.
+
+
+#
+#   Constants: MenuEntryType
+#
+#   The types of entries that can appear in the menu.
+#
+#       MENU_TITLE         - The title of the menu.
+#       MENU_SUBTITLE   - The sub-title of the menu.
+#       MENU_FILE           - A source file, relative to the source directory.
+#       MENU_GROUP       - A group.
+#       MENU_TEXT          - Arbitrary text.
+#       MENU_LINK           - A web link.
+#       MENU_FOOTER      - Footer text.
+#       MENU_INDEX        - An index.
+#       MENU_FORMAT     - The version of Natural Docs the menu file was generated with.
+#       MENU_ENDOFORIGINAL - A dummy entry that marks where the original group content ends.  This is used when automatically
+#                                           changing the groups so that the alphabetization or lack thereof can be detected without being
+#                                           affected by new entries tacked on to the end.
+#       MENU_DATA - Data not meant for user editing.
+#
+#   Dependency:
+#
+#       <PreviousMenuState.nd> depends on these values all being able to fit into a UInt8, i.e. <= 255.
+#
+use constant MENU_TITLE => 1;
+use constant MENU_SUBTITLE => 2;
+use constant MENU_FILE => 3;
+use constant MENU_GROUP => 4;
+use constant MENU_TEXT => 5;
+use constant MENU_LINK => 6;
+use constant MENU_FOOTER => 7;
+use constant MENU_INDEX => 8;
+use constant MENU_FORMAT => 9;
+use constant MENU_ENDOFORIGINAL => 10;
+use constant MENU_DATA => 11;
+
+
+#
+#   Constants: FileStatus
+#
+#   What happened to a file since Natural Docs' last execution.
+#
+#       FILE_NEW                - The file has been added since the last run.
+#       FILE_CHANGED        - The file has been modified since the last run.
+#       FILE_SAME               - The file hasn't been modified since the last run.
+#       FILE_DOESNTEXIST  - The file doesn't exist, or was deleted.
+#
+use constant FILE_NEW => 1;
+use constant FILE_CHANGED => 2;
+use constant FILE_SAME => 3;
+use constant FILE_DOESNTEXIST => 4;
+
+
+
+###############################################################################
+# Group: Flags
+# These constants can be combined with each other.
+
+
+#
+#   Constants: Menu Entry Flags
+#
+#   The various flags that can apply to a menu entry.  You cannot mix flags of different types, since they may overlap.
+#
+#   File Flags:
+#
+#       MENU_FILE_NOAUTOTITLE - Whether the file is auto-titled or not.
+#
+#   Group Flags:
+#
+#       MENU_GROUP_UPDATETITLES - The group should have its auto-titles regenerated.
+#       MENU_GROUP_UPDATESTRUCTURE - The group should be checked for structural changes, such as being removed or being
+#                                                             split into subgroups.
+#       MENU_GROUP_UPDATEORDER - The group should be resorted.
+#
+#       MENU_GROUP_HASENDOFORIGINAL - Whether the group contains a dummy <MENU_ENDOFORIGINAL> entry.
+#       MENU_GROUP_ISINDEXGROUP - Whether the group is used primarily for <MENU_INDEX> entries.  <MENU_TEXT> entries
+#                                                       are tolerated.
+#
+#       MENU_GROUP_UNSORTED - The group's contents are not sorted.
+#       MENU_GROUP_FILESSORTED - The group's files are sorted alphabetically.
+#       MENU_GROUP_FILESANDGROUPSSORTED - The group's files and sub-groups are sorted alphabetically.
+#       MENU_GROUP_EVERYTHINGSORTED - All entries in the group are sorted alphabetically.
+#
+use constant MENU_FILE_NOAUTOTITLE => 0x0001;
+
+use constant MENU_GROUP_UPDATETITLES => 0x0001;
+use constant MENU_GROUP_UPDATESTRUCTURE => 0x0002;
+use constant MENU_GROUP_UPDATEORDER => 0x0004;
+use constant MENU_GROUP_HASENDOFORIGINAL => 0x0008;
+
+# This could really be a two-bit field instead of four flags, but it's not worth the effort since it's only used internally.
+use constant MENU_GROUP_UNSORTED => 0x0010;
+use constant MENU_GROUP_FILESSORTED => 0x0020;
+use constant MENU_GROUP_FILESANDGROUPSSORTED => 0x0040;
+use constant MENU_GROUP_EVERYTHINGSORTED => 0x0080;
+
+use constant MENU_GROUP_ISINDEXGROUP => 0x0100;
+
+
+
+
+###############################################################################
+# Group: Support Functions
+
+
+#
+#   Function: IsClassHierarchyReference
+#   Returns whether the passed <ReferenceType> belongs to <NaturalDocs::ClassHierarchy>.
+#
+sub IsClassHierarchyReference #(reference)
+    {
+    my ($self, $reference) = @_;
+    return ($reference == ::REFERENCE_CH_CLASS() || $reference == ::REFERENCE_CH_PARENT());
+    };
+
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/DefineMembers.pm b/docs/tool/Modules/NaturalDocs/DefineMembers.pm
new file mode 100644
index 00000000..80a38dd9
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/DefineMembers.pm
@@ -0,0 +1,100 @@
+###############################################################################
+#
+#   Package: NaturalDocs::DefineMembers
+#
+###############################################################################
+#
+#   A custom Perl pragma to define member constants and accessors for use in Natural Docs objects while supporting inheritance.
+#
+#   Each member will be defined as a numeric constant which should be used as that variable's index into the object arrayref.
+#   They will be assigned sequentially from zero, and take into account any members defined this way in parent classes.  Note
+#   that you can *not* use multiple inheritance with this method.
+#
+#   If a parameter ends in parenthesis, it will be generated as an accessor for the previous member.  If it also starts with "Set",
+#   the accessor will accept a single parameter to replace the value with.  If it's followed with "duparrayref", it will assume the
+#   parameter is either an arrayref or undef, and if the former, will duplicate it to set the value.
+#
+#   Example:
+#
+#   > package MyPackage;
+#   >
+#   > use NaturalDocs::DefineMembers 'VAR_A', 'VarA()', 'SetVarA()',
+#   >                                'VAR_B', 'VarB()',
+#   >                                'VAR_C',
+#   >                                'VAR_D', 'VarD()', 'SetVarD() duparrayref';
+#   >
+#   > sub SetC #(C)
+#   >    {
+#   >    my ($self, $c) = @_;
+#   >    $self->[VAR_C] = $c;
+#   >    };
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+
+package NaturalDocs::DefineMembers;
+
+sub import #(member, member, ...)
+    {
+    my ($self, @parameters) = @_;
+    my $package = caller();
+
+    no strict 'refs';
+    my $parent = ${$package . '::ISA'}[0];
+    use strict 'refs';
+
+    my $memberConstant = 0;
+    my $lastMemberName;
+
+    if (defined $parent && $parent->can('END_OF_MEMBERS'))
+        {  $memberConstant = $parent->END_OF_MEMBERS();  };
+
+    my $code = '{ package ' . $package . ";\n";
+
+    foreach my $parameter (@parameters)
+        {
+        if ($parameter =~ /^(.+)\(\) *(duparrayref)?$/i)
+            {
+            my ($functionName, $pragma) = ($1, lc($2));
+
+            if ($functionName =~ /^Set/)
+                {
+                if ($pragma eq 'duparrayref')
+                    {
+                    $code .=
+                    'sub ' . $functionName . '
+                        {
+                        if (defined $_[1])
+                            {  $_[0]->[' . $lastMemberName . '] = [ @{$_[1]} ];  }
+                        else
+                            {  $_[0]->[' . $lastMemberName . '] = undef;  };
+                        };' . "\n";
+                    }
+                else
+                    {
+                    $code .= 'sub ' . $functionName . ' { $_[0]->[' . $lastMemberName . '] = $_[1];  };' . "\n";
+                    };
+                }
+            else
+                {
+                $code .= 'sub ' . $functionName . ' { return $_[0]->[' . $lastMemberName . '];  };' . "\n";
+                };
+            }
+        else
+            {
+            $code .= 'use constant ' . $parameter . ' => ' . $memberConstant . ";\n";
+            $memberConstant++;
+            $lastMemberName = $parameter;
+            };
+        };
+
+    $code .= 'use constant END_OF_MEMBERS => ' . $memberConstant . ";\n";
+    $code .= '};';
+
+    eval $code;
+    };
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Error.pm b/docs/tool/Modules/NaturalDocs/Error.pm
new file mode 100644
index 00000000..4a1e4a0a
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Error.pm
@@ -0,0 +1,305 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Error
+#
+###############################################################################
+#
+#   Manages all aspects of error handling in Natural Docs.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+$SIG{'__DIE__'} = \&NaturalDocs::Error::CatchDeath;
+
+
+package NaturalDocs::Error;
+
+
+###############################################################################
+# Group: Variables
+
+
+#
+#   handle: FH_CRASHREPORT
+#   The filehandle used for generating crash reports.
+#
+
+
+#
+#   var: stackTrace
+#   The stack trace generated by <CatchDeath()>.
+#
+my $stackTrace;
+
+
+#
+#   var: softDeath
+#   Whether the program exited using <SoftDeath()>.
+#
+my $softDeath;
+
+
+#
+#   var: currentAction
+#   What Natural Docs was doing when it crashed.  This stores strings generated by functions like <OnStartParsing()>.
+#
+my $currentAction;
+
+
+###############################################################################
+# Group: Functions
+
+
+#
+#   Function: SoftDeath
+#
+#   Generates a "soft" death, which means the program exits like with Perl's die(), but no crash report will be generated.
+#
+#   Parameter:
+#
+#       message - The error message to die with.
+#
+sub SoftDeath #(message)
+    {
+    my ($self, $message) = @_;
+
+    $softDeath = 1;
+    if ($message !~ /\n$/)
+        {  $message .= "\n";  };
+
+    die $message;
+    };
+
+
+#
+#   Function: OnStartParsing
+#
+#   Called whenever <NaturalDocs::Parser> starts parsing a source file.
+#
+sub OnStartParsing #(FileName file)
+    {
+    my ($self, $file) = @_;
+    $currentAction = 'Parsing ' . $file;
+    };
+
+
+#
+#   Function: OnEndParsing
+#
+#   Called whenever <NaturalDocs::Parser> is done parsing a source file.
+#
+sub OnEndParsing #(FileName file)
+    {
+    my ($self, $file) = @_;
+    $currentAction = undef;
+    };
+
+
+#
+#   Function: OnStartBuilding
+#
+#   Called whenever <NaturalDocs::Builder> starts building a source file.
+#
+sub OnStartBuilding #(FileName file)
+    {
+    my ($self, $file) = @_;
+    $currentAction = 'Building ' . $file;
+    };
+
+
+#
+#   Function: OnEndBuilding
+#
+#   Called whenever <NaturalDocs::Builder> is done building a source file.
+#
+sub OnEndBuilding #(FileName file)
+    {
+    my ($self, $file) = @_;
+    $currentAction = undef;
+    };
+
+
+#
+#   Function: HandleDeath
+#
+#   Should be called whenever Natural Docs dies out of execution.
+#
+sub HandleDeath
+    {
+    my $self = shift;
+
+    my $reason = $::EVAL_ERROR;
+    $reason =~ s/[\n\r]+$//;
+
+    my $errorMessage =
+         "\n"
+         . "Natural Docs encountered the following error and was stopped:\n"
+         . "\n"
+         . "   " . $reason . "\n"
+         . "\n"
+
+         . "You can get help at the following web site:\n"
+         . "\n"
+         . "   " . NaturalDocs::Settings->AppURL() . "\n"
+         . "\n";
+
+    if (!$softDeath)
+        {
+        my $crashReport = $self->GenerateCrashReport();
+
+        if ($crashReport)
+            {
+            $errorMessage .=
+             "If sending an error report, please include the information found in the\n"
+             . "following file:\n"
+             . "\n"
+             . "   " . $crashReport . "\n"
+             . "\n";
+            }
+        else
+            {
+            $errorMessage .=
+             "If sending an error report, please include the following information:\n"
+             . "\n"
+             . "   Natural Docs version: " . NaturalDocs::Settings->TextAppVersion() . "\n"
+             . "   Perl version: " . $self->PerlVersion() . " on " . $::OSNAME . "\n"
+             . "\n";
+             };
+        };
+
+    die $errorMessage;
+    };
+
+
+###############################################################################
+# Group: Support Functions
+
+
+#
+#   Function: PerlVersion
+#   Returns the current Perl version as a string.
+#
+sub PerlVersion
+    {
+    my $self = shift;
+
+    my $perlVersion;
+
+    if ($^V)
+        {  $perlVersion = sprintf('%vd', $^V);  }
+    if (!$perlVersion || substr($perlVersion, 0, 1) eq '%')
+        {  $perlVersion = $];  };
+
+    return $perlVersion;
+    };
+
+
+#
+#   Function: GenerateCrashReport
+#
+#   Generates a report and returns the <FileName> it's located at.  Returns undef if it could not generate one.
+#
+sub GenerateCrashReport
+    {
+    my $self = shift;
+
+    my $errorMessage = $::EVAL_ERROR;
+    $errorMessage =~ s/[\r\n]+$//;
+
+    my $reportDirectory = NaturalDocs::Settings->ProjectDirectory();
+
+    if (!$reportDirectory || !-d $reportDirectory)
+        {  return undef;  };
+
+    my $file = NaturalDocs::File->JoinPaths($reportDirectory, 'LastCrash.txt');
+
+    open(FH_CRASHREPORT, '>' . $file) or return undef;
+
+    print FH_CRASHREPORT
+    'Crash Message:' . "\n\n"
+    . '   ' . $errorMessage . "\n\n";
+
+    if ($currentAction)
+        {
+        print FH_CRASHREPORT
+        'Current Action:' . "\n\n"
+        . '   ' . $currentAction . "\n\n";
+        };
+
+    print FH_CRASHREPORT
+    'Natural Docs version ' . NaturalDocs::Settings->TextAppVersion() . "\n"
+    . 'Perl version ' . $self->PerlVersion . ' on ' . $::OSNAME . "\n\n"
+    . 'Command Line:' . "\n\n"
+    . '   ' . join(' ', @ARGV) . "\n\n";
+
+    if ($stackTrace)
+        {
+        print FH_CRASHREPORT
+        'Stack Trace:' . "\n\n"
+        . $stackTrace;
+        }
+    else
+        {
+        print FH_CRASHREPORT
+        'Stack Trace not available.' . "\n\n";
+        };
+
+    close(FH_CRASHREPORT);
+    return $file;
+    };
+
+
+###############################################################################
+# Group: Signal Handlers
+
+
+#
+#   Function: CatchDeath
+#
+#   Catches Perl die calls.
+#
+#   *IMPORTANT:* This function is a signal handler and should not be called manually.  Also, because of this, it does not have
+#   a $self parameter.
+#
+#   Parameters:
+#
+#       message - The error message to die with.
+#
+sub CatchDeath #(message)
+    {
+    # No $self because it's a signal handler.
+    my $message = shift;
+
+    if (!$NaturalDocs::Error::softDeath)
+        {
+        my $i = 0;
+        my ($lastPackage, $lastFile, $lastLine, $lastFunction);
+
+        while (my ($package, $file, $line, $function) = caller($i))
+            {
+            if ($i != 0)
+                {  $stackTrace .= ', called from' . "\n";  };
+
+            $stackTrace .= '   ' . $function;
+
+            if (defined $lastLine)
+                {
+                $stackTrace .= ', line ' . $lastLine;
+
+                if ($function !~ /^NaturalDocs::/)
+                    {  $stackTrace .= ' of ' . $lastFile;  };
+                };
+
+            ($lastPackage, $lastFile, $lastLine, $lastFunction) = ($package, $file, $line, $function);
+            $i++;
+            };
+        };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/File.pm b/docs/tool/Modules/NaturalDocs/File.pm
new file mode 100644
index 00000000..754d708b
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/File.pm
@@ -0,0 +1,540 @@
+###############################################################################
+#
+#   Package: NaturalDocs::File
+#
+###############################################################################
+#
+#   A package to manage file access across platforms.  Incorporates functions from various standard File:: packages, but more
+#   importantly, works around the glorious suckage present in File::Spec, at least in version 0.82 and earlier.  Read the "Why oh
+#   why?" sections for why this package was necessary.
+#
+#   Usage and Dependencies:
+#
+#       - The package doesn't depend on any other Natural Docs packages and is ready to use immediately.
+#
+#       - All functions except <CanonizePath()> assume that all parameters are canonized.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use File::Spec ();
+use File::Path ();
+use File::Copy ();
+
+use strict;
+use integer;
+
+package NaturalDocs::File;
+
+
+#
+#   Function: CheckCompatibility
+#
+#   Checks if the standard packages required by this one are up to snuff and dies if they aren't.  This is done because I can't
+#   tell which versions of File::Spec have splitpath just by the version numbers.
+#
+sub CheckCompatibility
+    {
+    my ($self) = @_;
+
+    eval {
+        File::Spec->splitpath('');
+    };
+
+    if ($@)
+        {
+        NaturalDocs::Error->SoftDeath("Natural Docs requires a newer version of File::Spec than you have.  "
+                                                    . "You must either upgrade it or upgrade Perl.");
+        };
+    };
+
+
+###############################################################################
+# Group: Path String Functions
+
+
+#
+#   Function: CanonizePath
+#
+#   Takes a path and returns a logically simplified version of it.
+#
+#   Why oh why?:
+#
+#       Because File::Spec->canonpath doesn't strip quotes on Windows.  So if you pass in "a b\c" or "a b"\c, they still end up as
+#       different strings even though they're logically the same.
+#
+#       It also doesn't remove things like "..", so "a/b/../c" doesn't simplify to "a/c" like it should.
+#
+sub CanonizePath #(path)
+    {
+    my ($self, $path) = @_;
+
+    if ($::OSNAME eq 'MSWin32')
+        {
+        # We don't have to use a smarter algorithm for dropping quotes because they're invalid characters for actual file and
+        # directory names.
+        $path =~ s/\"//g;
+        };
+
+    $path = File::Spec->canonpath($path);
+
+    # Condense a/b/../c into a/c.
+
+    my $upDir = File::Spec->updir();
+    if (index($path, $upDir) != -1)
+        {
+        my ($volume, $directoryString, $file) = $self->SplitPath($path);
+        my @directories = $self->SplitDirectories($directoryString);
+
+        my $i = 1;
+        while ($i < scalar @directories)
+            {
+            if ($i > 0 && $directories[$i] eq $upDir && $directories[$i - 1] ne $upDir)
+                {
+                splice(@directories, $i - 1, 2);
+                $i--;
+                }
+            else
+                {  $i++;  };
+            };
+
+        $directoryString = $self->JoinDirectories(@directories);
+        $path = $self->JoinPath($volume, $directoryString, $file);
+        };
+
+    return $path;
+    };
+
+
+#
+#   Function: PathIsAbsolute
+#
+#   Returns whether the passed path is absolute.
+#
+sub PathIsAbsolute #(path)
+    {
+    my ($self, $path) = @_;
+    return File::Spec->file_name_is_absolute($path);
+    };
+
+
+#
+#   Function: JoinPath
+#
+#   Creates a path from its elements.
+#
+#   Parameters:
+#
+#       volume - The volume, such as the drive letter on Windows.  Undef if none.
+#       dirString - The directory string.  Create with <JoinDirectories()> if necessary.
+#       file - The file name, or undef if none.
+#
+#   Returns:
+#
+#       The joined path.
+#
+sub JoinPath #(volume, dirString, $file)
+    {
+    my ($self, $volume, $dirString, $file) = @_;
+    return File::Spec->catpath($volume, $dirString, $file);
+    };
+
+
+#
+#   Function: JoinPaths
+#
+#   Joins two paths.
+#
+#   Parameters:
+#
+#       basePath       - May be a relative path, an absolute path, or undef.
+#       extraPath      - May be a relative path, a file, a relative path and file together, or undef.
+#       noFileInExtra - Set this to true if extraPath is a relative path only, and doesn't have a file.
+#
+#   Returns:
+#
+#       The joined path.
+#
+#   Why oh why?:
+#
+#       Because nothing in File::Spec will simply slap two paths together.  They have to be split up for catpath/file, and rel2abs
+#       requires the base to be absolute.
+#
+sub JoinPaths #(basePath, extraPath, noFileInExtra)
+    {
+    my ($self, $basePath, $extraPath, $noFileInExtra) = @_;
+
+    # If both are undef, it will return undef, which is what we want.
+    if (!defined $basePath)
+        {  return $extraPath;  }
+    elsif (!defined $extraPath)
+        {  return $basePath;  };
+
+    my ($baseVolume, $baseDirString, $baseFile) = File::Spec->splitpath($basePath, 1);
+    my ($extraVolume, $extraDirString, $extraFile) = File::Spec->splitpath($extraPath, $noFileInExtra);
+
+    my @baseDirectories = $self->SplitDirectories($baseDirString);
+    my @extraDirectories = $self->SplitDirectories($extraDirString);
+
+    my $fullDirString = $self->JoinDirectories(@baseDirectories, @extraDirectories);
+
+    my $fullPath = File::Spec->catpath($baseVolume, $fullDirString, $extraFile);
+
+    return $self->CanonizePath($fullPath);
+    };
+
+
+#
+#   Function: SplitPath
+#
+#   Takes a path and returns its elements.
+#
+#   Parameters:
+#
+#       path - The path to split.
+#       noFile - Set to true if the path doesn't have a file at the end.
+#
+#   Returns:
+#
+#       The array ( volume, directoryString, file ).  If any don't apply, they will be undef.  Use <SplitDirectories()> to split the
+#       directory string if desired.
+#
+#   Why oh Why?:
+#
+#       Because File::Spec->splitpath may leave a trailing slash/backslash/whatever on the directory string, which makes
+#       it a bit hard to match it with results from File::Spec->catdir.
+#
+sub SplitPath #(path, noFile)
+    {
+    my ($self, $path, $noFile) = @_;
+
+    my @segments = File::Spec->splitpath($path, $noFile);
+
+    if (!length $segments[0])
+        {  $segments[0] = undef;  };
+    if (!length $segments[2])
+        {  $segments[2] = undef;  };
+
+    $segments[1] = File::Spec->catdir( File::Spec->splitdir($segments[1]) );
+
+    return @segments;
+    };
+
+
+#
+#   Function: JoinDirectories
+#
+#   Creates a directory string from an array of directory names.
+#
+#   Parameters:
+#
+#       directory - A directory name.  There may be as many of these as desired.
+#
+sub JoinDirectories #(directory, directory, ...)
+    {
+    my ($self, @directories) = @_;
+    return File::Spec->catdir(@directories);
+    };
+
+
+#
+#   Function: SplitDirectories
+#
+#   Takes a string of directories and returns an array of its elements.
+#
+#   Why oh why?:
+#
+#       Because File::Spec->splitdir might leave an empty element at the end of the array, which screws up both joining in
+#       <ConvertToURL> and navigation in <MakeRelativePath>.
+#
+sub SplitDirectories #(directoryString)
+    {
+    my ($self, $directoryString) = @_;
+
+    my @directories = File::Spec->splitdir($directoryString);
+
+    if (!length $directories[-1])
+        {  pop @directories;  };
+
+    return @directories;
+    };
+
+
+#
+#   Function: MakeRelativePath
+#
+#   Takes two paths and returns a relative path between them.
+#
+#   Parameters:
+#
+#       basePath    - The starting path.  May be relative or absolute, so long as the target path is as well.
+#       targetPath  - The target path.  May be relative or absolute, so long as the base path is as well.
+#
+#       If both paths are relative, they are assumed to be relative to the same base.
+#
+#   Returns:
+#
+#       The target path relative to base.
+#
+#   Why oh why?:
+#
+#       First, there's nothing that gives a relative path between two relative paths.
+#
+#       Second, if target and base are absolute but on different volumes, File::Spec->abs2rel creates a totally non-functional
+#       relative path.  It should return the target as is, since there is no relative path.
+#
+#       Third, File::Spec->abs2rel between absolute paths on the same volume, at least on Windows, leaves the drive letter
+#       on.  So abs2rel('a:\b\c\d', 'a:\b') returns 'a:c\d' instead of the expected 'c\d'.  That makes no sense whatsoever.  It's
+#       not like it was designed to handle only directory names, either; the documentation says 'path' and the code seems to
+#       explicitly handle it.  There's just an 'unless' in there that tacks on the volume, defeating the purpose of a *relative* path
+#       and making the function worthless.
+#
+sub MakeRelativePath #(basePath, targetPath)
+    {
+    my ($self, $basePath, $targetPath) = @_;
+
+    my ($baseVolume, $baseDirString, $baseFile) = $self->SplitPath($basePath, 1);
+    my ($targetVolume, $targetDirString, $targetFile) = $self->SplitPath($targetPath);
+
+    # If the volumes are different, there is no possible relative path.
+    if ($targetVolume ne $baseVolume)
+        {  return $targetPath;  };
+
+    my @baseDirectories = $self->SplitDirectories($baseDirString);
+    my @targetDirectories = $self->SplitDirectories($targetDirString);
+
+    # Skip the parts of the path that are the same.
+    while (scalar @baseDirectories && @targetDirectories && $baseDirectories[0] eq $targetDirectories[0])
+        {
+        shift @baseDirectories;
+        shift @targetDirectories;
+        };
+
+    # Back out of the base path until it reaches where they were similar.
+    for (my $i = 0; $i < scalar @baseDirectories; $i++)
+        {
+        unshift @targetDirectories, File::Spec->updir();
+        };
+
+    $targetDirString = $self->JoinDirectories(@targetDirectories);
+
+    return File::Spec->catpath(undef, $targetDirString, $targetFile);
+    };
+
+
+#
+#   Function: IsSubPathOf
+#
+#   Returns whether the path is a descendant of another path.
+#
+#   Parameters:
+#
+#       base - The base path to test against.
+#       path - The possible subpath to test.
+#
+#   Returns:
+#
+#       Whether path is a descendant of base.
+#
+sub IsSubPathOf #(base, path)
+    {
+    my ($self, $base, $path) = @_;
+
+    # This is a quick test that should find a false quickly.
+    if ($base eq substr($path, 0, length($base)))
+        {
+        # This doesn't guarantee true, because it could be "C:\A B" and "C:\A B C\File".  So we test for it by seeing if the last
+        # directory in base is the same as the equivalent directory in path.
+
+        my ($baseVolume, $baseDirString, $baseFile) = NaturalDocs::File->SplitPath($base, 1);
+        my @baseDirectories = NaturalDocs::File->SplitDirectories($baseDirString);
+
+        my ($pathVolume, $pathDirString, $pathFile) = NaturalDocs::File->SplitPath($path);
+        my @pathDirectories = NaturalDocs::File->SplitDirectories($pathDirString);
+
+        return ( $baseDirectories[-1] eq $pathDirectories[ scalar @baseDirectories - 1 ] );
+        }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: ConvertToURL
+#
+#   Takes a relative path and converts it from the native format to a relative URL.  Note that it _doesn't_ convert special characters
+#   to amp chars.
+#
+sub ConvertToURL #(path)
+    {
+    my ($self, $path) = @_;
+
+    my ($pathVolume, $pathDirString, $pathFile) = $self->SplitPath($path);
+    my @pathDirectories = $self->SplitDirectories($pathDirString);
+
+    my $i = 0;
+    while ($i < scalar @pathDirectories && $pathDirectories[$i] eq File::Spec->updir())
+        {
+        $pathDirectories[$i] = '..';
+        $i++;
+        };
+
+    return join('/', @pathDirectories, $pathFile);
+    };
+
+
+#
+#   Function: NoUpwards
+#
+#   Takes an array of directory entries and returns one without all the entries that refer to the parent directory, such as '.' and '..'.
+#
+sub NoUpwards #(array)
+    {
+    my ($self, @array) = @_;
+    return File::Spec->no_upwards(@array);
+    };
+
+
+#
+#   Function: NoFileName
+#
+#   Takes a path and returns a version without the file name.  Useful for sending paths to <CreatePath()>.
+#
+sub NoFileName #(path)
+    {
+    my ($self, $path) = @_;
+
+    my ($pathVolume, $pathDirString, $pathFile) = File::Spec->splitpath($path);
+
+    return File::Spec->catpath($pathVolume, $pathDirString, undef);
+    };
+
+
+#
+#   Function: NoExtension
+#
+#   Returns the path without an extension.
+#
+sub NoExtension #(path)
+    {
+    my ($self, $path) = @_;
+
+    my $extension = $self->ExtensionOf($path);
+
+    if ($extension)
+        {  $path = substr($path, 0, length($path) - length($extension) - 1);  };
+
+    return $path;
+    };
+
+
+#
+#   Function: ExtensionOf
+#
+#   Returns the extension of the passed path, or undef if none.
+#
+sub ExtensionOf #(path)
+    {
+    my ($self, $path) = @_;
+
+    my ($pathVolume, $pathDirString, $pathFile) = File::Spec->splitpath($path);
+
+    # We need the leading dot in the regex so files that start with a dot but don't have an extension count as extensionless files.
+    if ($pathFile =~ /.\.([^\.]+)$/)
+        {  return $1;  }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: IsCaseSensitive
+#
+#   Returns whether the current platform has case-sensitive paths.
+#
+sub IsCaseSensitive
+    {
+    return !(File::Spec->case_tolerant());
+    };
+
+
+
+###############################################################################
+# Group: Disk Functions
+
+
+#
+#   Function: CreatePath
+#
+#   Creates a directory tree corresponding to the passed path, regardless of how many directories do or do not already exist.
+#   Do _not_ include a file name in the path.  Use <NoFileName()> first if you need to.
+#
+sub CreatePath #(path)
+    {
+    my ($self, $path) = @_;
+    File::Path::mkpath($path);
+    };
+
+
+#
+#   Function: RemoveEmptyTree
+#
+#   Removes an empty directory tree.  The passed directory will be removed if it's empty, and it will keep removing its parents
+#   until it reaches one that's not empty or a set limit.
+#
+#   Parameters:
+#
+#       path - The path to start from.  It will try to remove this directory and work it's way down.
+#       limit - The path to stop at if it doesn't find any non-empty directories first.  This path will *not* be removed.
+#
+sub RemoveEmptyTree #(path, limit)
+    {
+    my ($self, $path, $limit) = @_;
+
+    my ($volume, $directoryString) = $self->SplitPath($path, 1);
+    my @directories = $self->SplitDirectories($directoryString);
+
+    my $directory = $path;
+
+    while (-d $directory && $directory ne $limit)
+        {
+        opendir FH_ND_FILE, $directory;
+        my @entries = readdir FH_ND_FILE;
+        closedir FH_ND_FILE;
+
+        @entries = $self->NoUpwards(@entries);
+
+        if (scalar @entries || !rmdir($directory))
+            {  last;  };
+
+        pop @directories;
+        $directoryString = $self->JoinDirectories(@directories);
+        $directory = $self->JoinPath($volume, $directoryString);
+        };
+    };
+
+
+#
+#   Function: Copy
+#
+#   Copies a file from one path to another.  If the destination file exists, it is overwritten.
+#
+#   Parameters:
+#
+#       source       - The file to copy.
+#       destination - The destination to copy to.
+#
+#   Returns:
+#
+#       Whether it succeeded
+#
+sub Copy #(source, destination) => bool
+    {
+    my ($self, $source, $destination) = @_;
+    return File::Copy::copy($source, $destination);
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/ImageReferenceTable.pm b/docs/tool/Modules/NaturalDocs/ImageReferenceTable.pm
new file mode 100644
index 00000000..8f13ce5f
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/ImageReferenceTable.pm
@@ -0,0 +1,383 @@
+###############################################################################
+#
+#   Package: NaturalDocs::ImageReferenceTable
+#
+###############################################################################
+#
+#   A <NaturalDocs::SourceDB>-based package that manages all the image references appearing in source files.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+use NaturalDocs::ImageReferenceTable::String;
+use NaturalDocs::ImageReferenceTable::Reference;
+
+
+package NaturalDocs::ImageReferenceTable;
+
+use base 'NaturalDocs::SourceDB::Extension';
+
+
+###############################################################################
+# Group: Information
+
+#
+#   Topic: Usage
+#
+#       - <NaturalDocs::Project> and <NaturalDocs::SourceDB> must be initialized before this package can be used.
+#
+#       - Call <Register()> before using.
+#
+#
+#   Topic: Programming Notes
+#
+#       When working on this code, remember that there are three things it has to juggle.
+#
+#       - The information in <NaturalDocs::SourceDB>.
+#       - Image file references in <NaturalDocs::Project>.
+#       - Source file rebuilding on changes.
+#
+#       Managing the actual image files will be handled between <NaturalDocs::Project> and the <NaturalDocs::Builder>
+#       sub-packages.
+#
+#
+#   Topic: Implementation
+#
+#       Managing image references is simpler than managing the references in <NaturalDocs::SymbolTable>.  In SymbolTable,
+#       you have to worry about reference targets popping into and out of existence.  A link may go to a file that hasn't been
+#       reparsed yet and the target may no longer exist.  We have to deal with that when we know it, which may be after the
+#       reference's file was parsed.  Also, a new definition may appear that serves as a better interpretation of a link than its
+#       current target, and again we may only know that after the reference's file has been parsed already.  So we have to deal
+#       with scores and potential symbols and each symbol knowing exactly what links to it and so forth.
+#
+#       Not so with image references.  All possible targets (all possible image files) are known by <NaturalDocs::Project> early
+#       on and will remain consistent throughout execution.  So because of that, we can get away with only storing reference
+#       counts with each image and determining exactly where a reference points to as we find them.
+#
+#       Reference counts are stored with the image file information in <NaturalDocs::Project>.  However, it is not loaded and
+#       saved to disk by it.  Rather, it is regenerated by this package when it loads <ImageReferenceTable.nd>.
+#       NaturalDocs::Project only stores the last modification time (so it can add files to the build list if they've changed) and
+#       whether it had any references at all on the last run (so it knows whether it should care if they've changed.)
+#       ImageReferenceTable.nd stores each reference's target, width, and height.  Whether their interpretations have changed is
+#       dealt with in the <Load()> function, again since the list of targets (image files) is constant.
+#
+#       The package is based on <NaturalDocs::SourceDB>, so read it's documentation for more information on how it works.
+#
+
+
+###############################################################################
+# Group: Variables
+
+
+#
+#   var: extensionID
+#   The <ExtensionID> granted by <NaturalDocs::SourceDB>.
+#
+my $extensionID;
+
+
+
+###############################################################################
+# Group: Files
+
+
+#
+#   File: ImageReferenceTable.nd
+#
+#   The data file which stores all the image references from the last run of Natural Docs.
+#
+#   Format:
+#
+#       > [Standard Binary Header]
+#
+#       It starts with the standard binary header from <NaturalDocs::BinaryFile>.
+#
+#       > [Image Reference String or undef]
+#       > [AString16: target file]
+#       > [UInt16: target width or 0]
+#       > [UInt16: target height or 0]
+#
+#       For each <ImageReferenceString>, it's target, width, and height are stored.  The target is needed so we can tell if it
+#       changed from the last run, and the dimensions are needed because if the target hasn't changed but the file's dimensions
+#       have, the source files need to be rebuilt.
+#
+#       <ImageReferenceStrings> are encoded by <NaturalDocs::ImageReferenceTable::String>.
+#
+#       > [AString16: definition file or undef] ...
+#
+#       Then comes a series of AString16s for all the files that define the reference until it hits an undef.
+#
+#       This whole series is repeated for each <ImageReferenceString> until it hits an undef.
+#
+#	Revisions:
+#
+#		1.4:
+#
+#			- The file was added to Natural Docs.
+#
+
+
+
+###############################################################################
+# Group: Functions
+
+
+#
+#   Function: Register
+#   Registers the package with <NaturalDocs::SourceDB>.
+#
+sub Register
+    {
+    my $self = shift;
+    $extensionID = NaturalDocs::SourceDB->RegisterExtension($self, 0);
+    };
+
+
+#
+#   Function: Load
+#
+#   Loads the data from <ImageReferenceTable.nd>.  Returns whether it was successful.
+#
+sub Load # => bool
+    {
+    my $self = shift;
+
+    if (NaturalDocs::Settings->RebuildData())
+        {  return 0;  };
+
+    # The file format hasn't changed since it was introduced.
+    if (!NaturalDocs::BinaryFile->OpenForReading( NaturalDocs::Project->DataFile('ImageReferenceTable.nd') ))
+        {  return 0;  };
+
+
+    # [Image Reference String or undef]
+    while (my $referenceString = NaturalDocs::ImageReferenceTable::String->FromBinaryFile())
+        {
+        NaturalDocs::SourceDB->AddItem($extensionID, $referenceString,
+                                                           NaturalDocs::ImageReferenceTable::Reference->New());
+
+        # [AString16: target file]
+        # [UInt16: target width or 0]
+        # [UInt16: target height or 0]
+
+        my $targetFile = NaturalDocs::BinaryFile->GetAString16();
+        my $width = NaturalDocs::BinaryFile->GetUInt16();
+        my $height = NaturalDocs::BinaryFile->GetUInt16();
+
+        my $newTargetFile = $self->SetReferenceTarget($referenceString);
+        my $newWidth;
+        my $newHeight;
+
+        if ($newTargetFile)
+            {
+            NaturalDocs::Project->AddImageFileReference($newTargetFile);
+            ($newWidth, $newHeight) = NaturalDocs::Project->ImageFileDimensions($newTargetFile);
+            };
+
+        my $rebuildDefinitions = ($newTargetFile ne $targetFile || $newWidth != $width || $newHeight != $height);
+
+
+        # [AString16: definition file or undef] ...
+        while (my $definitionFile = NaturalDocs::BinaryFile->GetAString16())
+            {
+            NaturalDocs::SourceDB->AddDefinition($extensionID, $referenceString, $definitionFile);
+
+            if ($rebuildDefinitions)
+                {  NaturalDocs::Project->RebuildFile($definitionFile);  };
+            };
+        };
+
+
+    NaturalDocs::BinaryFile->Close();
+    return 1;
+    };
+
+
+#
+#   Function: Save
+#
+#   Saves the data to <ImageReferenceTable.nd>.
+#
+sub Save
+    {
+    my $self = shift;
+
+    my $references = NaturalDocs::SourceDB->GetAllItemsHashRef($extensionID);
+
+    NaturalDocs::BinaryFile->OpenForWriting( NaturalDocs::Project->DataFile('ImageReferenceTable.nd') );
+
+    while (my ($referenceString, $referenceObject) = each %$references)
+        {
+        # [Image Reference String or undef]
+        # [AString16: target file]
+        # [UInt16: target width or 0]
+        # [UInt16: target height or 0]
+
+        NaturalDocs::ImageReferenceTable::String->ToBinaryFile($referenceString);
+
+        my $target = $referenceObject->Target();
+        my ($width, $height);
+
+        if ($target)
+            {  ($width, $height) = NaturalDocs::Project->ImageFileDimensions($target);  };
+
+        NaturalDocs::BinaryFile->WriteAString16( $referenceObject->Target() );
+        NaturalDocs::BinaryFile->WriteUInt16( ($width || 0) );
+        NaturalDocs::BinaryFile->WriteUInt16( ($height || 0) );
+
+        # [AString16: definition file or undef] ...
+
+        my $definitions = $referenceObject->GetAllDefinitionsHashRef();
+
+        foreach my $definition (keys %$definitions)
+            {  NaturalDocs::BinaryFile->WriteAString16($definition);  };
+
+        NaturalDocs::BinaryFile->WriteAString16(undef);
+        };
+
+    NaturalDocs::ImageReferenceTable::String->ToBinaryFile(undef);
+
+    NaturalDocs::BinaryFile->Close();
+    };
+
+
+#
+#   Function: AddReference
+#
+#   Adds a new image reference.
+#
+sub AddReference #(FileName file, string referenceText)
+    {
+    my ($self, $file, $referenceText) = @_;
+
+    my $referenceString = NaturalDocs::ImageReferenceTable::String->Make($file, $referenceText);
+
+    if (!NaturalDocs::SourceDB->HasItem($extensionID, $referenceString))
+        {
+        my $referenceObject = NaturalDocs::ImageReferenceTable::Reference->New();
+        NaturalDocs::SourceDB->AddItem($extensionID, $referenceString, $referenceObject);
+
+        my $target = $self->SetReferenceTarget($referenceString);
+        if ($target)
+            {  NaturalDocs::Project->AddImageFileReference($target);  };
+        };
+
+    NaturalDocs::SourceDB->AddDefinition($extensionID, $referenceString, $file);
+    };
+
+
+#
+#   Function: OnDeletedDefinition
+#
+#   Called for each definition deleted by <NaturalDocs::SourceDB>.  This is called *after* the definition has been deleted from
+#   the database, so don't expect to be able to read it.
+#
+sub OnDeletedDefinition #(ImageReferenceString referenceString, FileName file, bool wasLastDefinition)
+    {
+    my ($self, $referenceString, $file, $wasLastDefinition) = @_;
+
+    if ($wasLastDefinition)
+        {
+        my $referenceObject = NaturalDocs::SourceDB->GetItem($extensionID, $referenceString);
+        my $target = $referenceObject->Target();
+
+        if ($target)
+            {  NaturalDocs::Project->DeleteImageFileReference($target);  };
+
+        NaturalDocs::SourceDB->DeleteItem($extensionID, $referenceString);
+        };
+    };
+
+
+#
+#   Function: GetReferenceTarget
+#
+#   Returns the image file the reference resolves to, or undef if none.
+#
+#   Parameters:
+#
+#       sourceFile - The source <FileName> the reference appears in.
+#       text - The reference text.
+#
+sub GetReferenceTarget #(FileName sourceFile, string text) => FileName
+    {
+    my ($self, $sourceFile, $text) = @_;
+
+    my $referenceString = NaturalDocs::ImageReferenceTable::String->Make($sourceFile, $text);
+    my $reference = NaturalDocs::SourceDB->GetItem($extensionID, $referenceString);
+
+    if (!defined $reference)
+        {  return undef;  }
+    else
+        {  return $reference->Target();  };
+    };
+
+
+#
+#   Function: SetReferenceTarget
+#
+#   Determines the best target for the passed <ImageReferenceString> and sets it on the
+#   <NaturalDocs::ImageReferenceTable::Reference> object.  Returns the new target <FileName>.  Does *not* add any source
+#   files to the bulid list.
+#
+sub SetReferenceTarget #(ImageReferenceString referenceString) => FileName
+    {
+    my ($self, $referenceString) = @_;
+
+    my $referenceObject = NaturalDocs::SourceDB->GetItem($extensionID, $referenceString);
+    my ($sourcePath, $text) = NaturalDocs::ImageReferenceTable::String->InformationOf($referenceString);
+
+
+    # Try the path relative to the source file first.
+
+    my $target;
+
+    my $imageFile = NaturalDocs::File->JoinPaths($sourcePath, $text);
+    my $exists = NaturalDocs::Project->ImageFileExists($imageFile);
+
+
+    # Then try relative image directories.
+
+    if (!$exists)
+        {
+        my $relativeImageDirectories = NaturalDocs::Settings->RelativeImageDirectories();
+
+        for (my $i = 0; $i < scalar @$relativeImageDirectories && !$exists; $i++)
+            {
+            $imageFile = NaturalDocs::File->JoinPaths($sourcePath, $relativeImageDirectories->[$i], 1);
+            $imageFile = NaturalDocs::File->JoinPaths($imageFile, $text);
+
+            $exists = NaturalDocs::Project->ImageFileExists($imageFile);
+            };
+        };
+
+
+    # Then try absolute image directories.
+
+    if (!$exists)
+        {
+        my $imageDirectories = NaturalDocs::Settings->ImageDirectories();
+
+        for (my $i = 0; $i < scalar @$imageDirectories && !$exists; $i++)
+            {
+            $imageFile = NaturalDocs::File->JoinPaths($imageDirectories->[$i], $text);
+            $exists = NaturalDocs::Project->ImageFileExists($imageFile);
+            };
+        };
+
+
+    if ($exists)
+        {  $target = NaturalDocs::Project->ImageFileCapitalization($imageFile);  };
+    #else leave it as undef.
+
+    $referenceObject->SetTarget($target);
+    return $target;
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/ImageReferenceTable/Reference.pm b/docs/tool/Modules/NaturalDocs/ImageReferenceTable/Reference.pm
new file mode 100644
index 00000000..10d0a96b
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/ImageReferenceTable/Reference.pm
@@ -0,0 +1,44 @@
+###############################################################################
+#
+#   Package: NaturalDocs::ImageReferenceTable::Reference
+#
+###############################################################################
+#
+#   A class for references being tracked in <NaturalDocs::SourceDB>.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+
+package NaturalDocs::ImageReferenceTable::Reference;
+
+use base 'NaturalDocs::SourceDB::Item';
+
+
+use NaturalDocs::DefineMembers 'TARGET', 'Target()', 'SetTarget()',
+                                                 'NEEDS_REBUILD', 'NeedsRebuild()', 'SetNeedsRebuild()';
+
+
+#
+#   Variables: Members
+#
+#   The following constants are indexes into the object array.
+#
+#   TARGET - The image <FileName> this reference resolves to, or undef if none.
+#
+
+
+#
+#   Functions: Member Functions
+#
+#   Target - Returns the image <FileName> this reference resolves to, or undef if none.
+#   SetTarget - Replaces the image <FileName> this reference resolves to, or undef if none.
+#
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/ImageReferenceTable/String.pm b/docs/tool/Modules/NaturalDocs/ImageReferenceTable/String.pm
new file mode 100644
index 00000000..982e9f13
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/ImageReferenceTable/String.pm
@@ -0,0 +1,110 @@
+###############################################################################
+#
+#   Package: NaturalDocs::ImageReferenceTable::String
+#
+###############################################################################
+#
+#   A package for creating and managing <ImageReferenceStrings>.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+
+package NaturalDocs::ImageReferenceTable::String;
+
+
+#
+#   Type: ImageReferenceString
+#
+#   A string representing a unique image reference.  It's composed of the reference text and the directory of the source file.
+#   The source file name itself isn't included because two files in the same directory with the same reference text will always go
+#   to the same targets.
+#
+
+
+#
+#   Function: Make
+#
+#   Converts a source <FileName> and the reference text to an <ImageReferenceString>.
+#
+sub Make #(FileName sourceFile, string text) => ImageReferenceString
+    {
+    my ($self, $sourceFile, $text) = @_;
+
+    my $path = NaturalDocs::File->NoFileName($sourceFile);
+
+    # Condense whitespace and remove any separator characters.
+    $path =~ tr/ \t\r\n\x1C/ /s;
+    $text =~ tr/ \t\r\n\x1C/ /s;
+
+    return $path . "\x1C" . $text;
+    };
+
+
+#
+#   Function: InformationOf
+#
+#   Returns the information contained in the <ImageReferenceString> as the array ( path, text ).
+#
+sub InformationOf #(ImageReferenceString referenceString)
+    {
+    my ($self, $referenceString) = @_;
+    return split(/\x1C/, $referenceString);
+    };
+
+
+#
+#   Function: ToBinaryFile
+#
+#   Writes an <ImageReferenceString> to <NaturalDocs::BinaryFile>.  Can also encode an undef.
+#
+#   Format:
+#
+#       > [AString16: path] [AString16: reference text] ...
+#
+#       Undef is represented by the first AString16 being undef.
+#
+sub ToBinaryFile #(ImageReferenceString referenceString)
+    {
+    my ($self, $referenceString) = @_;
+
+    if (defined $referenceString)
+        {
+        my ($path, $text) = split(/\x1C/, $referenceString);
+
+        NaturalDocs::BinaryFile->WriteAString16($path);
+        NaturalDocs::BinaryFile->WriteAString16($text);
+        }
+    else
+        {
+        NaturalDocs::BinaryFile->WriteAString16(undef);
+        };
+    };
+
+
+#
+#   Function: FromBinaryFile
+#
+#   Loads an <ImageReferenceString> or undef from <NaturalDocs::BinaryFile> and returns it.
+#
+sub FromBinaryFile
+    {
+    my $self = shift;
+
+    my $path = NaturalDocs::BinaryFile->GetAString16();
+
+    if (!defined $path)
+        {  return undef;  };
+
+    my $text = NaturalDocs::BinaryFile->GetAString16();
+
+    return $path . "\x1C" . $text;
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Languages.pm b/docs/tool/Modules/NaturalDocs/Languages.pm
new file mode 100644
index 00000000..dde54b9d
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Languages.pm
@@ -0,0 +1,1475 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Languages
+#
+###############################################################################
+#
+#   A package to manage all the programming languages Natural Docs supports.
+#
+#   Usage and Dependencies:
+#
+#       - Prior to use, <NaturalDocs::Settings> must be initialized and <Load()> must be called.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use Text::Wrap();
+
+use NaturalDocs::Languages::Prototype;
+
+use NaturalDocs::Languages::Base;
+use NaturalDocs::Languages::Simple;
+use NaturalDocs::Languages::Advanced;
+
+use NaturalDocs::Languages::Perl;
+use NaturalDocs::Languages::CSharp;
+use NaturalDocs::Languages::ActionScript;
+
+use NaturalDocs::Languages::Ada;
+use NaturalDocs::Languages::PLSQL;
+use NaturalDocs::Languages::Pascal;
+use NaturalDocs::Languages::Tcl;
+
+use strict;
+use integer;
+
+package NaturalDocs::Languages;
+
+
+###############################################################################
+# Group: Variables
+
+
+#
+#   handle: FH_LANGUAGES
+#
+#   The file handle used for writing to <Languages.txt>.
+#
+
+
+#
+#   hash: languages
+#
+#   A hash of all the defined languages.  The keys are the all-lowercase language names, and the values are
+#   <NaturalDocs::Languages::Base>-derived objects.
+#
+my %languages;
+
+#
+#   hash: extensions
+#
+#   A hash of all the defined languages' extensions.  The keys are the all-lowercase extensions, and the values are the
+#   all-lowercase names of the languages that defined them.
+#
+my %extensions;
+
+#
+#   hash: shebangStrings
+#
+#   A hash of all the defined languages' strings to search for in the shebang (#!) line.  The keys are the all-lowercase strings, and
+#   the values are the all-lowercase names of the languages that defined them.
+#
+my %shebangStrings;
+
+#
+#   hash: shebangFiles
+#
+#   A hash of all the defined languages for files where it needs to be found via shebang strings.  The keys are the file names,
+#   and the values are language names, or undef if the file isn't supported.  These values should be filled in the first time
+#   each file is parsed for a shebang string so that it doesn't have to be done multiple times.
+#
+my %shebangFiles;
+
+#
+#   array: mainLanguageNames
+#
+#   An array of the language names that are defined in the main <Languages.txt>.
+#
+my @mainLanguageNames;
+
+
+
+###############################################################################
+# Group: Files
+
+
+#
+#   File: Languages.txt
+#
+#   The configuration file that defines or overrides the language definitions for Natural Docs.  One version sits in Natural Docs'
+#   configuration directory, and another can be in a project directory to add to or override them.
+#
+#   > # [comments]
+#
+#   Everything after a # symbol is ignored.  However, for this particular file, comments can only appear on their own lines.
+#   They cannot appear after content on the same line.
+#
+#   > Format: [version]
+#
+#   Specifies the file format version of the file.
+#
+#
+#   Sections:
+#
+#       > Ignore[d] Extension[s]: [extension] [extension] ...
+#
+#       Causes the listed file extensions to be ignored, even if they were previously defined to be part of a language.  The list is
+#       space-separated.  ex. "Ignore Extensions: cvs txt"
+#
+#
+#       > Language: [name]
+#
+#       Creates a new language.  Everything underneath applies to this language until the next one.  Names can use any
+#       characters.
+#
+#       The languages "Text File" and "Shebang Script" have special meanings.  Text files are considered all comment and don't
+#       have comment symbols.  Shebang scripts have their language determined by the shebang string and automatically
+#       include files with no extension in addition to the extensions defined.
+#
+#       If "Text File" doesn't define ignored prefixes, a package separator, or enum value behavior, those settings will be copied
+#       from the language with the most files in the source tree.
+#
+#
+#       > Alter Language: [name]
+#
+#       Alters an existing language.  Everything underneath it overrides the previous settings until the next one.  Note that if a
+#       property has an [Add/Replace] form and that property has already been defined, you have to specify whether you're adding
+#       to or replacing the defined list.
+#
+#
+#   Language Properties:
+#
+#       > Extension[s]: [extension] [extension] ...
+#       > [Add/Replace] Extension[s]: ...
+#
+#       Defines file extensions for the language's source files.  The list is space-separated.  ex. "Extensions: c cpp".  You can use
+#       extensions that were previously used by another language to redefine them.
+#
+#
+#       > Shebang String[s]: [string] [string] ...
+#       > [Add/Replace] Shebang String[s]: ...
+#
+#       Defines a list of strings that can appear in the shebang (#!) line to designate that it's part of this language.  They can
+#       appear anywhere in the line, so "php" will work for "#!/user/bin/php4".  You can use strings that were previously used by
+#       another language to redefine them.
+#
+#
+#       > Ignore[d] Prefix[es] in Index: [prefix] [prefix] ...
+#       > Ignore[d] [Topic Type] Prefix[es] in Index: [prefix] [prefix] ...
+#       > [Add/Replace] Ignore[d] Prefix[es] in Index: ...
+#       > [Add/Replace] Ignore[d] [Topic Type] Prefix[es] in Index: ...
+#
+#       Specifies prefixes that should be ignored when sorting symbols for an index.  Can be specified in general or for a specific
+#       <TopicType>.  The prefixes will still appear, the symbols will just be sorted as if they're not there.  For example, specifying
+#       "ADO_" for functions will mean that "ADO_DoSomething" will appear under D instead of A.
+#
+#
+#   Basic Language Support Properties:
+#
+#       These attributes are only available for languages with basic language support.
+#
+#
+#       > Line Comment[s]: [symbol] [symbol] ...
+#
+#       Defines a space-separated list of symbols that are used for line comments, if any.  ex. "Line Comment: //".
+#
+#
+#       > Block Comment[s]: [opening symbol] [closing symbol] [opening symbol] [closing symbol] ...
+#
+#       Defines a space-separated list of symbol pairs that are used for block comments, if any.  ex. "Block Comment: /* */".
+#
+#
+#       > Package Separator: [symbol]
+#
+#       Defines the default package separator symbol, such as . or ::.  This is for presentation only and will not affect how
+#       Natural Docs links are parsed.  The default is a dot.
+#
+#
+#       > [Topic Type] Prototype Ender[s]: [symbol] [symbol] ...
+#
+#       When defined, Natural Docs will attempt to collect prototypes from the code following the specified <TopicType>.  It grabs
+#       code until the first ender symbol or the next Natural Docs comment, and if it contains the topic name, it serves as its
+#       prototype.  Use \n to specify a line break.  ex. "Function Prototype Enders: { ;", "Variable Prototype Enders: = ;".
+#
+#
+#       > Line Extender: [symbol]
+#
+#       Defines the symbol that allows a prototype to span multiple lines if normally a line break would end it.
+#
+#
+#       > Enum Values: [global|under type|under parent]
+#
+#       Defines how enum values are referenced.  The default is global.
+#
+#       global - Values are always global, referenced as 'value'.
+#       under type - Values are under the enum type, referenced as 'package.enum.value'.
+#       under parent - Values are under the enum's parent, referenced as 'package.value'.
+#
+#
+#       > Perl Package: [perl package]
+#
+#       Specifies the Perl package used to fine-tune the language behavior in ways too complex to do in this file.
+#
+#
+#   Full Language Support Properties:
+#
+#       These attributes are only available for languages with full language support.
+#
+#
+#       > Full Language Support: [perl package]
+#
+#       Specifies the Perl package that has the parsing routines necessary for full language support.
+#
+#
+#   Revisions:
+#
+#       1.32:
+#
+#           - Package Separator is now a basic language support only property.
+#           - Added Enum Values setting.
+#
+#       1.3:
+#
+#           - The file was introduced.
+
+
+###############################################################################
+# Group: File Functions
+
+
+#
+#   Function: Load
+#
+#   Loads both the master and the project version of <Languages.txt>.
+#
+sub Load
+    {
+    my $self = shift;
+
+    # Hashrefs where the keys are all-lowercase extensions/shebang strings, and the values are arrayrefs of the languages
+    # that defined them, earliest first, all lowercase.
+    my %tempExtensions;
+    my %tempShebangStrings;
+
+    $self->LoadFile(1, \%tempExtensions, \%tempShebangStrings);  # Main
+
+    if (!exists $languages{'shebang script'})
+        {  NaturalDocs::ConfigFile->AddError('You must define "Shebang Script" in the main languages file.');  };
+    if (!exists $languages{'text file'})
+        {  NaturalDocs::ConfigFile->AddError('You must define "Text File" in the main languages file.');  };
+
+    my $errorCount = NaturalDocs::ConfigFile->ErrorCount();
+
+    if ($errorCount)
+        {
+        NaturalDocs::ConfigFile->PrintErrorsAndAnnotateFile();
+        NaturalDocs::Error->SoftDeath('There ' . ($errorCount == 1 ? 'is an error' : 'are ' . $errorCount . ' errors')
+                                                    . ' in ' . NaturalDocs::Project->MainConfigFile('Languages.txt'));
+        }
+
+
+    $self->LoadFile(0, \%tempExtensions, \%tempShebangStrings);  # User
+
+    $errorCount = NaturalDocs::ConfigFile->ErrorCount();
+
+    if ($errorCount)
+        {
+        NaturalDocs::ConfigFile->PrintErrorsAndAnnotateFile();
+        NaturalDocs::Error->SoftDeath('There ' . ($errorCount == 1 ? 'is an error' : 'are ' . $errorCount . ' errors')
+                                                    . ' in ' . NaturalDocs::Project->UserConfigFile('Languages.txt'));
+        };
+
+
+    # Convert the temp hashes into the real ones.
+
+    while (my ($extension, $languages) = each %tempExtensions)
+        {
+        $extensions{$extension} = $languages->[-1];
+        };
+    while (my ($shebangString, $languages) = each %tempShebangStrings)
+        {
+        $shebangStrings{$shebangString} = $languages->[-1];
+        };
+    };
+
+
+#
+#   Function: LoadFile
+#
+#   Loads a particular version of <Languages.txt>.
+#
+#   Parameters:
+#
+#       isMain - Whether the file is the main file or not.
+#       tempExtensions - A hashref where the keys are all-lowercase extensions, and the values are arrayrefs of the all-lowercase
+#                                 names of the languages that defined them, earliest first.  It will be changed by this function.
+#       tempShebangStrings - A hashref where the keys are all-lowercase shebang strings, and the values are arrayrefs of the
+#                                        all-lowercase names of the languages that defined them, earliest first.  It will be changed by this
+#                                        function.
+#
+sub LoadFile #(isMain, tempExtensions, tempShebangStrings)
+    {
+    my ($self, $isMain, $tempExtensions, $tempShebangStrings) = @_;
+
+    my ($file, $status);
+
+    if ($isMain)
+        {
+        $file = NaturalDocs::Project->MainConfigFile('Languages.txt');
+        $status = NaturalDocs::Project->MainConfigFileStatus('Languages.txt');
+        }
+    else
+        {
+        $file = NaturalDocs::Project->UserConfigFile('Languages.txt');
+        $status = NaturalDocs::Project->UserConfigFileStatus('Languages.txt');
+        };
+
+
+    my $version;
+
+    # An array of properties for the current language.  Each entry is the three consecutive values ( lineNumber, keyword, value ).
+    my @properties;
+
+    if ($version = NaturalDocs::ConfigFile->Open($file))
+        {
+        # The format hasn't changed significantly since the file was introduced.
+
+        if ($status == ::FILE_CHANGED())
+            {
+            NaturalDocs::Project->ReparseEverything();
+            NaturalDocs::SymbolTable->RebuildAllIndexes();  # Because the ignored prefixes could change.
+            };
+
+        my ($keyword, $value, $comment);
+
+        while (($keyword, $value, $comment) = NaturalDocs::ConfigFile->GetLine())
+            {
+            $value .= $comment;
+            $value =~ s/^ //;
+
+            # Process previous properties.
+            if (($keyword eq 'language' || $keyword eq 'alter language') && scalar @properties)
+                {
+                if ($isMain && $properties[1] eq 'language')
+                    {  push @mainLanguageNames, $properties[2];  };
+
+                $self->ProcessProperties(\@properties, $version, $tempExtensions, $tempShebangStrings);
+                @properties = ( );
+                };
+
+            if ($keyword =~ /^ignored? extensions?$/)
+                {
+                $value =~ tr/.*//d;
+                my @extensions = split(/ /, lc($value));
+
+                foreach my $extension (@extensions)
+                    {  delete $tempExtensions->{$extension};  };
+                }
+            else
+                {
+                push @properties, NaturalDocs::ConfigFile->LineNumber(), $keyword, $value;
+                };
+            };
+
+        if (scalar @properties)
+            {
+            if ($isMain && $properties[1] eq 'language')
+                {  push @mainLanguageNames, $properties[2];  };
+
+            $self->ProcessProperties(\@properties, $version, $tempExtensions, $tempShebangStrings);
+            };
+        }
+
+    else # couldn't open file
+        {
+        if ($isMain)
+            {  die "Couldn't open languages file " . $file . "\n";  };
+        };
+    };
+
+
+#
+#   Function: ProcessProperties
+#
+#   Processes an array of language properties from <Languages.txt>.
+#
+#   Parameters:
+#
+#       properties - An arrayref of properties where each entry is the three consecutive values ( lineNumber, keyword, value ).
+#                         It must start with the Language or Alter Language property.
+#       version - The <VersionInt> of the file.
+#       tempExtensions - A hashref where the keys are all-lowercase extensions, and the values are arrayrefs of the all-lowercase
+#                                 names of the languages that defined them, earliest first.  It will be changed by this function.
+#       tempShebangStrings - A hashref where the keys are all-lowercase shebang strings, and the values are arrayrefs of the
+#                                        all-lowercase names of the languages that defined them, earliest first.  It will be changed by this
+#                                        function.
+#
+sub ProcessProperties #(properties, version, tempExtensions, tempShebangStrings)
+    {
+    my ($self, $properties, $version, $tempExtensions, $tempShebangStrings) = @_;
+
+
+    # First validate the name and check whether the language has full support.
+
+    my $language;
+    my $fullLanguageSupport;
+    my ($lineNumber, $languageKeyword, $languageName) = @$properties[0..2];
+    my $lcLanguageName = lc($languageName);
+    my ($keyword, $value);
+
+    if ($languageKeyword eq 'alter language')
+        {
+        $language = $languages{$lcLanguageName};
+
+        if (!defined $language)
+            {
+            NaturalDocs::ConfigFile->AddError('The language ' . $languageName . ' is not defined.', $lineNumber);
+            return;
+            }
+        else
+            {
+            $fullLanguageSupport = (!$language->isa('NaturalDocs::Languages::Simple'));
+            };
+        }
+
+    elsif ($languageKeyword eq 'language')
+        {
+        if (exists $languages{$lcLanguageName})
+            {
+            NaturalDocs::ConfigFile->AddError('The language ' . $value . ' is already defined.  Use "Alter Language" if you want '
+                                                             . 'to override its settings.', $lineNumber);
+            return;
+            };
+
+        # Case is important with these two.
+        if ($lcLanguageName eq 'shebang script')
+            {  $languageName = 'Shebang Script';  }
+        elsif ($lcLanguageName eq 'text file')
+            {  $languageName = 'Text File';  };
+
+
+        # Go through the properties looking for whether the language has basic or full support and which package to use to create
+        # it.
+
+        for (my $i = 3; $i < scalar @$properties; $i += 3)
+            {
+            ($lineNumber, $keyword, $value) = @$properties[$i..$i+2];
+
+            if ($keyword eq 'full language support')
+                {
+                $fullLanguageSupport = 1;
+
+                eval
+                    {
+                    $language = $value->New($languageName);
+                    };
+                if ($::EVAL_ERROR)
+                    {
+                    NaturalDocs::ConfigFile->AddError('Could not create ' . $value . ' object.', $lineNumber);
+                    return;
+                    };
+
+                last;
+                }
+
+            elsif ($keyword eq 'perl package')
+                {
+                eval
+                    {
+                    $language = $value->New($languageName);
+                    };
+                if ($::EVAL_ERROR)
+                    {
+                    NaturalDocs::ConfigFile->AddError('Could not create ' . $value . ' object.', $lineNumber);
+                    return;
+                    };
+                };
+            };
+
+        # If $language was not created by now, it's a generic basic support language.
+        if (!defined $language)
+            {  $language = NaturalDocs::Languages::Simple->New($languageName);  };
+
+        $languages{$lcLanguageName} = $language;
+        }
+
+    else # not language or alter language
+        {
+        NaturalDocs::ConfigFile->AddError('You must start this line with "Language", "Alter Language", or "Ignore Extensions".',
+                                                           $lineNumber);
+        return;
+        };
+
+
+    # Decode the properties.
+
+    for (my $i = 3; $i < scalar @$properties; $i += 3)
+        {
+        ($lineNumber, $keyword, $value) = @$properties[$i..$i+2];
+
+        if ($keyword =~ /^(?:(add|replace) )?extensions?$/)
+            {
+            my $command = $1;
+
+
+            # Remove old extensions.
+
+            if (defined $language->Extensions() && $command eq 'replace')
+                {
+                foreach my $extension (@{$language->Extensions()})
+                    {
+                    if (exists $tempExtensions->{$extension})
+                        {
+                        my $languages = $tempExtensions->{$extension};
+                        my $i = 0;
+
+                        while ($i < scalar @$languages)
+                            {
+                            if ($languages->[$i] eq $lcLanguageName)
+                                {  splice(@$languages, $i, 1);  }
+                            else
+                                {  $i++;  };
+                            };
+
+                        if (!scalar @$languages)
+                            {  delete $tempExtensions->{$extension};  };
+                        };
+                    };
+                };
+
+
+            # Add new extensions.
+
+            # Ignore stars and dots so people could use .ext or *.ext.
+            $value =~ s/\*\.|\.//g;
+
+            my @extensions = split(/ /, lc($value));
+
+            foreach my $extension (@extensions)
+                {
+                if (!exists $tempExtensions->{$extension})
+                    {  $tempExtensions->{$extension} = [ ];  };
+
+                push @{$tempExtensions->{$extension}}, $lcLanguageName;
+                };
+
+
+            # Set the extensions for the language object.
+
+            if (defined $language->Extensions())
+                {
+                if ($command eq 'add')
+                    {  push @extensions, @{$language->Extensions()};  }
+                elsif (!$command)
+                    {
+                    NaturalDocs::ConfigFile->AddError('You need to specify whether you are adding to or replacing the list of extensions.',
+                                                                       $lineNumber);
+                    };
+                };
+
+            $language->SetExtensions(\@extensions);
+            }
+
+        elsif ($keyword =~ /^(?:(add|replace) )?shebang strings?$/)
+            {
+            my $command = $1;
+
+
+            # Remove old strings.
+
+            if (defined $language->ShebangStrings() && $command eq 'replace')
+                {
+                foreach my $shebangString (@{$language->ShebangStrings()})
+                    {
+                    if (exists $tempShebangStrings->{$shebangString})
+                        {
+                        my $languages = $tempShebangStrings->{$shebangString};
+                        my $i = 0;
+
+                        while ($i < scalar @$languages)
+                            {
+                            if ($languages->[$i] eq $lcLanguageName)
+                                {  splice(@$languages, $i, 1);  }
+                            else
+                                {  $i++;  };
+                            };
+
+                        if (!scalar @$languages)
+                            {  delete $tempShebangStrings->{$shebangString};  };
+                        };
+                    };
+                };
+
+
+            # Add new strings.
+
+            my @shebangStrings = split(/ /, lc($value));
+
+            foreach my $shebangString (@shebangStrings)
+                {
+                if (!exists $tempShebangStrings->{$shebangString})
+                    {  $tempShebangStrings->{$shebangString} = [ ];  };
+
+                push @{$tempShebangStrings->{$shebangString}}, $lcLanguageName;
+                };
+
+
+            # Set the strings for the language object.
+
+            if (defined $language->ShebangStrings())
+                {
+                if ($command eq 'add')
+                    {  push @shebangStrings, @{$language->ShebangStrings()};  }
+                elsif (!$command)
+                    {
+                    NaturalDocs::ConfigFile->AddError('You need to specify whether you are adding to or replacing the list of shebang '
+                                                                     . 'strings.', $lineNumber);
+                    };
+                };
+
+            $language->SetShebangStrings(\@shebangStrings);
+            }
+
+        elsif ($keyword eq 'package separator')
+            {
+            if ($fullLanguageSupport)
+                {
+                # Prior to 1.32, package separator was used with full language support too.  Accept it without complaining, even though
+                # we ignore it.
+                if ($version >= NaturalDocs::Version->FromString('1.32'))
+                    {
+                    NaturalDocs::ConfigFile->AddError('You cannot define this property when using full language support.', $lineNumber);
+                    };
+                }
+            else
+                {  $language->SetPackageSeparator($value);  };
+            }
+
+        elsif ($keyword =~ /^(?:(add|replace) )?ignored? (?:(.+) )?prefix(?:es)? in index$/)
+            {
+            my ($command, $topicName) = ($1, $2);
+            my $topicType;
+
+            if ($topicName)
+                {
+                if (!( ($topicType, undef) = NaturalDocs::Topics->NameInfo($topicName) ))
+                    {
+                    NaturalDocs::ConfigFile->AddError($topicName . ' is not a defined topic type.', $lineNumber);
+                    };
+                }
+            else
+                {  $topicType = ::TOPIC_GENERAL();  };
+
+            if ($topicType)
+                {
+                my @prefixes;
+
+                if (defined $language->IgnoredPrefixesFor($topicType))
+                    {
+                    if ($command eq 'add')
+                        {  @prefixes = @{$language->IgnoredPrefixesFor($topicType)};  }
+                    elsif (!$command)
+                        {
+                        NaturalDocs::ConfigFile->AddError('You need to specify whether you are adding to or replacing the list of '
+                                                                         . 'ignored prefixes.', $lineNumber);
+                        };
+                    };
+
+                push @prefixes, split(/ /, $value);
+                $language->SetIgnoredPrefixesFor($topicType, \@prefixes);
+                };
+            }
+
+        elsif ($keyword eq 'full language support' || $keyword eq 'perl package')
+            {
+            if ($languageKeyword eq 'alter language')
+                {
+                NaturalDocs::ConfigFile->AddError('You cannot use ' . $keyword . ' with Alter Language.', $lineNumber);
+                };
+            # else ignore it.
+            }
+
+        elsif ($keyword =~ /^line comments?$/)
+            {
+            if ($fullLanguageSupport)
+                {
+                NaturalDocs::ConfigFile->AddError('You cannot define this property when using full language support.', $lineNumber);
+                }
+            else
+                {
+                my @symbols = split(/ /, $value);
+                $language->SetLineCommentSymbols(\@symbols);
+                };
+            }
+
+        elsif ($keyword =~ /^block comments?$/)
+            {
+            if ($fullLanguageSupport)
+                {
+                NaturalDocs::ConfigFile->AddError('You cannot define this property when using full language support.', $lineNumber);
+                }
+            else
+                {
+                my @symbols = split(/ /, $value);
+
+                if ((scalar @symbols) % 2 == 0)
+                    {  $language->SetBlockCommentSymbols(\@symbols);  }
+                else
+                    {  NaturalDocs::ConfigFile->AddError('Block comment symbols must appear in pairs.', $lineNumber);  };
+                };
+            }
+
+        elsif ($keyword =~ /^(?:(.+) )?prototype enders?$/)
+            {
+            if ($fullLanguageSupport)
+                {
+                NaturalDocs::ConfigFile->AddError('You cannot define this property when using full language support.', $lineNumber);
+                }
+            else
+                {
+                my $topicName = $1;
+                my $topicType;
+
+                if ($topicName)
+                    {
+                    if (!( ($topicType, undef) = NaturalDocs::Topics->NameInfo($topicName) ))
+                        {
+                        NaturalDocs::ConfigFile->AddError($topicName . ' is not a defined topic type.', $lineNumber);
+                        };
+                    }
+                else
+                    {  $topicType = ::TOPIC_GENERAL();  };
+
+                if ($topicType)
+                    {
+                    $value =~ s/\\n/\n/g;
+                    my @symbols = split(/ /, $value);
+                    $language->SetPrototypeEndersFor($topicType, \@symbols);
+                    };
+                };
+            }
+
+        elsif ($keyword eq 'line extender')
+            {
+            if ($fullLanguageSupport)
+                {
+                NaturalDocs::ConfigFile->AddError('You cannot define this property when using full language support.', $lineNumber);
+                }
+            else
+                {
+                $language->SetLineExtender($value);
+                };
+            }
+
+        elsif ($keyword eq 'enum values')
+            {
+            if ($fullLanguageSupport)
+                {
+                NaturalDocs::ConfigFile->AddError('You cannot define this property when using full language support.', $lineNumber);
+                }
+            else
+                {
+                $value = lc($value);
+                my $constant;
+
+                if ($value eq 'global')
+                    {  $constant = ::ENUM_GLOBAL();  }
+                elsif ($value eq 'under type')
+                    {  $constant = ::ENUM_UNDER_TYPE();  }
+                elsif ($value eq 'under parent')
+                    {  $constant = ::ENUM_UNDER_PARENT();  };
+
+                if (defined $constant)
+                    {  $language->SetEnumValues($constant);  }
+                else
+                    {
+                    NaturalDocs::ConfigFile->AddError('Enum Values must be "Global", "Under Type", or "Under Parent".', $lineNumber);
+                    };
+                };
+            }
+
+        else
+            {
+            NaturalDocs::ConfigFile->AddError($keyword . ' is not a valid keyword.', $lineNumber);
+            };
+        };
+    };
+
+
+#
+#   Function: Save
+#
+#   Saves the main and user versions of <Languages.txt>.
+#
+sub Save
+    {
+    my $self = shift;
+
+    $self->SaveFile(1); # Main
+    $self->SaveFile(0); # User
+    };
+
+
+#
+#   Function: SaveFile
+#
+#   Saves a particular version of <Topics.txt>.
+#
+#   Parameters:
+#
+#       isMain - Whether the file is the main file or not.
+#
+sub SaveFile #(isMain)
+    {
+    my ($self, $isMain) = @_;
+
+    my $file;
+
+    if ($isMain)
+        {
+        if (NaturalDocs::Project->MainConfigFileStatus('Languages.txt') == ::FILE_SAME())
+            {  return;  };
+        $file = NaturalDocs::Project->MainConfigFile('Languages.txt');
+        }
+    else
+        {
+        # Have to check the main too because this file lists the languages defined there.
+        if (NaturalDocs::Project->UserConfigFileStatus('Languages.txt') == ::FILE_SAME() &&
+            NaturalDocs::Project->MainConfigFileStatus('Languages.txt') == ::FILE_SAME())
+            {  return;  };
+        $file = NaturalDocs::Project->UserConfigFile('Languages.txt');
+        };
+
+
+    # Array of segments, with each being groups of three consecutive entries.  The first is the keyword ('language' or
+    # 'alter language'), the second is the value, and the third is a hashref of all the properties.
+    # - For properties that can accept a topic type, the property values are hashrefs mapping topic types to the values.
+    # - For properties that can accept 'add' or 'replace', there is an additional property ending in 'command' that stores it.
+    # - For properties that can accept both, the 'command' thing is applied to the topic types rather than the properties.
+    my @segments;
+
+    my @ignoredExtensions;
+
+    my $currentProperties;
+    my $version;
+
+    if ($version = NaturalDocs::ConfigFile->Open($file))
+        {
+        # We can assume the file is valid.
+
+        while (my ($keyword, $value, $comment) = NaturalDocs::ConfigFile->GetLine())
+            {
+            $value .= $comment;
+            $value =~ s/^ //;
+
+            if ($keyword eq 'language')
+                {
+                $currentProperties = { };
+
+                # Case is important with these two.
+                if (lc($value) eq 'shebang script')
+                    {  $value = 'Shebang Script';  }
+                elsif (lc($value) eq 'text file')
+                    {  $value = 'Text File';  };
+
+                push @segments, 'language', $value, $currentProperties;
+                }
+
+            elsif ($keyword eq 'alter language')
+                {
+                $currentProperties = { };
+                push @segments, 'alter language', $languages{lc($value)}->Name(), $currentProperties;
+                }
+
+            elsif ($keyword =~ /^ignored? extensions?$/)
+                {
+                $value =~ tr/*.//d;
+                push @ignoredExtensions, split(/ /, $value);
+                }
+
+            elsif ($keyword eq 'package separator' || $keyword eq 'full language support' || $keyword eq 'perl package' ||
+                    $keyword eq 'line extender' || $keyword eq 'enum values')
+                {
+                $currentProperties->{$keyword} = $value;
+                }
+
+            elsif ($keyword =~ /^line comments?$/)
+                {
+                $currentProperties->{'line comments'} = $value;
+                }
+            elsif ($keyword =~ /^block comments?$/)
+                {
+                $currentProperties->{'block comments'} = $value;
+                }
+
+            elsif ($keyword =~ /^(?:(add|replace) )?extensions?$/)
+                {
+                my $command = $1;
+
+                if ($command eq 'add' && exists $currentProperties->{'extensions'})
+                    {  $currentProperties->{'extensions'} .= ' ' . $value;  }
+                else
+                    {
+                    $currentProperties->{'extensions'} = $value;
+                    $currentProperties->{'extensions command'} = $command;
+                    };
+                }
+
+            elsif ($keyword =~ /^(?:(add|replace) )?shebang strings?$/)
+                {
+                my $command = $1;
+
+                if ($command eq 'add' && exists $currentProperties->{'shebang strings'})
+                    {  $currentProperties->{'shebang strings'} .= ' ' . $value;  }
+                else
+                    {
+                    $currentProperties->{'shebang strings'} = $value;
+                    $currentProperties->{'shebang strings command'} = $command;
+                    };
+                }
+
+            elsif ($keyword =~ /^(?:(.+) )?prototype enders?$/)
+                {
+                my $topicName = $1;
+                my $topicType;
+
+                if ($topicName)
+                    {  ($topicType, undef) = NaturalDocs::Topics->NameInfo($topicName);  }
+                else
+                    {  $topicType = ::TOPIC_GENERAL();  };
+
+                my $currentTypeProperties = $currentProperties->{'prototype enders'};
+
+                if (!defined $currentTypeProperties)
+                    {
+                    $currentTypeProperties = { };
+                    $currentProperties->{'prototype enders'} = $currentTypeProperties;
+                    };
+
+                $currentTypeProperties->{$topicType} = $value;
+                }
+
+            elsif ($keyword =~ /^(?:(add|replace) )?ignored? (?:(.+) )?prefix(?:es)? in index$/)
+                {
+                my ($command, $topicName) = ($1, $2);
+                my $topicType;
+
+                if ($topicName)
+                    {  ($topicType, undef) = NaturalDocs::Topics->NameInfo($topicName);  }
+                else
+                    {  $topicType = ::TOPIC_GENERAL();  };
+
+                my $currentTypeProperties = $currentProperties->{'ignored prefixes in index'};
+
+                if (!defined $currentTypeProperties)
+                    {
+                    $currentTypeProperties = { };
+                    $currentProperties->{'ignored prefixes in index'} = $currentTypeProperties;
+                    };
+
+                if ($command eq 'add' && exists $currentTypeProperties->{$topicType})
+                    {  $currentTypeProperties->{$topicType} .= ' ' . $value;  }
+                else
+                    {
+                    $currentTypeProperties->{$topicType} = $value;
+                    $currentTypeProperties->{$topicType . ' command'} = $command;
+                    };
+                };
+            };
+
+        NaturalDocs::ConfigFile->Close();
+        };
+
+
+    if (!open(FH_LANGUAGES, '>' . $file))
+        {
+        # The main file may be on a shared volume or some other place the user doesn't have write access to.  Since this is only to
+        # reformat the file, we can ignore the failure.
+        if ($isMain)
+            {  return;  }
+        else
+            {  die "Couldn't save " . $file;  };
+        };
+
+    print FH_LANGUAGES 'Format: ' . NaturalDocs::Settings->TextAppVersion() . "\n\n";
+
+    # Remember the 80 character limit.
+
+    if ($isMain)
+        {
+        print FH_LANGUAGES
+        "# This is the main Natural Docs languages file.  If you change anything here,\n"
+        . "# it will apply to EVERY PROJECT you use Natural Docs on.  If you'd like to\n"
+        . "# change something for just one project, edit the Languages.txt in its project\n"
+        . "# directory instead.\n";
+        }
+    else
+        {
+        print FH_LANGUAGES
+        "# This is the Natural Docs languages file for this project.  If you change\n"
+        . "# anything here, it will apply to THIS PROJECT ONLY.  If you'd like to change\n"
+        . "# something for all your projects, edit the Languages.txt in Natural Docs'\n"
+        . "# Config directory instead.\n\n\n";
+
+        if (scalar @ignoredExtensions == 1)
+            {
+            print FH_LANGUAGES
+            'Ignore Extension: ' . $ignoredExtensions[0] . "\n";
+            }
+        elsif (scalar @ignoredExtensions)
+            {
+            print FH_LANGUAGES
+            'Ignore Extensions: ' . join(' ', @ignoredExtensions) . "\n";
+            }
+        else
+            {
+            print FH_LANGUAGES
+            "# You can prevent certain file extensions from being scanned like this:\n"
+            . "# Ignore Extensions: [extension] [extension] ...\n"
+            };
+        };
+
+    print FH_LANGUAGES
+    "\n\n"
+    . "#-------------------------------------------------------------------------------\n"
+    . "# SYNTAX:\n"
+    . "#\n"
+    . "# Unlike other Natural Docs configuration files, in this file all comments\n"
+    . "# MUST be alone on a line.  Some languages deal with the # character, so you\n"
+    . "# cannot put comments on the same line as content.\n"
+    . "#\n"
+    . "# Also, all lists are separated with spaces, not commas, again because some\n"
+    . "# languages may need to use them.\n"
+    . "#\n";
+
+    if ($isMain)
+        {
+        print FH_LANGUAGES
+        "# Language: [name]\n"
+        . "#    Defines a new language.  Its name can use any characters.\n"
+        . "#\n";
+        }
+    else
+        {
+        print FH_LANGUAGES
+        "# Language: [name]\n"
+        . "# Alter Language: [name]\n"
+        . "#    Defines a new language or alters an existing one.  Its name can use any\n"
+        . "#    characters.  If any of the properties below have an add/replace form, you\n"
+        . "#    must use that when using Alter Language.\n"
+        . "#\n";
+        };
+
+    print FH_LANGUAGES
+    "#    The language Shebang Script is special.  It's entry is only used for\n"
+    . "#    extensions, and files with those extensions have their shebang (#!) lines\n"
+    . "#    read to determine the real language of the file.  Extensionless files are\n"
+    . "#    always treated this way.\n"
+    . "#\n"
+    . "#    The language Text File is also special.  It's treated as one big comment\n"
+    . "#    so you can put Natural Docs content in them without special symbols.  Also,\n"
+    . "#    if you don't specify a package separator, ignored prefixes, or enum value\n"
+    . "#    behavior, it will copy those settings from the language that is used most\n"
+    . "#    in the source tree.\n"
+    . "#\n"
+    . "# Extensions: [extension] [extension] ...\n";
+
+    if ($isMain)
+        {
+        print FH_LANGUAGES
+        "#    Defines the file extensions of the language's source files.  You can use *\n"
+        . "#    to mean any undefined extension.\n"
+        . "#\n"
+        . "# Shebang Strings: [string] [string] ...\n"
+        . "#    Defines a list of strings that can appear in the shebang (#!) line to\n"
+        . "#    designate that it's part of the language.\n"
+        . "#\n";
+        }
+    else
+        {
+        print FH_LANGUAGES
+        "# [Add/Replace] Extensions: [extension] [extension] ...\n"
+        . "#    Defines the file extensions of the language's source files.  You can\n"
+        . "#    redefine extensions found in the main languages file.  You can use * to\n"
+        . "#    mean any undefined extension.\n"
+        . "#\n"
+        . "# Shebang Strings: [string] [string] ...\n"
+        . "# [Add/Replace] Shebang Strings: [string] [string] ...\n"
+        . "#    Defines a list of strings that can appear in the shebang (#!) line to\n"
+        . "#    designate that it's part of the language.  You can redefine strings found\n"
+        . "#    in the main languages file.\n"
+        . "#\n";
+        };
+
+    print FH_LANGUAGES
+    "# Ignore Prefixes in Index: [prefix] [prefix] ...\n"
+    . (!$isMain ? "# [Add/Replace] Ignored Prefixes in Index: [prefix] [prefix] ...\n#\n" : '')
+    . "# Ignore [Topic Type] Prefixes in Index: [prefix] [prefix] ...\n"
+    . (!$isMain ? "# [Add/Replace] Ignored [Topic Type] Prefixes in Index: [prefix] [prefix] ...\n" : '')
+    . "#    Specifies prefixes that should be ignored when sorting symbols in an\n"
+    . "#    index.  Can be specified in general or for a specific topic type.\n"
+    . "#\n"
+    . "#------------------------------------------------------------------------------\n"
+    . "# For basic language support only:\n"
+    . "#\n"
+    . "# Line Comments: [symbol] [symbol] ...\n"
+    . "#    Defines a space-separated list of symbols that are used for line comments,\n"
+    . "#    if any.\n"
+    . "#\n"
+    . "# Block Comments: [opening sym] [closing sym] [opening sym] [closing sym] ...\n"
+    . "#    Defines a space-separated list of symbol pairs that are used for block\n"
+    . "#    comments, if any.\n"
+    . "#\n"
+    . "# Package Separator: [symbol]\n"
+    . "#    Defines the default package separator symbol.  The default is a dot.\n"
+    . "#\n"
+    . "# [Topic Type] Prototype Enders: [symbol] [symbol] ...\n"
+    . "#    When defined, Natural Docs will attempt to get a prototype from the code\n"
+    . "#    immediately following the topic type.  It stops when it reaches one of\n"
+    . "#    these symbols.  Use \\n for line breaks.\n"
+    . "#\n"
+    . "# Line Extender: [symbol]\n"
+    . "#    Defines the symbol that allows a prototype to span multiple lines if\n"
+    . "#    normally a line break would end it.\n"
+    . "#\n"
+    . "# Enum Values: [global|under type|under parent]\n"
+    . "#    Defines how enum values are referenced.  The default is global.\n"
+    . "#    global       - Values are always global, referenced as 'value'.\n"
+    . "#    under type   - Values are under the enum type, referenced as\n"
+    . "#               'package.enum.value'.\n"
+    . "#    under parent - Values are under the enum's parent, referenced as\n"
+    . "#               'package.value'.\n"
+    . "#\n"
+    . "# Perl Package: [perl package]\n"
+    . "#    Specifies the Perl package used to fine-tune the language behavior in ways\n"
+    . "#    too complex to do in this file.\n"
+    . "#\n"
+    . "#------------------------------------------------------------------------------\n"
+    . "# For full language support only:\n"
+    . "#\n"
+    . "# Full Language Support: [perl package]\n"
+    . "#    Specifies the Perl package that has the parsing routines necessary for full\n"
+    . "#    language support.\n"
+    . "#\n"
+    . "#-------------------------------------------------------------------------------\n\n";
+
+    if ($isMain)
+        {
+        print FH_LANGUAGES
+        "# The following languages MUST be defined in this file:\n"
+        . "#\n"
+        . "#    Text File, Shebang Script\n";
+        }
+    else
+        {
+        print FH_LANGUAGES
+        "# The following languages are defined in the main file, if you'd like to alter\n"
+        . "# them:\n"
+        . "#\n"
+        . Text::Wrap::wrap('#    ', '#    ', join(', ', @mainLanguageNames)) . "\n";
+        };
+
+    print FH_LANGUAGES "\n"
+    . "# If you add a language that you think would be useful to other developers\n"
+    . "# and should be included in Natural Docs by default, please e-mail it to\n"
+    . "# languages [at] naturaldocs [dot] org.\n";
+
+    my @topicTypeOrder = ( ::TOPIC_GENERAL(), ::TOPIC_CLASS(), ::TOPIC_FUNCTION(), ::TOPIC_VARIABLE(),
+                                         ::TOPIC_PROPERTY(), ::TOPIC_TYPE(), ::TOPIC_CONSTANT() );
+
+    for (my $i = 0; $i < scalar @segments; $i += 3)
+        {
+        my ($keyword, $name, $properties) = @segments[$i..$i+2];
+
+        print FH_LANGUAGES "\n\n";
+
+        if ($keyword eq 'language')
+            {  print FH_LANGUAGES 'Language: ' . $name . "\n\n";  }
+        else
+            {  print FH_LANGUAGES 'Alter Language: ' . $name . "\n\n";  };
+
+        if (exists $properties->{'extensions'})
+            {
+            print FH_LANGUAGES '   ';
+
+            if ($properties->{'extensions command'})
+                {  print FH_LANGUAGES ucfirst($properties->{'extensions command'}) . ' ';  };
+
+            my @extensions = split(/ /, $properties->{'extensions'}, 2);
+
+            if (scalar @extensions == 1)
+                {  print FH_LANGUAGES 'Extension: ';  }
+            else
+                {  print FH_LANGUAGES 'Extensions: ';  };
+
+            print FH_LANGUAGES lc($properties->{'extensions'}) . "\n";
+            };
+
+        if (exists $properties->{'shebang strings'})
+            {
+            print FH_LANGUAGES '   ';
+
+            if ($properties->{'shebang strings command'})
+                {  print FH_LANGUAGES ucfirst($properties->{'shebang strings command'}) . ' ';  };
+
+            my @shebangStrings = split(/ /, $properties->{'shebang strings'}, 2);
+
+            if (scalar @shebangStrings == 1)
+                {  print FH_LANGUAGES 'Shebang String: ';  }
+            else
+                {  print FH_LANGUAGES 'Shebang Strings: ';  };
+
+            print FH_LANGUAGES lc($properties->{'shebang strings'}) . "\n";
+            };
+
+        if (exists $properties->{'ignored prefixes in index'})
+            {
+            my $topicTypePrefixes = $properties->{'ignored prefixes in index'};
+
+            my %usedTopicTypes;
+            my @topicTypes = ( @topicTypeOrder, keys %$topicTypePrefixes );
+
+            foreach my $topicType (@topicTypes)
+                {
+                if ($topicType !~ / command$/ &&
+                    exists $topicTypePrefixes->{$topicType} &&
+                    !exists $usedTopicTypes{$topicType})
+                    {
+                    print FH_LANGUAGES '   ';
+
+                    if ($topicTypePrefixes->{$topicType . ' command'})
+                        {  print FH_LANGUAGES ucfirst($topicTypePrefixes->{$topicType . ' command'}) . ' Ignored ';  }
+                    else
+                        {  print FH_LANGUAGES 'Ignore ';  };
+
+                    if ($topicType ne ::TOPIC_GENERAL())
+                        {  print FH_LANGUAGES NaturalDocs::Topics->TypeInfo($topicType)->Name() . ' ';  };
+
+                    my @prefixes = split(/ /, $topicTypePrefixes->{$topicType}, 2);
+
+                    if (scalar @prefixes == 1)
+                        {  print FH_LANGUAGES 'Prefix in Index: ';  }
+                    else
+                        {  print FH_LANGUAGES 'Prefixes in Index: ';  };
+
+                    print FH_LANGUAGES $topicTypePrefixes->{$topicType} . "\n";
+
+                    $usedTopicTypes{$topicType} = 1;
+                    };
+                };
+            };
+
+        if (exists $properties->{'line comments'})
+            {
+            my @comments = split(/ /, $properties->{'line comments'}, 2);
+
+            if (scalar @comments == 1)
+                {  print FH_LANGUAGES '   Line Comment: ';  }
+            else
+                {  print FH_LANGUAGES '   Line Comments: ';  };
+
+            print FH_LANGUAGES $properties->{'line comments'} . "\n";
+            };
+
+        if (exists $properties->{'block comments'})
+            {
+            my @comments = split(/ /, $properties->{'block comments'}, 3);
+
+            if (scalar @comments == 2)
+                {  print FH_LANGUAGES '   Block Comment: ';  }
+            else
+                {  print FH_LANGUAGES '   Block Comments: ';  };
+
+            print FH_LANGUAGES $properties->{'block comments'} . "\n";
+            };
+
+        if (exists $properties->{'package separator'})
+            {
+            # Prior to 1.32, Package Separator was allowed for full language support.  Ignore it when reformatting.
+            if ($version >= NaturalDocs::Version->FromString('1.32') || !exists $properties->{'full language support'})
+                {  print FH_LANGUAGES '   Package Separator: ' . $properties->{'package separator'} . "\n";  };
+            };
+
+        if (exists $properties->{'enum values'})
+            {
+            print FH_LANGUAGES '   Enum Values: ' . ucfirst(lc($properties->{'enum values'})) . "\n";
+            };
+
+        if (exists $properties->{'prototype enders'})
+            {
+            my $topicTypeEnders = $properties->{'prototype enders'};
+
+            my %usedTopicTypes;
+            my @topicTypes = ( @topicTypeOrder, keys %$topicTypeEnders );
+
+            foreach my $topicType (@topicTypes)
+                {
+                if ($topicType !~ / command$/ &&
+                    exists $topicTypeEnders->{$topicType} &&
+                    !exists $usedTopicTypes{$topicType})
+                    {
+                    print FH_LANGUAGES '   ';
+
+                    if ($topicType ne ::TOPIC_GENERAL())
+                        {  print FH_LANGUAGES NaturalDocs::Topics->TypeInfo($topicType)->Name() . ' ';  };
+
+                    my @enders = split(/ /, $topicTypeEnders->{$topicType}, 2);
+
+                    if (scalar @enders == 1)
+                        {  print FH_LANGUAGES 'Prototype Ender: ';  }
+                    else
+                        {  print FH_LANGUAGES 'Prototype Enders: ';  };
+
+                    print FH_LANGUAGES $topicTypeEnders->{$topicType} . "\n";
+
+                    $usedTopicTypes{$topicType} = 1;
+                    };
+                };
+            };
+
+        if (exists $properties->{'line extender'})
+            {
+            print FH_LANGUAGES '   Line Extender: ' . $properties->{'line extender'} . "\n";
+            };
+
+        if (exists $properties->{'perl package'})
+            {
+            print FH_LANGUAGES '   Perl Package: ' . $properties->{'perl package'} . "\n";
+            };
+
+        if (exists $properties->{'full language support'})
+            {
+            print FH_LANGUAGES '   Full Language Support: ' . $properties->{'full language support'} . "\n";
+            };
+        };
+
+    close(FH_LANGUAGES);
+    };
+
+
+
+###############################################################################
+# Group: Functions
+
+
+#
+#   Function: LanguageOf
+#
+#   Returns the language of the passed source file.
+#
+#   Parameters:
+#
+#       sourceFile - The source <FileName> to get the language of.
+#
+#   Returns:
+#
+#       A <NaturalDocs::Languages::Base>-derived object for the passed file, or undef if the file is not a recognized language.
+#
+sub LanguageOf #(sourceFile)
+    {
+    my ($self, $sourceFile) = @_;
+
+    my $extension = NaturalDocs::File->ExtensionOf($sourceFile);
+    if (defined $extension)
+        {  $extension = lc($extension);  };
+
+    my $languageName;
+
+    if (!defined $extension)
+        {  $languageName = 'shebang script';  }
+    else
+        {  $languageName = $extensions{$extension};  };
+
+    if (!defined $languageName)
+        {  $languageName = $extensions{'*'};  };
+
+    if (defined $languageName)
+        {
+        if ($languageName eq 'shebang script')
+            {
+            if (exists $shebangFiles{$sourceFile})
+                {
+                if (defined $shebangFiles{$sourceFile})
+                    {  return $languages{$shebangFiles{$sourceFile}};  }
+                else
+                    {  return undef;  };
+                }
+
+            else # (!exists $shebangFiles{$sourceFile})
+                {
+                my $shebangLine;
+
+                if (open(SOURCEFILEHANDLE, '<' . $sourceFile))
+                	{
+	                read(SOURCEFILEHANDLE, $shebangLine, 2);
+	                if ($shebangLine eq '#!')
+	                    {  $shebangLine = <SOURCEFILEHANDLE>;  }
+	                else
+	                    {  $shebangLine = undef;  };
+
+	                close (SOURCEFILEHANDLE);
+	                }
+	            elsif (defined $extension)
+	            	{  die 'Could not open ' . $sourceFile;  }
+	            # Ignore extensionless files that can't be opened.  They may be system files.
+
+                if (!defined $shebangLine)
+                    {
+                    $shebangFiles{$sourceFile} = undef;
+                    return undef;
+                    }
+                else
+                    {
+                    $shebangLine = lc($shebangLine);
+
+                    foreach my $shebangString (keys %shebangStrings)
+                        {
+                        if (index($shebangLine, $shebangString) != -1)
+                            {
+                            $shebangFiles{$sourceFile} = $shebangStrings{$shebangString};
+                            return $languages{$shebangStrings{$shebangString}};
+                            };
+                        };
+
+                    $shebangFiles{$sourceFile} = undef;
+                    return undef;
+                    };
+                };
+            }
+
+        else # language name ne 'shebang script'
+            {  return $languages{$languageName};  };
+        }
+    else # !defined $language
+        {
+        return undef;
+        };
+    };
+
+
+#
+#   Function: OnMostUsedLanguageKnown
+#
+#   Called when the most used language is known.
+#
+sub OnMostUsedLanguageKnown
+    {
+    my $self = shift;
+
+    my $language = $languages{lc( NaturalDocs::Project->MostUsedLanguage() )};
+
+    if ($language)
+        {
+        if (!$languages{'text file'}->HasIgnoredPrefixes())
+            {  $languages{'text file'}->CopyIgnoredPrefixesOf($language);  };
+        if (!$languages{'text file'}->PackageSeparatorWasSet())
+            {  $languages{'text file'}->SetPackageSeparator($language->PackageSeparator());  };
+        if (!$languages{'text file'}->EnumValuesWasSet())
+            {  $languages{'text file'}->SetEnumValues($language->EnumValues());  };
+        };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Languages/ActionScript.pm b/docs/tool/Modules/NaturalDocs/Languages/ActionScript.pm
new file mode 100644
index 00000000..a55abaf2
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Languages/ActionScript.pm
@@ -0,0 +1,1473 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Languages::ActionScript
+#
+###############################################################################
+#
+#   A subclass to handle the language variations of Flash ActionScript.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Languages::ActionScript;
+
+use base 'NaturalDocs::Languages::Advanced';
+
+
+################################################################################
+# Group: Constants and Types
+
+
+#
+#   Constants: XML Tag Type
+#
+#   XML_OPENING_TAG - The tag is an opening one, such as <tag>.
+#   XML_CLOSING_TAG - The tag is a closing one, such as </tag>.
+#   XML_SELF_CONTAINED_TAG - The tag is self contained, such as <tag />.
+#
+use constant XML_OPENING_TAG => 1;
+use constant XML_CLOSING_TAG => 2;
+use constant XML_SELF_CONTAINED_TAG => 3;
+
+
+################################################################################
+# Group: Package Variables
+
+#
+#   hash: classModifiers
+#   An existence hash of all the acceptable class modifiers.  The keys are in all lowercase.
+#
+my %classModifiers = ( 'dynamic' => 1,
+                                   'intrinsic' => 1,
+                                   'final' => 1,
+                                   'internal' => 1,
+                                   'public' => 1 );
+
+#
+#   hash: memberModifiers
+#   An existence hash of all the acceptable class member modifiers.  The keys are in all lowercase.
+#
+my %memberModifiers = ( 'public' => 1,
+                                        'private' => 1,
+                                        'protected' => 1,
+                                        'static' => 1,
+                                        'internal' => 1,
+                                        'override' => 1 );
+
+
+#
+#   hash: declarationEnders
+#   An existence hash of all the tokens that can end a declaration.  This is important because statements don't require a semicolon
+#   to end.  The keys are in all lowercase.
+#
+my %declarationEnders = ( ';' => 1,
+                                        '}' => 1,
+                                        '{' => 1,
+                                        'public' => 1,
+                                        'private' => 1,
+                                        'protected' => 1,
+                                        'static' => 1,
+                                        'internal' => 1,
+                                        'dynamic' => 1,
+                                        'intrinsic' => 1,
+                                        'final' => 1,
+                                        'override' => 1,
+                                        'class' => 1,
+                                        'interface' => 1,
+                                        'var' => 1,
+                                        'function' => 1,
+                                        'const' => 1,
+                                        'namespace' => 1,
+                                        'import' => 1 );
+
+
+#
+#   var: isEscaped
+#   Whether the current file being parsed uses escapement.
+#
+my $isEscaped;
+
+
+
+################################################################################
+# Group: Interface Functions
+
+
+#
+#   Function: PackageSeparator
+#   Returns the package separator symbol.
+#
+sub PackageSeparator
+    {  return '.';  };
+
+
+#
+#   Function: EnumValues
+#   Returns the <EnumValuesType> that describes how the language handles enums.
+#
+sub EnumValues
+    {  return ::ENUM_GLOBAL();  };
+
+
+#
+#   Function: ParseParameterLine
+#   Parses a prototype parameter line and returns it as a <NaturalDocs::Languages::Prototype::Parameter> object.
+#
+sub ParseParameterLine #(line)
+    {
+    my ($self, $line) = @_;
+
+    if ($line =~ /^ ?\.\.\.\ (.+)$/)
+        {
+        # This puts them in the wrong fields as $1 should be the name and ... should be the type.  However, this is necessary
+        # because the order in the source is reversed from other parameter declarations and it's more important for the output
+        # to match the source.
+        return NaturalDocs::Languages::Prototype::Parameter->New($1, undef, '...', undef, undef, undef);
+        }
+    else
+        {  return $self->ParsePascalParameterLine($line);  };
+    };
+
+
+#
+#   Function: TypeBeforeParameter
+#   Returns whether the type appears before the parameter in prototypes.
+#
+sub TypeBeforeParameter
+    {  return 0;  };
+
+
+#
+#   Function: PreprocessFile
+#
+#   If the file is escaped, strips out all unescaped code.  Will translate any unescaped comments into comments surrounded by
+#   "\x1C\x1D\x1E\x1F" and "\x1F\x1E\x1D" characters, so chosen because they are the same character lengths as <!-- and -->
+#   and will not appear in normal code.
+#
+sub PreprocessFile
+    {
+    my ($self, $lines) = @_;
+
+    if (!$isEscaped)
+        {  return;  };
+
+    use constant MODE_UNESCAPED_REGULAR => 1;
+    use constant MODE_UNESCAPED_PI => 2;
+    use constant MODE_UNESCAPED_CDATA => 3;
+    use constant MODE_UNESCAPED_COMMENT => 4;
+    use constant MODE_ESCAPED_UNKNOWN_CDATA => 5;
+    use constant MODE_ESCAPED_CDATA => 6;
+    use constant MODE_ESCAPED_NO_CDATA => 7;
+
+    my $mode = MODE_UNESCAPED_REGULAR;
+
+    for (my $i = 0; $i < scalar @$lines; $i++)
+        {
+        my @tokens = split(/(<[ \t]*\/?[ \t]*mx:Script[^>]*>|<\?|\?>|<\!--|-->|<\!\[CDATA\[|\]\]\>)/, $lines->[$i]);
+        my $newLine;
+
+        foreach my $token (@tokens)
+            {
+            if ($mode == MODE_UNESCAPED_REGULAR)
+                {
+                if ($token eq '<?')
+                    {  $mode = MODE_UNESCAPED_PI;  }
+                elsif ($token eq '<![CDATA[')
+                    {  $mode = MODE_UNESCAPED_CDATA;  }
+                elsif ($token eq '<!--')
+                    {
+                    $mode = MODE_UNESCAPED_COMMENT;
+                    $newLine .= "\x1C\x1D\x1E\x1F";
+                    }
+                elsif ($token =~ /^<[ \t]*mx:Script/)
+                    {  $mode = MODE_ESCAPED_UNKNOWN_CDATA;  };
+                }
+
+            elsif ($mode == MODE_UNESCAPED_PI)
+                {
+                if ($token eq '?>')
+                    {  $mode = MODE_UNESCAPED_REGULAR;  };
+                }
+
+            elsif ($mode == MODE_UNESCAPED_CDATA)
+                {
+                if ($token eq ']]>')
+                    {  $mode = MODE_UNESCAPED_REGULAR;  };
+                }
+
+            elsif ($mode == MODE_UNESCAPED_COMMENT)
+                {
+                if ($token eq '-->')
+                    {
+                    $mode = MODE_UNESCAPED_REGULAR;
+                    $newLine .= "\x1F\x1E\x1D";
+                    }
+                else
+                    {  $newLine .= $token;  };
+                }
+
+            elsif ($mode == MODE_ESCAPED_UNKNOWN_CDATA)
+                {
+                if ($token eq '<![CDATA[')
+                    {  $mode = MODE_ESCAPED_CDATA;  }
+                elsif ($token =~ /^<[ \t]*\/[ \t]*mx:Script/)
+                    {
+                    $mode = MODE_UNESCAPED_REGULAR;
+                    $newLine .= '; ';
+                    }
+                elsif ($token !~ /^[ \t]*$/)
+                    {
+                    $mode = MODE_ESCAPED_NO_CDATA;
+                    $newLine .= $token;
+                    };
+                }
+
+            elsif ($mode == MODE_ESCAPED_CDATA)
+                {
+                if ($token eq ']]>')
+                    {
+                    $mode = MODE_UNESCAPED_REGULAR;
+                    $newLine .= '; ';
+                    }
+                else
+                    {  $newLine .= $token;  };
+                }
+
+            else #($mode == MODE_ESCAPED_NO_CDATA)
+                {
+                if ($token =~ /^<[ \t]*\/[ \t]*mx:Script/)
+                    {
+                    $mode = MODE_UNESCAPED_REGULAR;
+                    $newLine .= '; ';
+                    }
+                else
+                    {  $newLine .= $token;  };
+                };
+
+            };
+
+        $lines->[$i] = $newLine;
+        };
+    };
+
+
+#
+#   Function: ParseFile
+#
+#   Parses the passed source file, sending comments acceptable for documentation to <NaturalDocs::Parser->OnComment()>.
+#
+#   Parameters:
+#
+#       sourceFile - The <FileName> to parse.
+#       topicList - A reference to the list of <NaturalDocs::Parser::ParsedTopics> being built by the file.
+#
+#   Returns:
+#
+#       The array ( autoTopics, scopeRecord ).
+#
+#       autoTopics - An arrayref of automatically generated topics from the file, or undef if none.
+#       scopeRecord - An arrayref of <NaturalDocs::Languages::Advanced::ScopeChanges>, or undef if none.
+#
+sub ParseFile #(sourceFile, topicsList)
+    {
+    my ($self, $sourceFile, $topicsList) = @_;
+
+    # The \x1# comment symbols are inserted by PreprocessFile() to stand in for XML comments in escaped files.
+    my @parseParameters = ( [ '//' ], [ '/*', '*/', "\x1C\x1D\x1E\x1F", "\x1F\x1E\x1D" ], [ '///' ], [ '/**', '*/' ] );
+
+    my $extension = lc(NaturalDocs::File->ExtensionOf($sourceFile));
+    $isEscaped = ($extension eq 'mxml');
+
+    $self->ParseForCommentsAndTokens($sourceFile, @parseParameters);
+
+    my $tokens = $self->Tokens();
+    my $index = 0;
+    my $lineNumber = 1;
+
+    while ($index < scalar @$tokens)
+        {
+        if ($self->TryToSkipWhitespace(\$index, \$lineNumber) ||
+            $self->TryToGetImport(\$index, \$lineNumber) ||
+            $self->TryToGetClass(\$index, \$lineNumber) ||
+            $self->TryToGetFunction(\$index, \$lineNumber) ||
+            $self->TryToGetVariable(\$index, \$lineNumber) )
+            {
+            # The functions above will handle everything.
+            }
+
+        elsif ($tokens->[$index] eq '{')
+            {
+            $self->StartScope('}', $lineNumber, undef, undef, undef);
+            $index++;
+            }
+
+        elsif ($tokens->[$index] eq '}')
+            {
+            if ($self->ClosingScopeSymbol() eq '}')
+                {  $self->EndScope($lineNumber);  };
+
+            $index++;
+            }
+
+        else
+            {
+            $self->SkipToNextStatement(\$index, \$lineNumber);
+            };
+        };
+
+
+    # Don't need to keep these around.
+    $self->ClearTokens();
+
+
+    my $autoTopics = $self->AutoTopics();
+
+    my $scopeRecord = $self->ScopeRecord();
+    if (defined $scopeRecord && !scalar @$scopeRecord)
+        {  $scopeRecord = undef;  };
+
+    return ( $autoTopics, $scopeRecord );
+    };
+
+
+
+################################################################################
+# Group: Statement Parsing Functions
+# All functions here assume that the current position is at the beginning of a statement.
+#
+# Note for developers: I am well aware that the code in these functions do not check if we're past the end of the tokens as
+# often as it should.  We're making use of the fact that Perl will always return undef in these cases to keep the code simpler.
+
+
+#
+#   Function: TryToGetIdentifier
+#
+#   Determines whether the position is at an identifier, and if so, skips it and returns the complete identifier as a string.  Returns
+#   undef otherwise.
+#
+#   Parameters:
+#
+#       indexRef - A reference to the current token index.
+#       lineNumberRef - A reference to the current line number.
+#       allowStar - If set, allows the last identifier to be a star.
+#
+sub TryToGetIdentifier #(indexRef, lineNumberRef, allowStar)
+    {
+    my ($self, $indexRef, $lineNumberRef, $allowStar) = @_;
+    my $tokens = $self->Tokens();
+
+    my $index = $$indexRef;
+
+    use constant MODE_IDENTIFIER_START => 1;
+    use constant MODE_IN_IDENTIFIER => 2;
+    use constant MODE_AFTER_STAR => 3;
+
+    my $identifier;
+    my $mode = MODE_IDENTIFIER_START;
+
+    while ($index < scalar @$tokens)
+        {
+        if ($mode == MODE_IDENTIFIER_START)
+            {
+            if ($tokens->[$index] =~ /^[a-z\$\_]/i)
+                {
+                $identifier .= $tokens->[$index];
+                $index++;
+
+                $mode = MODE_IN_IDENTIFIER;
+                }
+            elsif ($allowStar && $tokens->[$index] eq '*')
+                {
+                $identifier .= '*';
+                $index++;
+
+                $mode = MODE_AFTER_STAR;
+                }
+            else
+                {  return undef;  };
+            }
+
+        elsif ($mode == MODE_IN_IDENTIFIER)
+            {
+            if ($tokens->[$index] eq '.')
+                {
+                $identifier .= '.';
+                $index++;
+
+                $mode = MODE_IDENTIFIER_START;
+                }
+            elsif ($tokens->[$index] =~ /^[a-z0-9\$\_]/i)
+                {
+                $identifier .= $tokens->[$index];
+                $index++;
+                }
+            else
+                {  last;  };
+            }
+
+        else #($mode == MODE_AFTER_STAR)
+            {
+            if ($tokens->[$index] =~ /^[a-z0-9\$\_\.]/i)
+                {  return undef;  }
+            else
+                {  last;  };
+            };
+        };
+
+    # We need to check again because we may have run out of tokens after a dot.
+    if ($mode != MODE_IDENTIFIER_START)
+        {
+        $$indexRef = $index;
+        return $identifier;
+        }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: TryToGetImport
+#
+#   Determines whether the position is at a import statement, and if so, adds it as a Using statement to the current scope, skips
+#   it, and returns true.
+#
+sub TryToGetImport #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $index = $$indexRef;
+    my $lineNumber = $$lineNumberRef;
+
+    if ($tokens->[$index] ne 'import')
+        {  return undef;  };
+
+    $index++;
+    $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+    my $identifier = $self->TryToGetIdentifier(\$index, \$lineNumber, 1);
+    if (!$identifier)
+        {  return undef;  };
+
+
+    # Currently we implement importing by stripping the last package level and treating it as a using.  So "import p1.p2.p3" makes
+    # p1.p2 the using path, which is over-tolerant but that's okay.  "import p1.p2.*" is treated the same way, but in this case it's
+    # not over-tolerant.  If there's no dot, there's no point to including it.
+
+    if (index($identifier, '.') != -1)
+        {
+        $identifier =~ s/\.[^\.]+$//;
+        $self->AddUsing( NaturalDocs::SymbolString->FromText($identifier) );
+        };
+
+    $$indexRef = $index;
+    $$lineNumberRef = $lineNumber;
+
+    return 1;
+    };
+
+
+#
+#   Function: TryToGetClass
+#
+#   Determines whether the position is at a class declaration statement, and if so, generates a topic for it, skips it, and
+#   returns true.
+#
+#   Supported Syntaxes:
+#
+#       - Classes
+#       - Interfaces
+#       - Classes and interfaces with _global
+#
+sub TryToGetClass #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $index = $$indexRef;
+    my $lineNumber = $$lineNumberRef;
+
+    my @modifiers;
+
+    while ($tokens->[$index] =~ /^[a-z]/i &&
+              exists $classModifiers{lc($tokens->[$index])} )
+        {
+        push @modifiers, lc($tokens->[$index]);
+        $index++;
+
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+        };
+
+    my $type;
+
+    if ($tokens->[$index] eq 'class' || $tokens->[$index] eq 'interface')
+        {
+        $type = $tokens->[$index];
+
+        $index++;
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+        }
+    else
+        {  return undef;  };
+
+    my $className = $self->TryToGetIdentifier(\$index, \$lineNumber);
+
+    if (!$className)
+        {  return undef;  };
+
+    $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+    my @parents;
+
+    if ($tokens->[$index] eq 'extends')
+        {
+        $index++;
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+        my $parent = $self->TryToGetIdentifier(\$index, \$lineNumber);
+        if (!$parent)
+            {  return undef;  };
+
+        push @parents, $parent;
+
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+        };
+
+    if ($type eq 'class' && $tokens->[$index] eq 'implements')
+        {
+        $index++;
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+        for (;;)
+            {
+            my $parent = $self->TryToGetIdentifier(\$index, \$lineNumber);
+            if (!$parent)
+                {  return undef;  };
+
+            push @parents, $parent;
+
+            $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+            if ($tokens->[$index] ne ',')
+                {  last;  }
+            else
+                {
+                $index++;
+                $self->TryToSkipWhitespace(\$index, \$lineNumber);
+                };
+            };
+        };
+
+    if ($tokens->[$index] ne '{')
+        {  return undef;  };
+
+    $index++;
+
+
+    # If we made it this far, we have a valid class declaration.
+
+    my $topicType;
+
+    if ($type eq 'interface')
+        {  $topicType = ::TOPIC_INTERFACE();  }
+    else
+        {  $topicType = ::TOPIC_CLASS();  };
+
+    $className =~ s/^_global.//;
+
+    my $autoTopic = NaturalDocs::Parser::ParsedTopic->New($topicType, $className,
+                                                                                         undef, $self->CurrentUsing(),
+                                                                                         undef,
+                                                                                         undef, undef, $$lineNumberRef);
+
+    $self->AddAutoTopic($autoTopic);
+    NaturalDocs::Parser->OnClass($autoTopic->Package());
+
+    foreach my $parent (@parents)
+        {
+        NaturalDocs::Parser->OnClassParent($autoTopic->Package(), NaturalDocs::SymbolString->FromText($parent),
+                                                               undef, $self->CurrentUsing(), ::RESOLVE_ABSOLUTE());
+        };
+
+    $self->StartScope('}', $lineNumber, $autoTopic->Package());
+
+    $$indexRef = $index;
+    $$lineNumberRef = $lineNumber;
+
+    return 1;
+    };
+
+
+#
+#   Function: TryToGetFunction
+#
+#   Determines if the position is on a function declaration, and if so, generates a topic for it, skips it, and returns true.
+#
+#   Supported Syntaxes:
+#
+#       - Functions
+#       - Constructors
+#       - Properties
+#       - Functions with _global
+#       - Functions with namespaces
+#
+sub TryToGetFunction #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $index = $$indexRef;
+    my $lineNumber = $$lineNumberRef;
+
+    my $startIndex = $index;
+    my $startLine = $lineNumber;
+
+    my @modifiers;
+    my $namespace;
+
+    while ($tokens->[$index] =~ /^[a-z]/i)
+        {
+        if ($tokens->[$index] eq 'function')
+            {  last;  }
+
+        elsif (exists $memberModifiers{lc($tokens->[$index])})
+            {
+            push @modifiers, lc($tokens->[$index]);
+            $index++;
+
+            $self->TryToSkipWhitespace(\$index, \$lineNumber);
+            }
+
+        elsif (!$namespace)
+            {
+            do
+                {
+                $namespace .= $tokens->[$index];
+                $index++;
+                }
+            while ($tokens->[$index] =~ /^[a-z0-9_]/i);
+
+            $self->TryToSkipWhitespace(\$index, \$lineNumber);
+            }
+
+        else
+            {  last;  };
+        };
+
+    if ($tokens->[$index] ne 'function')
+        {  return undef;  };
+    $index++;
+
+    $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+    my $type;
+
+    if ($tokens->[$index] eq 'get' || $tokens->[$index] eq 'set')
+        {
+        # This can either be a property ("function get Something()") or a function name ("function get()").
+
+        my $nextIndex = $index;
+        my $nextLineNumber = $lineNumber;
+
+        $nextIndex++;
+        $self->TryToSkipWhitespace(\$nextIndex, \$nextLineNumber);
+
+        if ($tokens->[$nextIndex] eq '(')
+            {
+            $type = ::TOPIC_FUNCTION();
+            # Ignore the movement and let the code ahead pick it up as the name.
+            }
+        else
+            {
+            $type = ::TOPIC_PROPERTY();
+            $index = $nextIndex;
+            $lineNumber = $nextLineNumber;
+            };
+        }
+    else
+        {  $type = ::TOPIC_FUNCTION();  };
+
+    my $name = $self->TryToGetIdentifier(\$index, \$lineNumber);
+    if (!$name)
+        {  return undef;  };
+
+    $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+    if ($tokens->[$index] ne '(')
+        {  return undef;  };
+
+    $index++;
+    $self->GenericSkipUntilAfter(\$index, \$lineNumber, ')');
+
+    $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+    if ($tokens->[$index] eq ':')
+        {
+        $index++;
+
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+        $self->TryToGetIdentifier(\$index, \$lineNumber, 1);
+
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+        };
+
+
+    my $prototype = $self->NormalizePrototype( $self->CreateString($startIndex, $index) );
+
+    if ($tokens->[$index] eq '{')
+        {  $self->GenericSkip(\$index, \$lineNumber);  }
+    elsif (!exists $declarationEnders{$tokens->[$index]})
+        {  return undef;  };
+
+
+    my $scope = $self->CurrentScope();
+
+    if ($name =~ s/^_global.//)
+        {  $scope = undef;  };
+    if ($namespace)
+        {  $scope = NaturalDocs::SymbolString->Join($scope, $namespace);  };
+
+    $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New($type, $name,
+                                                                                              $scope, $self->CurrentUsing(),
+                                                                                              $prototype,
+                                                                                              undef, undef, $startLine));
+
+
+    # We succeeded if we got this far.
+
+    $$indexRef = $index;
+    $$lineNumberRef = $lineNumber;
+
+    return 1;
+    };
+
+
+#
+#   Function: TryToGetVariable
+#
+#   Determines if the position is on a variable declaration statement, and if so, generates a topic for each variable, skips the
+#   statement, and returns true.
+#
+#   Supported Syntaxes:
+#
+#       - Variables
+#       - Variables with _global
+#       - Variables with type * (untyped)
+#       - Constants
+#       - Variables and constants with namespaces
+#
+sub TryToGetVariable #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $index = $$indexRef;
+    my $lineNumber = $$lineNumberRef;
+
+    my $startIndex = $index;
+    my $startLine = $lineNumber;
+
+    my @modifiers;
+    my $namespace;
+
+    while ($tokens->[$index] =~ /^[a-z]/i)
+        {
+        if ($tokens->[$index] eq 'var' || $tokens->[$index] eq 'const')
+            {  last;  }
+
+        elsif (exists $memberModifiers{lc($tokens->[$index])})
+            {
+            push @modifiers, lc($tokens->[$index]);
+            $index++;
+
+            $self->TryToSkipWhitespace(\$index, \$lineNumber);
+            }
+
+        elsif (!$namespace)
+            {
+            do
+                {
+                $namespace .= $tokens->[$index];
+                $index++;
+                }
+            while ($tokens->[$index] =~ /^[a-z0-9_]/i);
+
+            $self->TryToSkipWhitespace(\$index, \$lineNumber);
+            }
+
+        else
+            {  last;  };
+        };
+
+    my $type;
+
+    if ($tokens->[$index] eq 'var')
+        {  $type = ::TOPIC_VARIABLE();  }
+    elsif ($tokens->[$index] eq 'const')
+        {  $type = ::TOPIC_CONSTANT();  }
+    else
+        {  return undef;  };
+    $index++;
+
+    $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+    my $endTypeIndex = $index;
+    my @names;
+    my @types;
+
+    for (;;)
+        {
+        my $name = $self->TryToGetIdentifier(\$index, \$lineNumber);
+        if (!$name)
+            {  return undef;  };
+
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+        my $type;
+
+        if ($tokens->[$index] eq ':')
+            {
+            $index++;
+            $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+            $type = ': ' . $self->TryToGetIdentifier(\$index, \$lineNumber, 1);
+
+            $self->TryToSkipWhitespace(\$index, \$lineNumber);
+            };
+
+        if ($tokens->[$index] eq '=')
+            {
+            do
+                {
+                $self->GenericSkip(\$index, \$lineNumber);
+                }
+            while ($tokens->[$index] ne ',' && !exists $declarationEnders{$tokens->[$index]} && $index < scalar @$tokens);
+            };
+
+        push @names, $name;
+        push @types, $type;
+
+        if ($tokens->[$index] eq ',')
+            {
+            $index++;
+            $self->TryToSkipWhitespace(\$index, \$lineNumber);
+            }
+        elsif (exists $declarationEnders{$tokens->[$index]})
+            {  last;  }
+        else
+            {  return undef;  };
+        };
+
+
+    # We succeeded if we got this far.
+
+    my $prototypePrefix = $self->CreateString($startIndex, $endTypeIndex);
+
+    for (my $i = 0; $i < scalar @names; $i++)
+        {
+        my $prototype = $self->NormalizePrototype( $prototypePrefix . ' ' . $names[$i] . $types[$i]);
+        my $scope = $self->CurrentScope();
+
+        if ($names[$i] =~ s/^_global.//)
+            {  $scope = undef;  };
+        if ($namespace)
+            {  $scope = NaturalDocs::SymbolString->Join($scope, $namespace);  };
+
+        $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New($type, $names[$i],
+                                                                                                  $scope, $self->CurrentUsing(),
+                                                                                                  $prototype,
+                                                                                                  undef, undef, $startLine));
+        };
+
+    $$indexRef = $index;
+    $$lineNumberRef = $lineNumber;
+
+    return 1;
+    };
+
+
+
+################################################################################
+# Group: Low Level Parsing Functions
+
+
+#
+#   Function: GenericSkip
+#
+#   Advances the position one place through general code.
+#
+#   - If the position is on a string, it will skip it completely.
+#   - If the position is on an opening symbol, it will skip until the past the closing symbol.
+#   - If the position is on whitespace (including comments), it will skip it completely.
+#   - Otherwise it skips one token.
+#
+#   Parameters:
+#
+#       indexRef - A reference to the current index.
+#       lineNumberRef - A reference to the current line number.
+#
+sub GenericSkip #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    # We can ignore the scope stack because we're just skipping everything without parsing, and we need recursion anyway.
+    if ($tokens->[$$indexRef] eq '{')
+        {
+        $$indexRef++;
+        $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}');
+        }
+    elsif ($tokens->[$$indexRef] eq '(')
+        {
+        $$indexRef++;
+        $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ')');
+        }
+    elsif ($tokens->[$$indexRef] eq '[')
+        {
+        $$indexRef++;
+        $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ']');
+        }
+
+    elsif ($self->TryToSkipWhitespace($indexRef, $lineNumberRef) ||
+            $self->TryToSkipString($indexRef, $lineNumberRef) ||
+            $self->TryToSkipRegExp($indexRef, $lineNumberRef) ||
+            $self->TryToSkipXML($indexRef, $lineNumberRef) )
+        {
+        }
+
+    else
+        {  $$indexRef++;  };
+    };
+
+
+#
+#   Function: GenericSkipUntilAfter
+#
+#   Advances the position via <GenericSkip()> until a specific token is reached and passed.
+#
+sub GenericSkipUntilAfter #(indexRef, lineNumberRef, token)
+    {
+    my ($self, $indexRef, $lineNumberRef, $token) = @_;
+    my $tokens = $self->Tokens();
+
+    while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] ne $token)
+        {  $self->GenericSkip($indexRef, $lineNumberRef);  };
+
+    if ($tokens->[$$indexRef] eq "\n")
+        {  $$lineNumberRef++;  };
+    $$indexRef++;
+    };
+
+
+#
+#   Function: IndiscriminateSkipUntilAfterSequence
+#
+#   Advances the position indiscriminately until a specific token sequence is reached and passed.
+#
+sub IndiscriminateSkipUntilAfterSequence #(indexRef, lineNumberRef, token, token, ...)
+    {
+    my ($self, $indexRef, $lineNumberRef, @sequence) = @_;
+    my $tokens = $self->Tokens();
+
+    while ($$indexRef < scalar @$tokens && !$self->IsAtSequence($$indexRef, @sequence))
+        {
+        if ($tokens->[$$indexRef] eq "\n")
+            {  $$lineNumberRef++;  };
+        $$indexRef++;
+        };
+
+    if ($self->IsAtSequence($$indexRef, @sequence))
+        {
+        $$indexRef += scalar @sequence;
+        foreach my $token (@sequence)
+            {
+            if ($token eq "\n")
+                {  $$lineNumberRef++;  };
+            };
+        };
+    };
+
+
+#
+#   Function: SkipToNextStatement
+#
+#   Advances the position via <GenericSkip()> until the next statement, which is defined as anything in <declarationEnders> not
+#   appearing in brackets or strings.  It will always advance at least one token.
+#
+sub SkipToNextStatement #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    if ($tokens->[$$indexRef] eq ';')
+        {  $$indexRef++;  }
+
+    else
+        {
+        do
+            {
+            $self->GenericSkip($indexRef, $lineNumberRef);
+            }
+        while ( $$indexRef < scalar @$tokens &&
+                  !exists $declarationEnders{$tokens->[$$indexRef]} );
+        };
+    };
+
+
+#
+#   Function: TryToSkipRegExp
+#   If the current position is on a regular expression, skip past it and return true.
+#
+sub TryToSkipRegExp #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    if ($tokens->[$$indexRef] eq '/')
+        {
+        # A slash can either start a regular expression or be a divide symbol.  Skip backwards to see what the previous symbol is.
+        my $index = $$indexRef - 1;
+
+        while ($index >= 0 && $tokens->[$index] =~ /^(?: |\t|\n)/)
+            {  $index--;  };
+
+        if ($index < 0 || $tokens->[$index] !~ /^\=\(\[\,]/)
+            {  return 0;  };
+
+        $$indexRef++;
+
+        while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] ne '/')
+            {
+            if ($tokens->[$$indexRef] eq '\\')
+                {  $$indexRef += 2;  }
+            elsif ($tokens->[$$indexRef] eq "\n")
+                {
+                $$indexRef++;
+                $$lineNumberRef++;
+                }
+            else
+                {  $$indexRef++;  }
+            };
+
+        if ($$indexRef < scalar @$tokens)
+            {
+            $$indexRef++;
+
+            if ($tokens->[$$indexRef] =~ /^[gimsx]+$/i)
+                {  $$indexRef++;  };
+            };
+
+        return 1;
+        }
+    else
+        {  return 0;  };
+    };
+
+
+#
+#   Function: TryToSkipXML
+#   If the current position is on an XML literal, skip past it and return true.
+#
+sub TryToSkipXML #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    if ($tokens->[$$indexRef] eq '<')
+        {
+        # A < can either start an XML literal or be a comparison or shift operator.  First check the next character for << or <=.
+
+        my $index = $$indexRef + 1;
+
+        while ($index < scalar @$tokens && $tokens->[$index] =~ /^[\=\<]$/)
+            {  return 0;  };
+
+
+        # Next try the previous character.
+
+        $index = $$indexRef - 1;
+
+        while ($index >= 0 && $tokens->[$index] =~ /^[ |\t|\n]/)
+            {  $index--;  };
+
+        if ($index < 0 || $tokens->[$index] !~ /^[\=\(\[\,\>]/)
+            {  return 0;  };
+        }
+    else
+        {  return 0;  };
+
+
+    # Only handle the tag here if it's not an irregular XML section.
+    if (!$self->TryToSkipIrregularXML($indexRef, $lineNumberRef))
+        {
+        my @tagStack;
+
+        my ($tagType, $tagIdentifier) = $self->GetAndSkipXMLTag($indexRef, $lineNumberRef);
+        if ($tagType == XML_OPENING_TAG)
+            {  push @tagStack, $tagIdentifier;  };
+
+        while (scalar @tagStack && $$indexRef < scalar @$tokens)
+            {
+            $self->SkipToNextXMLTag($indexRef, $lineNumberRef);
+            ($tagType, $tagIdentifier) = $self->GetAndSkipXMLTag($indexRef, $lineNumberRef);
+
+            if ($tagType == XML_OPENING_TAG)
+                {  push @tagStack, $tagIdentifier;  }
+            elsif ($tagType == XML_CLOSING_TAG && $tagIdentifier eq $tagStack[-1])
+                {  pop @tagStack;  };
+            };
+        };
+
+
+    return 1;
+    };
+
+
+#
+#   Function: TryToSkipIrregularXML
+#
+#   If the current position is on an irregular XML tag, skip past it and return true.  Irregular XML tags are defined as
+#
+#       CDATA - <![CDATA[ ... ]]>
+#       Comments - <!-- ... -->
+#       PI - <? ... ?>
+#
+sub TryToSkipIrregularXML #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+
+    if ($self->IsAtSequence($$indexRef, '<', '!', '[', 'CDATA', '['))
+        {
+        $$indexRef += 5;
+        $self->IndiscriminateSkipUntilAfterSequence($indexRef, $lineNumberRef, ']', ']', '>');
+        return 1;
+        }
+
+    elsif ($self->IsAtSequence($$indexRef, '<', '!', '-', '-'))
+        {
+        $$indexRef += 4;
+        $self->IndiscriminateSkipUntilAfterSequence($indexRef, $lineNumberRef, '-', '-', '>');
+        return 1;
+        }
+
+    elsif ($self->IsAtSequence($$indexRef, '<', '?'))
+        {
+        $$indexRef += 2;
+        $self->IndiscriminateSkipUntilAfterSequence($indexRef, $lineNumberRef, '?', '>');
+        return 1;
+        }
+
+    else
+        {  return 0;  };
+    };
+
+
+#
+#   Function: GetAndSkipXMLTag
+#
+#   Processes the XML tag at the current position, moves beyond it, and returns information about it.  Assumes the position is on
+#   the opening angle bracket of the tag and the tag is a normal XML tag, not one of the ones handled by
+#   <TryToSkipIrregularXML()>.
+#
+#   Parameters:
+#
+#       indexRef - A reference to the index of the position of the opening angle bracket.
+#       lineNumberRef - A reference to the line number of the position of the opening angle bracket.
+#
+#   Returns:
+#
+#       The array ( tagType, name ).
+#
+#       tagType - One of the <XML Tag Type> constants.
+#       identifier - The identifier of the tag.  If it's an empty tag (<> or </>), this will be "(anonymous)".
+#
+sub GetAndSkipXMLTag #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    if ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] ne '<')
+        {  die "Tried to call GetXMLTag when the position isn't on an opening bracket.";  };
+
+    # Get the anonymous ones out of the way so we don't have to worry about them below, since they're rather exceptional.
+
+    if ($self->IsAtSequence($$indexRef, '<', '>'))
+        {
+        $$indexRef += 2;
+        return ( XML_OPENING_TAG, '(anonymous)' );
+        }
+    elsif ($self->IsAtSequence($$indexRef, '<', '/', '>'))
+        {
+        $$indexRef += 3;
+        return ( XML_CLOSING_TAG, '(anonymous)' );
+        };
+
+
+    # Grab the identifier.
+
+    my $tagType = XML_OPENING_TAG;
+    my $identifier;
+
+    $$indexRef++;
+
+    if ($tokens->[$$indexRef] eq '/')
+        {
+        $$indexRef++;
+        $tagType = XML_CLOSING_TAG;
+        };
+
+    $self->TryToSkipXMLWhitespace($indexRef, $lineNumberRef);
+
+
+    # The identifier could be a native expression in braces.
+
+    if ($tokens->[$$indexRef] eq '{')
+        {
+        my $startOfIdentifier = $$indexRef;
+
+        $$indexRef++;
+        $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}');
+
+        $identifier = $self->CreateString($startOfIdentifier, $$indexRef);
+        }
+
+
+    # Otherwise just grab content until whitespace or the end of the tag.
+
+    else
+        {
+        while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] !~ /^[\/\>\ \t]$/)
+            {
+            $identifier .= $tokens->[$$indexRef];
+            $$indexRef++;
+            };
+        };
+
+
+    # Skip to the end of the tag.
+
+    while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] !~ /^[\/\>]$/)
+        {
+        if ($tokens->[$$indexRef] eq '{')
+            {
+            $$indexRef++;
+            $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}');
+            }
+
+        elsif ($self->TryToSkipXMLWhitespace($indexRef, $lineNumberRef))
+            {  }
+
+        # We don't need to do special handling for attribute quotes or anything like that because there's no backslashing in
+        # XML.  It's all handled with entity characters.
+        else
+            {  $$indexRef++;  };
+        };
+
+
+    if ($tokens->[$$indexRef] eq '/')
+        {
+        if ($tagType == XML_OPENING_TAG)
+            {  $tagType = XML_SELF_CONTAINED_TAG;  };
+
+        $$indexRef++;
+        };
+
+    if ($tokens->[$$indexRef] eq '>')
+        {  $$indexRef++;  };
+
+    if (!$identifier)
+        {  $identifier = '(anonymous)';  };
+
+
+    return ( $tagType, $identifier );
+    };
+
+
+#
+#   Function: SkipToNextXMLTag
+#   Skips to the next normal XML tag.  It will not stop at elements handled by <TryToSkipIrregularXML()>.  Note that if the
+#   position is already at an XML tag, it will not move.
+#
+sub SkipToNextXMLTag #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    while ($$indexRef < scalar @$tokens)
+        {
+        if ($tokens->[$$indexRef] eq '{')
+            {
+            $$indexRef++;
+            $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}');
+            }
+
+        elsif ($self->TryToSkipIrregularXML($indexRef, $lineNumberRef))
+            {  }
+
+        elsif ($tokens->[$$indexRef] eq '<')
+            {  last;  }
+
+        else
+            {
+            if ($tokens->[$$indexRef] eq "\n")
+                {  $$lineNumberRef++;  };
+
+            $$indexRef++;
+            };
+        };
+    };
+
+
+#
+#   Function: TryToSkipXMLWhitespace
+#   If the current position is on XML whitespace, skip past it and return true.
+#
+sub TryToSkipXMLWhitespace #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $result;
+
+    while ($$indexRef < scalar @$tokens)
+        {
+        if ($tokens->[$$indexRef] =~ /^[ \t]/)
+            {
+            $$indexRef++;
+            $result = 1;
+            }
+        elsif ($tokens->[$$indexRef] eq "\n")
+            {
+            $$indexRef++;
+            $$lineNumberRef++;
+            $result = 1;
+            }
+        else
+            {  last;  };
+        };
+
+    return $result;
+    };
+
+
+#
+#   Function: TryToSkipString
+#   If the current position is on a string delimiter, skip past the string and return true.
+#
+#   Parameters:
+#
+#       indexRef - A reference to the index of the position to start at.
+#       lineNumberRef - A reference to the line number of the position.
+#
+#   Returns:
+#
+#       Whether the position was at a string.
+#
+#   Syntax Support:
+#
+#       - Supports quotes and apostrophes.
+#
+sub TryToSkipString #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+
+    return ($self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '\'') ||
+               $self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '"') );
+    };
+
+
+#
+#   Function: TryToSkipWhitespace
+#   If the current position is on a whitespace token, a line break token, or a comment, it skips them and returns true.  If there are
+#   a number of these in a row, it skips them all.
+#
+sub TryToSkipWhitespace #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $result;
+
+    while ($$indexRef < scalar @$tokens)
+        {
+        if ($tokens->[$$indexRef] =~ /^[ \t]/)
+            {
+            $$indexRef++;
+            $result = 1;
+            }
+        elsif ($tokens->[$$indexRef] eq "\n")
+            {
+            $$indexRef++;
+            $$lineNumberRef++;
+            $result = 1;
+            }
+        elsif ($self->TryToSkipComment($indexRef, $lineNumberRef))
+            {
+            $result = 1;
+            }
+        else
+            {  last;  };
+        };
+
+    return $result;
+    };
+
+
+#
+#   Function: TryToSkipComment
+#   If the current position is on a comment, skip past it and return true.
+#
+sub TryToSkipComment #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+
+    return ( $self->TryToSkipLineComment($indexRef, $lineNumberRef) ||
+                $self->TryToSkipMultilineComment($indexRef, $lineNumberRef) );
+    };
+
+
+#
+#   Function: TryToSkipLineComment
+#   If the current position is on a line comment symbol, skip past it and return true.
+#
+sub TryToSkipLineComment #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    if ($tokens->[$$indexRef] eq '/' && $tokens->[$$indexRef+1] eq '/')
+        {
+        $self->SkipRestOfLine($indexRef, $lineNumberRef);
+        return 1;
+        }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: TryToSkipMultilineComment
+#   If the current position is on an opening comment symbol, skip past it and return true.
+#
+sub TryToSkipMultilineComment #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    if ($tokens->[$$indexRef] eq '/' && $tokens->[$$indexRef+1] eq '*')
+        {
+        $self->SkipUntilAfter($indexRef, $lineNumberRef, '*', '/');
+        return 1;
+        }
+    else
+        {  return undef;  };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Languages/Ada.pm b/docs/tool/Modules/NaturalDocs/Languages/Ada.pm
new file mode 100644
index 00000000..d7369ac6
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Languages/Ada.pm
@@ -0,0 +1,38 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Languages::Ada
+#
+###############################################################################
+#
+#   A subclass to handle the language variations of Ada
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Languages::Ada;
+
+use base 'NaturalDocs::Languages::Simple';
+
+
+#
+#   Function: ParseParameterLine
+#   Overridden because Ada uses Pascal-style parameters
+#
+sub ParseParameterLine #(...)
+    {
+    my ($self, @params) = @_;
+    return $self->SUPER::ParsePascalParameterLine(@params);
+    };
+
+sub TypeBeforeParameter
+    {
+    return 0;
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Languages/Advanced.pm b/docs/tool/Modules/NaturalDocs/Languages/Advanced.pm
new file mode 100644
index 00000000..8ae27bfc
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Languages/Advanced.pm
@@ -0,0 +1,828 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Languages::Advanced
+#
+###############################################################################
+#
+#   The base class for all languages that have full support in Natural Docs.  Each one will have a custom parser capable
+#   of documenting undocumented aspects of the code.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+use NaturalDocs::Languages::Advanced::Scope;
+use NaturalDocs::Languages::Advanced::ScopeChange;
+
+package NaturalDocs::Languages::Advanced;
+
+use base 'NaturalDocs::Languages::Base';
+
+
+#############################################################################
+# Group: Implementation
+
+#
+#   Constants: Members
+#
+#   The class is implemented as a blessed arrayref.  The following constants are used as indexes.
+#
+#   TOKENS - An arrayref of tokens used in all the <Parsing Functions>.
+#   SCOPE_STACK - An arrayref of <NaturalDocs::Languages::Advanced::Scope> objects serving as a scope stack for parsing.
+#                            There will always be one available, with a symbol of undef, for the top level.
+#   SCOPE_RECORD - An arrayref of <NaturalDocs::Languages::Advanced::ScopeChange> objects, as generated by the scope
+#                              stack.  If there is more than one change per line, only the last is stored.
+#   AUTO_TOPICS - An arrayref of <NaturalDocs::Parser::ParsedTopics> generated automatically from the code.
+#
+use NaturalDocs::DefineMembers 'TOKENS', 'SCOPE_STACK', 'SCOPE_RECORD', 'AUTO_TOPICS';
+
+
+#############################################################################
+# Group: Functions
+
+#
+#   Function: New
+#
+#   Creates and returns a new object.
+#
+#   Parameters:
+#
+#       name - The name of the language.
+#
+sub New #(name)
+    {
+    my ($package, @parameters) = @_;
+
+    my $object = $package->SUPER::New(@parameters);
+    $object->[TOKENS] = undef;
+    $object->[SCOPE_STACK] = undef;
+    $object->[SCOPE_RECORD] = undef;
+
+    return $object;
+    };
+
+
+# Function: Tokens
+# Returns the tokens found by <ParseForCommentsAndTokens()>.
+sub Tokens
+    {  return $_[0]->[TOKENS];  };
+
+# Function: SetTokens
+# Replaces the tokens.
+sub SetTokens #(tokens)
+    {  $_[0]->[TOKENS] = $_[1];  };
+
+# Function: ClearTokens
+#  Resets the token list.  You may want to do this after parsing is over to save memory.
+sub ClearTokens
+    {  $_[0]->[TOKENS] = undef;  };
+
+# Function: AutoTopics
+# Returns the arrayref of automatically generated topics, or undef if none.
+sub AutoTopics
+    {  return $_[0]->[AUTO_TOPICS];  };
+
+# Function: AddAutoTopic
+# Adds a <NaturalDocs::Parser::ParsedTopic> to <AutoTopics()>.
+sub AddAutoTopic #(topic)
+    {
+    my ($self, $topic) = @_;
+    if (!defined $self->[AUTO_TOPICS])
+        {  $self->[AUTO_TOPICS] = [ ];  };
+    push @{$self->[AUTO_TOPICS]}, $topic;
+    };
+
+# Function: ClearAutoTopics
+# Resets the automatic topic list.  Not necessary if you call <ParseForCommentsAndTokens()>.
+sub ClearAutoTopics
+    {  $_[0]->[AUTO_TOPICS] = undef;  };
+
+# Function: ScopeRecord
+# Returns an arrayref of <NaturalDocs::Languages::Advanced::ScopeChange> objects describing how and when the scope
+# changed thoughout the file.  There will always be at least one entry, which will be for line 1 and undef as the scope.
+sub ScopeRecord
+    {  return $_[0]->[SCOPE_RECORD];  };
+
+
+
+###############################################################################
+#
+#   Group: Parsing Functions
+#
+#   These functions are good general language building blocks.  Use them to create your language-specific parser.
+#
+#   All functions work on <Tokens()> and assume it is set by <ParseForCommentsAndTokens()>.
+#
+
+
+#
+#   Function: ParseForCommentsAndTokens
+#
+#   Loads the passed file, sends all appropriate comments to <NaturalDocs::Parser->OnComment()>, and breaks the rest into
+#   an arrayref of tokens.  Tokens are defined as
+#
+#   - All consecutive alphanumeric and underscore characters.
+#   - All consecutive whitespace.
+#   - A single line break.  It will always be "\n"; you don't have to worry about platform differences.
+#   - A single character not included above, which is usually a symbol.  Multiple consecutive ones each get their own token.
+#
+#   The result will be placed in <Tokens()>.
+#
+#   Parameters:
+#
+#       sourceFile - The source <FileName> to load and parse.
+#       lineCommentSymbols - An arrayref of symbols that designate line comments, or undef if none.
+#       blockCommentSymbols - An arrayref of symbol pairs that designate multiline comments, or undef if none.  Symbol pairs are
+#                                            designated as two consecutive array entries, the opening symbol appearing first.
+#       javadocLineCommentSymbols - An arrayref of symbols that designate the start of a JavaDoc comment, or undef if none.
+#       javadocBlockCommentSymbols - An arrayref of symbol pairs that designate multiline JavaDoc comments, or undef if none.
+#
+#   Notes:
+#
+#       - This function automatically calls <ClearAutoTopics()> and <ClearScopeStack()>.  You only need to call those functions
+#         manually if you override this one.
+#       - To save parsing time, all comment lines sent to <NaturalDocs::Parser->OnComment()> will be replaced with blank lines
+#         in <Tokens()>.  It's all the same to most languages.
+#
+sub ParseForCommentsAndTokens #(FileName sourceFile, string[] lineCommentSymbols, string[] blockCommentSymbols, string[] javadocLineCommentSymbols, string[] javadocBlockCommentSymbols)
+    {
+    my ($self, $sourceFile, $lineCommentSymbols, $blockCommentSymbols,
+           $javadocLineCommentSymbols, $javadocBlockCommentSymbols) = @_;
+
+    open(SOURCEFILEHANDLE, '<' . $sourceFile)
+        or die "Couldn't open input file " . $sourceFile . "\n";
+
+    my $tokens = [ ];
+    $self->SetTokens($tokens);
+
+    # For convenience.
+    $self->ClearAutoTopics();
+    $self->ClearScopeStack();
+
+
+    # Load and preprocess the file
+
+    my @lines;
+    my $line = <SOURCEFILEHANDLE>;
+
+    # On the very first line, remove a Unicode BOM if present.  Information on it available at:
+    # http://www.unicode.org/faq/utf_bom.html#BOM
+    $line =~ s/^\xEF\xBB\xBF//;
+
+    while (defined $line)
+        {
+        ::XChomp(\$line);
+        push @lines, $line;
+
+        $line = <SOURCEFILEHANDLE>;
+        };
+
+    close(SOURCEFILEHANDLE);
+
+    $self->PreprocessFile(\@lines);
+
+
+    # Go through the file
+
+    my $lineIndex = 0;
+
+    while ($lineIndex < scalar @lines)
+        {
+        $line = $lines[$lineIndex];
+
+        my @commentLines;
+        my $commentLineNumber;
+        my $isJavaDoc;
+        my $closingSymbol;
+
+
+        # Retrieve single line comments.  This leaves $lineIndex at the next line.
+
+        if ( ($isJavaDoc = $self->StripOpeningJavaDocSymbols(\$line, $javadocLineCommentSymbols)) ||
+              $self->StripOpeningSymbols(\$line, $lineCommentSymbols))
+            {
+            $commentLineNumber = $lineIndex + 1;
+
+            do
+                {
+                push @commentLines, $line;
+                push @$tokens, "\n";
+
+                $lineIndex++;
+
+                if ($lineIndex >= scalar @lines)
+                    {  goto EndDo;  };
+
+                $line = $lines[$lineIndex];
+                }
+            while ($self->StripOpeningSymbols(\$line, $lineCommentSymbols));
+
+            EndDo:  # I hate Perl sometimes.
+            }
+
+
+        # Retrieve multiline comments.  This leaves $lineIndex at the next line.
+
+        elsif ( ($isJavaDoc = $self->StripOpeningJavaDocBlockSymbols(\$line, $javadocBlockCommentSymbols)) ||
+                 ($closingSymbol = $self->StripOpeningBlockSymbols(\$line, $blockCommentSymbols)) )
+            {
+            $commentLineNumber = $lineIndex + 1;
+
+            if ($isJavaDoc)
+                {  $closingSymbol = $isJavaDoc;  };
+
+            # Note that it is possible for a multiline comment to start correctly but not end so.  We want those comments to stay in
+            # the code.  For example, look at this prototype with this splint annotation:
+            #
+            # int get_array(integer_t id,
+            #                    /*@out@*/ array_t array);
+            #
+            # The annotation starts correctly but doesn't end so because it is followed by code on the same line.
+
+            my ($lineRemainder, $isMultiLine);
+
+            for (;;)
+                {
+                $lineRemainder = $self->StripClosingSymbol(\$line, $closingSymbol);
+
+                push @commentLines, $line;
+
+                #  If we found an end comment symbol...
+                if (defined $lineRemainder)
+                    {  last;  };
+
+                push @$tokens, "\n";
+                $lineIndex++;
+                $isMultiLine = 1;
+
+                if ($lineIndex >= scalar @lines)
+                    {  last;  };
+
+                $line = $lines[$lineIndex];
+                };
+
+            if ($lineRemainder !~ /^[ \t]*$/)
+                {
+                # If there was something past the closing symbol this wasn't an acceptable comment.
+
+                if ($isMultiLine)
+                    {  $self->TokenizeLine($lineRemainder);  }
+                else
+                    {
+                    # We go back to the original line if it wasn't a multiline comment because we want the comment to stay in the
+                    # code.  Otherwise the /*@out@*/ from the example would be removed.
+                    $self->TokenizeLine($lines[$lineIndex]);
+                    };
+
+                @commentLines = ( );
+                }
+            else
+                {
+                push @$tokens, "\n";
+                };
+
+            $lineIndex++;
+            }
+
+
+        # Otherwise just add it to the code.
+
+        else
+            {
+            $self->TokenizeLine($line);
+            $lineIndex++;
+            };
+
+
+        # If there were comments, send them to Parser->OnComment().
+
+        if (scalar @commentLines)
+            {
+            NaturalDocs::Parser->OnComment(\@commentLines, $commentLineNumber, $isJavaDoc);
+            @commentLines = ( );
+            $isJavaDoc = undef;
+            };
+
+        # $lineIndex was incremented by the individual code paths above.
+
+        };  # while ($lineIndex < scalar @lines)
+    };
+
+
+#
+#   Function: PreprocessFile
+#
+#   An overridable function if you'd like to preprocess the file before it goes into <ParseForCommentsAndTokens()>.
+#
+#   Parameters:
+#
+#       lines - An arrayref to the file's lines.  Each line has its line break stripped off, but is otherwise untouched.
+#
+sub PreprocessFile #(lines)
+    {
+    };
+
+
+#
+#   Function: TokenizeLine
+#
+#   Converts the passed line to tokens as described in <ParseForCommentsAndTokens> and adds them to <Tokens()>.  Also
+#   adds a line break token after it.
+#
+sub TokenizeLine #(line)
+    {
+    my ($self, $line) = @_;
+    push @{$self->Tokens()}, $line =~ /(\w+|[ \t]+|.)/g, "\n";
+    };
+
+
+#
+#   Function: TryToSkipString
+#
+#   If the position is on a string delimiter, moves the position to the token following the closing delimiter, or past the end of the
+#   tokens if there is none.  Assumes all other characters are allowed in the string, the delimiter itself is allowed if it's preceded by
+#   a backslash, and line breaks are allowed in the string.
+#
+#   Parameters:
+#
+#       indexRef - A reference to the position's index into <Tokens()>.
+#       lineNumberRef - A reference to the position's line number.
+#       openingDelimiter - The opening string delimiter, such as a quote or an apostrophe.
+#       closingDelimiter - The closing string delimiter, if different.  If not defined, assumes the same as openingDelimiter.
+#       startContentIndexRef - A reference to a variable in which to store the index of the first token of the string's content.
+#                                         May be undef.
+#       endContentIndexRef - A reference to a variable in which to store the index of the end of the string's content, which is one
+#                                        past the last index of content.  May be undef.
+#
+#   Returns:
+#
+#       Whether the position was on the passed delimiter or not.  The index, line number, and content index ref variables will be
+#       updated only if true.
+#
+sub TryToSkipString #(indexRef, lineNumberRef, openingDelimiter, closingDelimiter, startContentIndexRef, endContentIndexRef)
+    {
+    my ($self, $index, $lineNumber, $openingDelimiter, $closingDelimiter, $startContentIndexRef, $endContentIndexRef) = @_;
+    my $tokens = $self->Tokens();
+
+    if (!defined $closingDelimiter)
+        {  $closingDelimiter = $openingDelimiter;  };
+
+    if ($tokens->[$$index] ne $openingDelimiter)
+        {  return undef;  };
+
+
+    $$index++;
+    if (defined $startContentIndexRef)
+        {  $$startContentIndexRef = $$index;  };
+
+    while ($$index < scalar @$tokens)
+        {
+        if ($tokens->[$$index] eq "\\")
+            {
+            # Skip the token after it.
+            $$index += 2;
+            }
+        elsif ($tokens->[$$index] eq "\n")
+            {
+            $$lineNumber++;
+            $$index++;
+            }
+        elsif ($tokens->[$$index] eq $closingDelimiter)
+            {
+            if (defined $endContentIndexRef)
+                {  $$endContentIndexRef = $$index;  };
+
+            $$index++;
+            last;
+            }
+        else
+            {
+            $$index++;
+            };
+        };
+
+    if ($$index >= scalar @$tokens && defined $endContentIndexRef)
+        {  $$endContentIndexRef = scalar @$tokens;  };
+
+    return 1;
+    };
+
+
+#
+#   Function: SkipRestOfLine
+#
+#   Moves the position to the token following the next line break, or past the end of the tokens array if there is none.  Useful for
+#   line comments.
+#
+#   Note that it skips blindly.  It assumes there cannot be anything of interest, such as a string delimiter, between the position
+#   and the end of the line.
+#
+#   Parameters:
+#
+#       indexRef - A reference to the position's index into <Tokens()>.
+#       lineNumberRef - A reference to the position's line number.
+
+sub SkipRestOfLine #(indexRef, lineNumberRef)
+    {
+    my ($self, $index, $lineNumber) = @_;
+    my $tokens = $self->Tokens();
+
+    while ($$index < scalar @$tokens)
+        {
+        if ($tokens->[$$index] eq "\n")
+            {
+            $$lineNumber++;
+            $$index++;
+            last;
+            }
+        else
+            {
+            $$index++;
+            };
+        };
+    };
+
+
+#
+#   Function: SkipUntilAfter
+#
+#   Moves the position to the token following the next occurance of a particular token sequence, or past the end of the tokens
+#   array if it never occurs.  Useful for multiline comments.
+#
+#   Note that it skips blindly.  It assumes there cannot be anything of interest, such as a string delimiter, between the position
+#   and the end of the line.
+#
+#   Parameters:
+#
+#       indexRef - A reference to the position's index.
+#       lineNumberRef - A reference to the position's line number.
+#       token - A token that must be matched.  Can be specified multiple times to match a sequence of tokens.
+#
+sub SkipUntilAfter #(indexRef, lineNumberRef, token, token, ...)
+    {
+    my ($self, $index, $lineNumber, @target) = @_;
+    my $tokens = $self->Tokens();
+
+    while ($$index < scalar @$tokens)
+        {
+        if ($tokens->[$$index] eq $target[0] && ($$index + scalar @target) <= scalar @$tokens)
+            {
+            my $match = 1;
+
+            for (my $i = 1; $i < scalar @target; $i++)
+                {
+                if ($tokens->[$$index+$i] ne $target[$i])
+                    {
+                    $match = 0;
+                    last;
+                    };
+                };
+
+            if ($match)
+                {
+                $$index += scalar @target;
+                return;
+                };
+            };
+
+        if ($tokens->[$$index] eq "\n")
+            {
+            $$lineNumber++;
+            $$index++;
+            }
+        else
+            {
+            $$index++;
+            };
+        };
+    };
+
+
+#
+#   Function: IsFirstLineToken
+#
+#   Returns whether the position is at the first token of a line, not including whitespace.
+#
+#   Parameters:
+#
+#       index - The index of the position.
+#
+sub IsFirstLineToken #(index)
+    {
+    my ($self, $index) = @_;
+    my $tokens = $self->Tokens();
+
+    if ($index == 0)
+        {  return 1;  };
+
+    $index--;
+
+    if ($tokens->[$index] =~ /^[ \t]/)
+        {  $index--;  };
+
+    if ($index <= 0 || $tokens->[$index] eq "\n")
+        {  return 1;  }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: IsLastLineToken
+#
+#   Returns whether the position is at the last token of a line, not including whitespace.
+#
+#   Parameters:
+#
+#       index - The index of the position.
+#
+sub IsLastLineToken #(index)
+    {
+    my ($self, $index) = @_;
+    my $tokens = $self->Tokens();
+
+    do
+        {  $index++;  }
+    while ($index < scalar @$tokens && $tokens->[$index] =~ /^[ \t]/);
+
+    if ($index >= scalar @$tokens || $tokens->[$index] eq "\n")
+        {  return 1;  }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: IsAtSequence
+#
+#   Returns whether the position is at a sequence of tokens.
+#
+#   Parameters:
+#
+#       index - The index of the position.
+#       token - A token to match.  Specify multiple times to specify the sequence.
+#
+sub IsAtSequence #(index, token, token, token ...)
+    {
+    my ($self, $index, @target) = @_;
+    my $tokens = $self->Tokens();
+
+    if ($index + scalar @target > scalar @$tokens)
+        {  return undef;  };
+
+    for (my $i = 0; $i < scalar @target; $i++)
+        {
+        if ($tokens->[$index + $i] ne $target[$i])
+            {  return undef;  };
+        };
+
+    return 1;
+    };
+
+
+#
+#   Function: IsBackslashed
+#
+#   Returns whether the position is after a backslash.
+#
+#   Parameters:
+#
+#       index - The index of the postition.
+#
+sub IsBackslashed #(index)
+    {
+    my ($self, $index) = @_;
+    my $tokens = $self->Tokens();
+
+    if ($index > 0 && $tokens->[$index - 1] eq "\\")
+        {  return 1;  }
+    else
+        {  return undef;  };
+    };
+
+
+
+###############################################################################
+#
+#   Group: Scope Functions
+#
+#   These functions provide a nice scope stack implementation for language-specific parsers to use.  The default implementation
+#   makes the following assumptions.
+#
+#   - Packages completely replace one another, rather than concatenating.  You need to concatenate manually if that's the
+#     behavior.
+#
+#   - Packages inherit, so if a scope level doesn't set its own, the package is the same as the parent scope's.
+#
+
+
+#
+#   Function: ClearScopeStack
+#
+#   Clears the scope stack for a new file.  Not necessary if you call <ParseForCommentsAndTokens()>.
+#
+sub ClearScopeStack
+    {
+    my ($self) = @_;
+    $self->[SCOPE_STACK] = [ NaturalDocs::Languages::Advanced::Scope->New(undef, undef) ];
+    $self->[SCOPE_RECORD] = [ NaturalDocs::Languages::Advanced::ScopeChange->New(undef, 1) ];
+    };
+
+
+#
+#   Function: StartScope
+#
+#   Records a new scope level.
+#
+#   Parameters:
+#
+#       closingSymbol - The closing symbol of the scope.
+#       lineNumber - The line number where the scope begins.
+#       package - The package <SymbolString> of the scope.  Undef means no change.
+#
+sub StartScope #(closingSymbol, lineNumber, package)
+    {
+    my ($self, $closingSymbol, $lineNumber, $package) = @_;
+
+    push @{$self->[SCOPE_STACK]},
+            NaturalDocs::Languages::Advanced::Scope->New($closingSymbol, $package, $self->CurrentUsing());
+
+    $self->AddToScopeRecord($self->CurrentScope(), $lineNumber);
+    };
+
+
+#
+#   Function: EndScope
+#
+#   Records the end of the current scope level.  Note that this is blind; you need to manually check <ClosingScopeSymbol()> if
+#   you need to determine if it is correct to do so.
+#
+#   Parameters:
+#
+#       lineNumber - The line number where the scope ends.
+#
+sub EndScope #(lineNumber)
+    {
+    my ($self, $lineNumber) = @_;
+
+    if (scalar @{$self->[SCOPE_STACK]} > 1)
+        {  pop @{$self->[SCOPE_STACK]};  };
+
+    $self->AddToScopeRecord($self->CurrentScope(), $lineNumber);
+    };
+
+
+#
+#   Function: ClosingScopeSymbol
+#
+#   Returns the symbol that ends the current scope level, or undef if we are at the top level.
+#
+sub ClosingScopeSymbol
+    {
+    my ($self) = @_;
+    return $self->[SCOPE_STACK]->[-1]->ClosingSymbol();
+    };
+
+
+#
+#   Function: CurrentScope
+#
+#   Returns the current calculated scope, or undef if global.  The default implementation just returns <CurrentPackage()>.  This
+#   is a separate function because C++ may need to track namespaces and classes separately, and so the current scope would
+#   be a concatenation of them.
+#
+sub CurrentScope
+    {
+    return $_[0]->CurrentPackage();
+    };
+
+
+#
+#   Function: CurrentPackage
+#
+#   Returns the current calculated package or class, or undef if none.
+#
+sub CurrentPackage
+    {
+    my ($self) = @_;
+
+    my $package;
+
+    for (my $index = scalar @{$self->[SCOPE_STACK]} - 1; $index >= 0 && !defined $package; $index--)
+        {
+        $package = $self->[SCOPE_STACK]->[$index]->Package();
+        };
+
+    return $package;
+    };
+
+
+#
+#   Function: SetPackage
+#
+#   Sets the package for the current scope level.
+#
+#   Parameters:
+#
+#       package - The new package <SymbolString>.
+#       lineNumber - The line number the new package starts on.
+#
+sub SetPackage #(package, lineNumber)
+    {
+    my ($self, $package, $lineNumber) = @_;
+    $self->[SCOPE_STACK]->[-1]->SetPackage($package);
+
+    $self->AddToScopeRecord($self->CurrentScope(), $lineNumber);
+    };
+
+
+#
+#   Function: CurrentUsing
+#
+#   Returns the current calculated arrayref of <SymbolStrings> from Using statements, or undef if none.
+#
+sub CurrentUsing
+    {
+    my ($self) = @_;
+    return $self->[SCOPE_STACK]->[-1]->Using();
+    };
+
+
+#
+#   Function: AddUsing
+#
+#   Adds a Using <SymbolString> to the current scope.
+#
+sub AddUsing #(using)
+    {
+    my ($self, $using) = @_;
+    $self->[SCOPE_STACK]->[-1]->AddUsing($using);
+    };
+
+
+
+###############################################################################
+# Group: Support Functions
+
+
+#
+#   Function: AddToScopeRecord
+#
+#   Adds a change to the scope record, condensing unnecessary entries.
+#
+#   Parameters:
+#
+#       newScope - What the scope <SymbolString> changed to.
+#       lineNumber - Where the scope changed.
+#
+sub AddToScopeRecord #(newScope, lineNumber)
+    {
+    my ($self, $scope, $lineNumber) = @_;
+    my $scopeRecord = $self->ScopeRecord();
+
+    if ($scope ne $scopeRecord->[-1]->Scope())
+        {
+        if ($scopeRecord->[-1]->LineNumber() == $lineNumber)
+            {  $scopeRecord->[-1]->SetScope($scope);  }
+        else
+            {  push @$scopeRecord, NaturalDocs::Languages::Advanced::ScopeChange->New($scope, $lineNumber);  };
+        };
+    };
+
+
+#
+#   Function: CreateString
+#
+#   Converts the specified tokens into a string and returns it.
+#
+#   Parameters:
+#
+#       startIndex - The starting index to convert.
+#       endIndex - The ending index, which is *not inclusive*.
+#
+#   Returns:
+#
+#       The string.
+#
+sub CreateString #(startIndex, endIndex)
+    {
+    my ($self, $startIndex, $endIndex) = @_;
+    my $tokens = $self->Tokens();
+
+    my $string;
+
+    while ($startIndex < $endIndex && $startIndex < scalar @$tokens)
+        {
+        $string .= $tokens->[$startIndex];
+        $startIndex++;
+        };
+
+    return $string;
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Languages/Advanced/Scope.pm b/docs/tool/Modules/NaturalDocs/Languages/Advanced/Scope.pm
new file mode 100644
index 00000000..e1e50a95
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Languages/Advanced/Scope.pm
@@ -0,0 +1,95 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Languages::Advanced::Scope
+#
+###############################################################################
+#
+#   A class used to store a scope level.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Languages::Advanced::Scope;
+
+#
+#   Constants: Implementation
+#
+#   The object is implemented as a blessed arrayref.  The constants below are used as indexes.
+#
+#   CLOSING_SYMBOL - The closing symbol character of the scope.
+#   PACKAGE - The package <SymbolString> of the scope.
+#   USING - An arrayref of <SymbolStrings> for using statements, or undef if none.
+#
+use NaturalDocs::DefineMembers 'CLOSING_SYMBOL', 'PACKAGE', 'USING';
+# Dependency: New() depends on the order of these constants as well as that there is no inherited members.
+
+
+#
+#   Function: New
+#
+#   Creates and returns a new object.
+#
+#   Parameters:
+#
+#       closingSymbol - The closing symbol character of the scope.
+#       package - The package <SymbolString> of the scope.
+#       using - An arrayref of using <SymbolStrings>, or undef if none.  The contents of the array will be duplicated.
+#
+#       If package is set to undef, it is assumed that it inherits the value of the previous scope on the stack.
+#
+sub New #(closingSymbol, package, using)
+    {
+    # Dependency: This depends on the order of the parameters matching the constants, and that there are no inherited
+    # members.
+    my $package = shift;
+
+    my $object = [ @_ ];
+    bless $object, $package;
+
+    if (defined $object->[USING])
+        {  $object->[USING] = [ @{$object->[USING]} ];  };
+
+    return $object;
+    };
+
+
+# Function: ClosingSymbol
+# Returns the closing symbol character of the scope.
+sub ClosingSymbol
+    {  return $_[0]->[CLOSING_SYMBOL];  };
+
+# Function: Package
+# Returns the package <SymbolString> of the scope, or undef if none.
+sub Package
+    {  return $_[0]->[PACKAGE];  };
+
+# Function: SetPackage
+# Sets the package <SymbolString> of the scope.
+sub SetPackage #(package)
+    {  $_[0]->[PACKAGE] = $_[1];  };
+
+# Function: Using
+# Returns an arrayref of <SymbolStrings> for using statements, or undef if none
+sub Using
+    {  return $_[0]->[USING];  };
+
+# Function: AddUsing
+# Adds a <SymbolString> to the <Using()> array.
+sub AddUsing #(using)
+    {
+    my ($self, $using) = @_;
+
+    if (!defined $self->[USING])
+        {  $self->[USING] = [ ];  };
+
+    push @{$self->[USING]}, $using;
+    };
+
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Languages/Advanced/ScopeChange.pm b/docs/tool/Modules/NaturalDocs/Languages/Advanced/ScopeChange.pm
new file mode 100644
index 00000000..24de5503
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Languages/Advanced/ScopeChange.pm
@@ -0,0 +1,70 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Languages::Advanced::ScopeChange
+#
+###############################################################################
+#
+#   A class used to store a scope change.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Languages::Advanced::ScopeChange;
+
+#
+#   Constants: Implementation
+#
+#   The object is implemented as a blessed arrayref.  The constants below are used as indexes.
+#
+#   SCOPE - The new scope <SymbolString>.
+#   LINE_NUMBER - The line number of the change.
+#
+use NaturalDocs::DefineMembers 'SCOPE', 'LINE_NUMBER';
+# Dependency: New() depends on the order of these constants as well as that there is no inherited members.
+
+
+#
+#   Function: New
+#
+#   Creates and returns a new object.
+#
+#   Parameters:
+#
+#       scope - The <SymbolString> the scope was changed to.
+#       lineNumber - What line it occurred on.
+#
+sub New #(scope, lineNumber)
+    {
+    # Dependency: This depends on the order of the parameters matching the constants, and that there are no inherited
+    # members.
+    my $self = shift;
+
+    my $object = [ @_ ];
+    bless $object, $self;
+
+    return $object;
+    };
+
+
+# Function: Scope
+# Returns the <SymbolString> the scope was changed to.
+sub Scope
+    {  return $_[0]->[SCOPE];  };
+
+# Function: SetScope
+# Replaces the <SymbolString> the scope was changed to.
+sub SetScope #(scope)
+    {  $_[0]->[SCOPE] = $_[1];  };
+
+# Function: LineNumber
+# Returns the line number of the change.
+sub LineNumber
+    {  return $_[0]->[LINE_NUMBER];  };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Languages/Base.pm b/docs/tool/Modules/NaturalDocs/Languages/Base.pm
new file mode 100644
index 00000000..f89b7045
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Languages/Base.pm
@@ -0,0 +1,832 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Languages::Base
+#
+###############################################################################
+#
+#   A base class for all programming language parsers.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Languages::Base;
+
+use NaturalDocs::DefineMembers 'NAME', 'Name()',
+                                                 'EXTENSIONS', 'Extensions()', 'SetExtensions() duparrayref',
+                                                 'SHEBANG_STRINGS', 'ShebangStrings()', 'SetShebangStrings() duparrayref',
+                                                 'IGNORED_PREFIXES',
+                                                 'ENUM_VALUES';
+
+use base 'Exporter';
+our @EXPORT = ('ENUM_GLOBAL', 'ENUM_UNDER_TYPE', 'ENUM_UNDER_PARENT');
+
+
+#
+#   Constants: EnumValuesType
+#
+#   How enum values are handled in the language.
+#
+#   ENUM_GLOBAL - Values are always global and thus 'value'.
+#   ENUM_UNDER_TYPE - Values are under the type in the hierarchy, and thus 'package.enum.value'.
+#   ENUM_UNDER_PARENT - Values are under the parent in the hierarchy, putting them on the same level as the enum itself.  Thus
+#                                       'package.value'.
+#
+use constant ENUM_GLOBAL => 1;
+use constant ENUM_UNDER_TYPE => 2;
+use constant ENUM_UNDER_PARENT => 3;
+
+
+#
+#   Handle: SOURCEFILEHANDLE
+#
+#   The handle of the source file currently being parsed.
+#
+
+
+#
+#   Function: New
+#
+#   Creates and returns a new object.
+#
+#   Parameters:
+#
+#       name - The name of the language.
+#
+sub New #(name)
+    {
+    my ($selfPackage, $name) = @_;
+
+    my $object = [ ];
+
+    $object->[NAME] = $name;
+
+    bless $object, $selfPackage;
+    return $object;
+    };
+
+
+#
+#   Functions: Members
+#
+#   Name - Returns the language's name.
+#   Extensions - Returns an arrayref of the language's file extensions, or undef if none.
+#   SetExtensions - Replaces the arrayref of the language's file extensions.
+#   ShebangStrings - Returns an arrayref of the language's shebang strings, or undef if none.
+#   SetShebangStrings - Replaces the arrayref of the language's shebang strings.
+#
+
+#
+#   Function: PackageSeparator
+#   Returns the language's package separator string.
+#
+sub PackageSeparator
+    {  return '.';  };
+
+#
+#   Function: PackageSeparatorWasSet
+#   Returns whether the language's package separator string was ever changed from the default.
+#
+sub PackageSeparatorWasSet
+    {  return 0;  };
+
+
+#
+#   Function: EnumValues
+#   Returns the <EnumValuesType> that describes how the language handles enums.
+#
+sub EnumValues
+    {  return ENUM_GLOBAL;  };
+
+
+#
+#   Function: IgnoredPrefixesFor
+#
+#   Returns an arrayref of ignored prefixes for the passed <TopicType>, or undef if none.  The array is sorted so that the longest
+#   prefixes are first.
+#
+sub IgnoredPrefixesFor #(type)
+    {
+    my ($self, $type) = @_;
+
+    if (defined $self->[IGNORED_PREFIXES])
+        {  return $self->[IGNORED_PREFIXES]->{$type};  }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: SetIgnoredPrefixesFor
+#
+#   Replaces the arrayref of ignored prefixes for the passed <TopicType>.
+#
+sub SetIgnoredPrefixesFor #(type, prefixes)
+    {
+    my ($self, $type, $prefixesRef) = @_;
+
+    if (!defined $self->[IGNORED_PREFIXES])
+        {  $self->[IGNORED_PREFIXES] = { };  };
+
+    if (!defined $prefixesRef)
+        {  delete $self->[IGNORED_PREFIXES]->{$type};  }
+    else
+        {
+        my $prefixes = [ @$prefixesRef ];
+
+        # Sort prefixes to be longest to shortest.
+        @$prefixes = sort { length $b <=> length $a } @$prefixes;
+
+        $self->[IGNORED_PREFIXES]->{$type} = $prefixes;
+        };
+    };
+
+
+#
+#   Function: HasIgnoredPrefixes
+#
+#   Returns whether the language has any ignored prefixes at all.
+#
+sub HasIgnoredPrefixes
+    {  return defined $_[0]->[IGNORED_PREFIXES];  };
+
+
+#
+#   Function: CopyIgnoredPrefixesOf
+#
+#   Copies all the ignored prefix settings of the passed <NaturalDocs::Languages::Base> object.
+#
+sub CopyIgnoredPrefixesOf #(language)
+    {
+    my ($self, $language) = @_;
+
+    if ($language->HasIgnoredPrefixes())
+        {
+        $self->[IGNORED_PREFIXES] = { };
+
+        while (my ($topicType, $prefixes) = each %{$language->[IGNORED_PREFIXES]})
+            {
+            $self->[IGNORED_PREFIXES]->{$topicType} = [ @$prefixes ];
+            };
+        };
+    };
+
+
+
+###############################################################################
+# Group: Parsing Functions
+
+
+#
+#   Function: ParseFile
+#
+#   Parses the passed source file, sending comments acceptable for documentation to <NaturalDocs::Parser->OnComment()>.
+#   This *must* be defined by a subclass.
+#
+#   Parameters:
+#
+#       sourceFile - The <FileName> of the source file to parse.
+#       topicList - A reference to the list of <NaturalDocs::Parser::ParsedTopics> being built by the file.
+#
+#   Returns:
+#
+#       The array ( autoTopics, scopeRecord ).
+#
+#       autoTopics - An arrayref of automatically generated <NaturalDocs::Parser::ParsedTopics> from the file, or undef if none.
+#       scopeRecord - An arrayref of <NaturalDocs::Languages::Advanced::ScopeChanges>, or undef if none.
+#
+
+
+#
+#   Function: ParsePrototype
+#
+#   Parses the prototype and returns it as a <NaturalDocs::Languages::Prototype> object.
+#
+#   Parameters:
+#
+#       type - The <TopicType>.
+#       prototype - The text prototype.
+#
+#   Returns:
+#
+#       A <NaturalDocs::Languages::Prototype> object.
+#
+sub ParsePrototype #(type, prototype)
+    {
+    my ($self, $type, $prototype) = @_;
+
+    my $isClass = NaturalDocs::Topics->TypeInfo($type)->ClassHierarchy();
+
+    if ($prototype !~ /\(.*[^ ].*\)/ && (!$isClass || $prototype !~ /\{.*[^ ].*\}/))
+        {
+        my $object = NaturalDocs::Languages::Prototype->New($prototype);
+        return $object;
+        };
+
+
+    # Parse the parameters out of the prototype.
+
+    my @tokens = $prototype =~ /([^\(\)\[\]\{\}\<\>\'\"\,\;]+|.)/g;
+
+    my $parameter;
+    my @parameterLines;
+
+    my @symbolStack;
+    my $finishedParameters;
+
+    my ($beforeParameters, $afterParameters);
+
+    foreach my $token (@tokens)
+        {
+        if ($finishedParameters)
+            {  $afterParameters .= $token;  }
+
+        elsif ($symbolStack[-1] eq '\'' || $symbolStack[-1] eq '"')
+            {
+            if ($symbolStack[0] eq '(' || ($isClass && $symbolStack[0] eq '{'))
+                {  $parameter .= $token;  }
+            else
+                {  $beforeParameters .= $token;  };
+
+            if ($token eq $symbolStack[-1])
+                {  pop @symbolStack;  };
+            }
+
+        elsif ($token =~ /^[\(\[\{\<\'\"]$/)
+            {
+            if ($symbolStack[0] eq '(' || ($isClass && $symbolStack[0] eq '{'))
+                {  $parameter .= $token;   }
+            else
+                {  $beforeParameters .= $token;  };
+
+            push @symbolStack, $token;
+            }
+
+        elsif ( ($token eq ')' && $symbolStack[-1] eq '(') ||
+                 ($token eq ']' && $symbolStack[-1] eq '[') ||
+                 ($token eq '}' && $symbolStack[-1] eq '{') ||
+                 ($token eq '>' && $symbolStack[-1] eq '<') )
+            {
+            if ($symbolStack[0] eq '(')
+                {
+                if ($token eq ')' && scalar @symbolStack == 1)
+                    {
+                    if ($parameter ne ' ')
+                        {  push @parameterLines, $parameter;  };
+
+                    $finishedParameters = 1;
+                    $afterParameters .= $token;
+                    }
+                else
+                    {  $parameter .= $token;  };
+                }
+            elsif ($isClass && $symbolStack[0] eq '{')
+                {
+                if ($token eq '}' && scalar @symbolStack == 1)
+                    {
+                    if ($parameter ne ' ')
+                        {  push @parameterLines, $parameter;  };
+
+                    $finishedParameters = 1;
+                    $afterParameters .= $token;
+                    }
+                else
+                    {  $parameter .= $token;  };
+                }
+            else
+                {
+                $beforeParameters .= $token;
+                };
+
+            pop @symbolStack;
+            }
+
+        elsif ($token eq ',' || $token eq ';')
+            {
+            if ($symbolStack[0] eq '(' || ($isClass && $symbolStack[0] eq '{'))
+                {
+                if (scalar @symbolStack == 1)
+                    {
+                    push @parameterLines, $parameter . $token;
+                    $parameter = undef;
+                    }
+                else
+                    {
+                    $parameter .= $token;
+                    };
+                }
+            else
+                {
+                $beforeParameters .= $token;
+                };
+            }
+
+        else
+            {
+            if ($symbolStack[0] eq '(' || ($isClass && $symbolStack[0] eq '{'))
+                {  $parameter .= $token;  }
+            else
+                {  $beforeParameters .= $token;  };
+            };
+        };
+
+    foreach my $part (\$beforeParameters, \$afterParameters)
+        {
+        $$part =~ s/^ //;
+        $$part =~ s/ $//;
+        };
+
+    my $prototypeObject = NaturalDocs::Languages::Prototype->New($beforeParameters, $afterParameters);
+
+
+    # Parse the actual parameters.
+
+    foreach my $parameterLine (@parameterLines)
+        {
+        $prototypeObject->AddParameter( $self->ParseParameterLine($parameterLine) );
+        };
+
+    return $prototypeObject;
+    };
+
+
+#
+#   Function: ParseParameterLine
+#
+#   Parses a prototype parameter line and returns it as a <NaturalDocs::Languages::Prototype::Parameter> object.
+#
+#   This vesion assumes a C++ style line.  If you need a Pascal style line, override this function to forward to
+#   <ParsePascalParameterLine()>.
+#
+#   > Function(parameter, type parameter, type parameter = value);
+#
+sub ParseParameterLine #(line)
+    {
+    my ($self, $line) = @_;
+
+    $line =~ s/^ //;
+    $line =~ s/ $//;
+
+    my @tokens = $line =~ /([^ \(\)\{\}\[\]\<\>\'\"\=]+|.)/g;
+
+    my @symbolStack;
+    my @parameterWords = ( undef );
+    my ($defaultValue, $defaultValuePrefix, $inDefaultValue);
+
+    foreach my $token (@tokens)
+        {
+        if ($inDefaultValue)
+            {  $defaultValue .= $token;  }
+
+        elsif ($symbolStack[-1] eq '\'' || $symbolStack[-1] eq '"')
+            {
+            $parameterWords[-1] .= $token;
+
+            if ($token eq $symbolStack[-1])
+                {  pop @symbolStack;  };
+            }
+
+        elsif ($token =~ /^[\(\[\{\<\'\"]$/)
+            {
+            push @symbolStack, $token;
+            $parameterWords[-1] .= $token;
+            }
+
+        elsif ( ($token eq ')' && $symbolStack[-1] eq '(') ||
+                 ($token eq ']' && $symbolStack[-1] eq '[') ||
+                 ($token eq '}' && $symbolStack[-1] eq '{') ||
+                 ($token eq '>' && $symbolStack[-1] eq '<') )
+            {
+            pop @symbolStack;
+            $parameterWords[-1] .= $token;
+            }
+
+        elsif ($token eq ' ')
+            {
+            if (!scalar @symbolStack)
+                {  push @parameterWords, undef;  }
+            else
+                {  $parameterWords[-1] .= $token;  };
+            }
+
+        elsif ($token eq '=')
+            {
+            if (!scalar @symbolStack)
+                {
+                $defaultValuePrefix = $token;
+                $inDefaultValue = 1;
+                }
+            else
+                {  $parameterWords[-1] .= $token;  };
+            }
+
+        else
+            {
+            $parameterWords[-1] .= $token;
+            };
+        };
+
+    my ($name, $namePrefix, $type, $typePrefix);
+
+    if (!$parameterWords[-1])
+        {  pop @parameterWords;  };
+
+    $name = pop @parameterWords;
+
+    if ($parameterWords[-1]=~ /([\*\&]+)$/)
+        {
+        $namePrefix = $1;
+        $parameterWords[-1] = substr($parameterWords[-1], 0, 0 - length($namePrefix));
+        $parameterWords[-1] =~ s/ $//;
+
+        if (!$parameterWords[-1])
+            {  pop @parameterWords;  };
+        }
+    elsif ($name =~ /^([\*\&]+)/)
+        {
+        $namePrefix = $1;
+        $name = substr($name, length($namePrefix));
+        $name =~ s/^ //;
+        };
+
+    $type = pop @parameterWords;
+    $typePrefix = join(' ', @parameterWords);
+
+    if ($typePrefix)
+        {  $typePrefix .= ' ';  };
+
+    if ($type =~ /^([a-z0-9_\:\.]+(?:\.|\:\:))[a-z0-9_]/i)
+        {
+        my $attachedTypePrefix = $1;
+
+        $typePrefix .= $attachedTypePrefix;
+        $type = substr($type, length($attachedTypePrefix));
+        };
+
+    $defaultValue =~ s/ $//;
+
+    return NaturalDocs::Languages::Prototype::Parameter->New($type, $typePrefix, $name, $namePrefix,
+                                                                                             $defaultValue, $defaultValuePrefix);
+    };
+
+
+#
+#   Function: ParsePascalParameterLine
+#
+#   Parses a Pascal-like prototype parameter line and returns it as a <NaturalDocs::Languages::Prototype::Parameter> object.
+#   Pascal lines are as follows:
+#
+#   > Function (name: type; name, name: type := value)
+#
+#   Also supports ActionScript lines
+#
+#   > Function (name: type, name, name: type = value)
+#
+sub ParsePascalParameterLine #(line)
+    {
+    my ($self, $line) = @_;
+
+    $line =~ s/^ //;
+    $line =~ s/ $//;
+
+    my @tokens = $line =~ /([^\(\)\{\}\[\]\<\>\'\"\=\:]+|\:\=|.)/g;
+    my ($type, $name, $defaultValue, $defaultValuePrefix, $afterName, $afterDefaultValue);
+    my @symbolStack;
+
+    foreach my $token (@tokens)
+        {
+        if ($afterDefaultValue)
+            {  $defaultValue .= $token;  }
+
+        elsif ($symbolStack[-1] eq '\'' || $symbolStack[-1] eq '"')
+            {
+            if ($afterName)
+                {  $type .= $token;  }
+            else
+                {  $name .= $token;  };
+
+            if ($token eq $symbolStack[-1])
+                {  pop @symbolStack;  };
+            }
+
+        elsif ($token =~ /^[\(\[\{\<\'\"]$/)
+            {
+            push @symbolStack, $token;
+
+            if ($afterName)
+                {  $type .= $token;  }
+            else
+                {  $name .= $token;  };
+            }
+
+        elsif ( ($token eq ')' && $symbolStack[-1] eq '(') ||
+                 ($token eq ']' && $symbolStack[-1] eq '[') ||
+                 ($token eq '}' && $symbolStack[-1] eq '{') ||
+                 ($token eq '>' && $symbolStack[-1] eq '<') )
+            {
+            pop @symbolStack;
+
+            if ($afterName)
+                {  $type .= $token;  }
+            else
+                {  $name .= $token;  };
+            }
+
+        elsif ($afterName)
+            {
+            if (($token eq ':=' || $token eq '=') && !scalar @symbolStack)
+                {
+                $defaultValuePrefix = $token;
+                $afterDefaultValue = 1;
+                }
+            else
+                {  $type .= $token;  };
+            }
+
+        elsif ($token eq ':' && !scalar @symbolStack)
+            {
+            $name .= $token;
+            $afterName = 1;
+            }
+
+        else
+            {  $name .= $token;  };
+        };
+
+    foreach my $part (\$type, \$name, \$defaultValue)
+        {
+        $$part =~ s/^ //;
+        $$part =~ s/ $//;
+        };
+
+    return NaturalDocs::Languages::Prototype::Parameter->New($type, undef, $name, undef, $defaultValue, $defaultValuePrefix);
+    };
+
+
+#
+#   Function: TypeBeforeParameter
+#
+#   Returns whether the type appears before the parameter in prototypes.
+#
+#   For example, it does in C++
+#   > void Function (int a, int b)
+#
+#   but does not in Pascal
+#   > function Function (a: int; b, c: int)
+#
+sub TypeBeforeParameter
+    {
+    return 1;
+    };
+
+
+
+#
+#   Function: IgnoredPrefixLength
+#
+#   Returns the length of the prefix that should be ignored in the index, or zero if none.
+#
+#   Parameters:
+#
+#       name - The name of the symbol.
+#       type  - The symbol's <TopicType>.
+#
+#   Returns:
+#
+#       The length of the prefix to ignore, or zero if none.
+#
+sub IgnoredPrefixLength #(name, type)
+    {
+    my ($self, $name, $type) = @_;
+
+    foreach my $prefixes ($self->IgnoredPrefixesFor($type), $self->IgnoredPrefixesFor(::TOPIC_GENERAL()))
+        {
+        if (defined $prefixes)
+            {
+            foreach my $prefix (@$prefixes)
+                {
+                if (substr($name, 0, length($prefix)) eq $prefix)
+                    {  return length($prefix);  };
+                };
+            };
+        };
+
+    return 0;
+    };
+
+
+
+###############################################################################
+# Group: Support Functions
+
+
+#
+#   Function: StripOpeningSymbols
+#
+#   Determines if the line starts with any of the passed symbols, and if so, replaces it with spaces.  This only happens
+#   if the only thing before it on the line is whitespace.
+#
+#   Parameters:
+#
+#       lineRef - A reference to the line to check.
+#       symbols - An arrayref of the symbols to check for.
+#
+#   Returns:
+#
+#       If the line starts with any of the passed comment symbols, it will replace it in the line with spaces and return the symbol.
+#       If the line doesn't, it will leave the line alone and return undef.
+#
+sub StripOpeningSymbols #(lineRef, symbols)
+    {
+    my ($self, $lineRef, $symbols) = @_;
+
+    if (!defined $symbols)
+        {  return undef;  };
+
+    my ($index, $symbol) = ::FindFirstSymbol($$lineRef, $symbols);
+
+    if ($index != -1 && substr($$lineRef, 0, $index) =~ /^[ \t]*$/)
+        {
+        return substr($$lineRef, $index, length($symbol), ' ' x length($symbol));
+        };
+
+    return undef;
+    };
+
+
+#
+#   Function: StripOpeningJavaDocSymbols
+#
+#   Determines if the line starts with any of the passed symbols, and if so, replaces it with spaces.  This only happens
+#   if the only thing before it on the line is whitespace and the next character after it is whitespace or the end of the line.
+#
+#   Parameters:
+#
+#       lineRef - A reference to the line to check.
+#       symbols - An arrayref of the symbols to check for.
+#
+#   Returns:
+#
+#       If the line starts with any of the passed comment symbols, it will replace it in the line with spaces and return the symbol.
+#       If the line doesn't, it will leave the line alone and return undef.
+#
+sub StripOpeningJavaDocSymbols #(lineRef, symbols)
+    {
+    my ($self, $lineRef, $symbols) = @_;
+
+    if (!defined $symbols)
+        {  return undef;  };
+
+    my ($index, $symbol) = ::FindFirstSymbol($$lineRef, $symbols);
+
+    if ($index != -1 && substr($$lineRef, 0, $index) =~ /^[ \t]*$/ && substr($$lineRef, $index + length($symbol), 1) =~ /^[ \t]?$/)
+        {
+        return substr($$lineRef, $index, length($symbol), ' ' x length($symbol));
+        };
+
+    return undef;
+    };
+
+
+#
+#   Function: StripOpeningBlockSymbols
+#
+#   Determines if the line starts with any of the opening symbols in the passed symbol pairs, and if so, replaces it with spaces.
+#   This only happens if the only thing before it on the line is whitespace.
+#
+#   Parameters:
+#
+#       lineRef - A reference to the line to check.
+#       symbolPairs - An arrayref of the symbol pairs to check for.  Pairs are specified as two consecutive array entries, with the
+#                            opening symbol first.
+#
+#   Returns:
+#
+#       If the line starts with any of the opening symbols, it will replace it in the line with spaces and return the closing symbol.
+#       If the line doesn't, it will leave the line alone and return undef.
+#
+sub StripOpeningBlockSymbols #(lineRef, symbolPairs)
+    {
+    my ($self, $lineRef, $symbolPairs) = @_;
+
+    if (!defined $symbolPairs)
+        {  return undef;  };
+
+    for (my $i = 0; $i < scalar @$symbolPairs; $i += 2)
+        {
+        my $index = index($$lineRef, $symbolPairs->[$i]);
+
+        if ($index != -1 && substr($$lineRef, 0, $index) =~ /^[ \t]*$/)
+            {
+            substr($$lineRef, $index, length($symbolPairs->[$i]), ' ' x length($symbolPairs->[$i]));
+            return $symbolPairs->[$i + 1];
+            };
+        };
+
+    return undef;
+    };
+
+
+#
+#   Function: StripOpeningJavaDocBlockSymbols
+#
+#   Determines if the line starts with any of the opening symbols in the passed symbol pairs, and if so, replaces it with spaces.
+#   This only happens if the only thing before it on the line is whitespace and the next character is whitespace or the end of the line.
+#
+#   Parameters:
+#
+#       lineRef - A reference to the line to check.
+#       symbolPairs - An arrayref of the symbol pairs to check for.  Pairs are specified as two consecutive array entries, with the
+#                            opening symbol first.
+#
+#   Returns:
+#
+#       If the line starts with any of the opening symbols, it will replace it in the line with spaces and return the closing symbol.
+#       If the line doesn't, it will leave the line alone and return undef.
+#
+sub StripOpeningJavaDocBlockSymbols #(lineRef, symbolPairs)
+    {
+    my ($self, $lineRef, $symbolPairs) = @_;
+
+    if (!defined $symbolPairs)
+        {  return undef;  };
+
+    for (my $i = 0; $i < scalar @$symbolPairs; $i += 2)
+        {
+        my $index = index($$lineRef, $symbolPairs->[$i]);
+
+        if ($index != -1 && substr($$lineRef, 0, $index) =~ /^[ \t]*$/ &&
+            substr($$lineRef, $index + length($symbolPairs->[$i]), 1) =~ /^[ \t]?$/)
+            {
+            substr($$lineRef, $index, length($symbolPairs->[$i]), ' ' x length($symbolPairs->[$i]));
+            return $symbolPairs->[$i + 1];
+            };
+        };
+
+    return undef;
+    };
+
+
+#
+#   Function: StripClosingSymbol
+#
+#   Determines if the line contains a symbol, and if so, truncates it just before the symbol.
+#
+#   Parameters:
+#
+#       lineRef - A reference to the line to check.
+#       symbol - The symbol to check for.
+#
+#   Returns:
+#
+#       The remainder of the line, or undef if the symbol was not found.
+#
+sub StripClosingSymbol #(lineRef, symbol)
+    {
+    my ($self, $lineRef, $symbol) = @_;
+
+    my $index = index($$lineRef, $symbol);
+
+    if ($index != -1)
+        {
+        my $lineRemainder = substr($$lineRef, $index + length($symbol));
+        $$lineRef = substr($$lineRef, 0, $index);
+
+        return $lineRemainder;
+        }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: NormalizePrototype
+#
+#   Normalizes a prototype.  Specifically, condenses spaces, tabs, and line breaks into single spaces and removes leading and
+#   trailing ones.
+#
+#   Parameters:
+#
+#       prototype - The original prototype string.
+#
+#   Returns:
+#
+#       The normalized prototype.
+#
+sub NormalizePrototype #(prototype)
+    {
+    my ($self, $prototype) = @_;
+
+    $prototype =~ tr/ \t\r\n/ /s;
+    $prototype =~ s/^ //;
+    $prototype =~ s/ $//;
+
+    return $prototype;
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Languages/CSharp.pm b/docs/tool/Modules/NaturalDocs/Languages/CSharp.pm
new file mode 100644
index 00000000..5bcd50be
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Languages/CSharp.pm
@@ -0,0 +1,1484 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Languages::CSharp
+#
+###############################################################################
+#
+#   A subclass to handle the language variations of C#.
+#
+#
+#   Topic: Language Support
+#
+#       Supported:
+#
+#       - Classes
+#       - Namespaces (no topic generated)
+#       - Functions
+#       - Constructors and Destructors
+#       - Properties
+#       - Indexers
+#       - Operators
+#       - Delegates
+#       - Variables
+#       - Constants
+#       - Events
+#       - Enums
+#
+#       Not supported yet:
+#
+#       - Autodocumenting enum members
+#       - Using alias
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Languages::CSharp;
+
+use base 'NaturalDocs::Languages::Advanced';
+
+
+###############################################################################
+# Group: Package Variables
+
+#
+#   hash: classKeywords
+#   An existence hash of all the acceptable class keywords.  The keys are in all lowercase.
+#
+my %classKeywords = ( 'class' => 1,
+                                    'struct' => 1,
+                                    'interface' => 1 );
+
+#
+#   hash: classModifiers
+#   An existence hash of all the acceptable class modifiers.  The keys are in all lowercase.
+#
+my %classModifiers = ( 'new' => 1,
+                                   'public' => 1,
+                                   'protected' => 1,
+                                   'internal' => 1,
+                                   'private' => 1,
+                                   'abstract' => 1,
+                                   'sealed' => 1,
+                                   'unsafe' => 1,
+                                   'static' => 1 );
+
+#
+#   hash: functionModifiers
+#   An existence hash of all the acceptable function modifiers.  Also applies to properties.  Also encompasses those for operators
+#   and indexers, but have more than are valid for them.  The keys are in all lowercase.
+#
+my %functionModifiers = ( 'new' => 1,
+                                       'public' => 1,
+                                       'protected' => 1,
+                                       'internal' => 1,
+                                       'private' => 1,
+                                       'static' => 1,
+                                       'virtual' => 1,
+                                       'sealed' => 1,
+                                       'override' => 1,
+                                       'abstract' => 1,
+                                       'extern' => 1,
+                                       'unsafe' => 1 );
+
+#
+#   hash: variableModifiers
+#   An existence hash of all the acceptable variable modifiers.  The keys are in all lowercase.
+#
+my %variableModifiers = ( 'new' => 1,
+                                       'public' => 1,
+                                       'protected' => 1,
+                                       'internal' => 1,
+                                       'private' => 1,
+                                       'static' => 1,
+                                       'readonly' => 1,
+                                       'volatile' => 1,
+                                       'unsafe' => 1 );
+
+#
+#   hash: enumTypes
+#   An existence hash of all the possible enum types.  The keys are in all lowercase.
+#
+my %enumTypes = ( 'sbyte' => 1,
+                             'byte' => 1,
+                             'short' => 1,
+                             'ushort' => 1,
+                             'int' => 1,
+                             'uint' => 1,
+                             'long' => 1,
+                             'ulong' => 1 );
+
+#
+#   hash: impossibleTypeWords
+#   An existence hash of all the reserved words that cannot be in a type.  This includes 'enum' and all modifiers.  The keys are in
+#   all lowercase.
+#
+my %impossibleTypeWords = ( 'abstract' => 1, 'as' => 1, 'base' => 1, 'break' => 1, 'case' => 1, 'catch' => 1,
+                                              'checked' => 1, 'class' => 1, 'const' => 1, 'continue' => 1, 'default' => 1, 'delegate' => 1,
+                                              'do' => 1, 'else' => 1, 'enum' => 1, 'event' => 1, 'explicit' => 1, 'extern' => 1,
+                                              'false' => 1, 'finally' => 1, 'fixed' => 1, 'for' => 1, 'foreach' => 1, 'goto' => 1, 'if' => 1,
+                                              'implicit' => 1, 'in' => 1, 'interface' => 1, 'internal' => 1, 'is' => 1, 'lock' => 1,
+                                              'namespace' => 1, 'new' => 1, 'null' => 1, 'operator' => 1, 'out' => 1, 'override' => 1,
+                                              'params' => 1, 'private' => 1, 'protected' => 1, 'public' => 1, 'readonly' => 1, 'ref' => 1,
+                                              'return' => 1, 'sealed' => 1, 'sizeof' => 1, 'stackalloc' => 1, 'static' => 1,
+                                              'struct' => 1, 'switch' => 1, 'this' => 1, 'throw' => 1, 'true' => 1, 'try' => 1, 'typeof' => 1,
+                                              'unchecked' => 1, 'unsafe' => 1, 'using' => 1, 'virtual' => 1, 'volatile' => 1, 'while' => 1 );
+# Deleted from the list: object, string, bool, decimal, sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, void
+
+
+
+###############################################################################
+# Group: Interface Functions
+
+
+#
+#   Function: PackageSeparator
+#   Returns the package separator symbol.
+#
+sub PackageSeparator
+    {  return '.';  };
+
+
+#
+#   Function: EnumValues
+#   Returns the <EnumValuesType> that describes how the language handles enums.
+#
+sub EnumValues
+    {  return ::ENUM_UNDER_TYPE();  };
+
+
+#
+#   Function: ParseFile
+#
+#   Parses the passed source file, sending comments acceptable for documentation to <NaturalDocs::Parser->OnComment()>.
+#
+#   Parameters:
+#
+#       sourceFile - The <FileName> to parse.
+#       topicList - A reference to the list of <NaturalDocs::Parser::ParsedTopics> being built by the file.
+#
+#   Returns:
+#
+#       The array ( autoTopics, scopeRecord ).
+#
+#       autoTopics - An arrayref of automatically generated topics from the file, or undef if none.
+#       scopeRecord - An arrayref of <NaturalDocs::Languages::Advanced::ScopeChanges>, or undef if none.
+#
+sub ParseFile #(sourceFile, topicsList)
+    {
+    my ($self, $sourceFile, $topicsList) = @_;
+
+    $self->ParseForCommentsAndTokens($sourceFile, [ '//' ], [ '/*', '*/' ], [ '///' ], [ '/**', '*/' ] );
+
+    my $tokens = $self->Tokens();
+    my $index = 0;
+    my $lineNumber = 1;
+
+    while ($index < scalar @$tokens)
+        {
+        if ($self->TryToSkipWhitespace(\$index, \$lineNumber) ||
+            $self->TryToGetNamespace(\$index, \$lineNumber) ||
+            $self->TryToGetUsing(\$index, \$lineNumber) ||
+            $self->TryToGetClass(\$index, \$lineNumber) ||
+            $self->TryToGetFunction(\$index, \$lineNumber) ||
+            $self->TryToGetOverloadedOperator(\$index, \$lineNumber) ||
+            $self->TryToGetVariable(\$index, \$lineNumber) ||
+            $self->TryToGetEnum(\$index, \$lineNumber) )
+            {
+            # The functions above will handle everything.
+            }
+
+        elsif ($tokens->[$index] eq '{')
+            {
+            $self->StartScope('}', $lineNumber, undef, undef, undef);
+            $index++;
+            }
+
+        elsif ($tokens->[$index] eq '}')
+            {
+            if ($self->ClosingScopeSymbol() eq '}')
+                {  $self->EndScope($lineNumber);  };
+
+            $index++;
+            }
+
+        else
+            {
+            $self->SkipRestOfStatement(\$index, \$lineNumber);
+            };
+        };
+
+
+    # Don't need to keep these around.
+    $self->ClearTokens();
+
+
+    my $autoTopics = $self->AutoTopics();
+
+    my $scopeRecord = $self->ScopeRecord();
+    if (defined $scopeRecord && !scalar @$scopeRecord)
+        {  $scopeRecord = undef;  };
+
+    return ( $autoTopics, $scopeRecord );
+    };
+
+
+
+###############################################################################
+# Group: Statement Parsing Functions
+# All functions here assume that the current position is at the beginning of a statement.
+#
+# Note for developers: I am well aware that the code in these functions do not check if we're past the end of the tokens as
+# often as it should.  We're making use of the fact that Perl will always return undef in these cases to keep the code simpler.
+
+
+#
+#   Function: TryToGetNamespace
+#
+#   Determines whether the position is at a namespace declaration statement, and if so, adjusts the scope, skips it, and returns
+#   true.
+#
+#   Why no topic?:
+#
+#       The main reason we don't create a Natural Docs topic for a namespace is because in order to declare class A.B.C in C#,
+#       you must do this:
+#
+#       > namespace A.B
+#       >    {
+#       >    class C
+#       >        { ... }
+#       >    }
+#
+#       That would result in a namespace topic whose only purpose is really to qualify C.  It would take the default page title, and
+#       thus the default menu title.  So if you have files for A.B.X, A.B.Y, and A.B.Z, they all will appear as A.B on the menu.
+#
+#       If something actually appears in the namespace besides a class, it will be handled by
+#       <NaturalDocs::Parser->AddPackageDelineators()>.  That function will add a package topic to correct the scope.
+#
+#       If the user actually documented it, it will still appear because of the manual topic.
+#
+sub TryToGetNamespace #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    if (lc($tokens->[$$indexRef]) ne 'namespace')
+        {  return undef;  };
+
+    my $index = $$indexRef + 1;
+    my $lineNumber = $$lineNumberRef;
+
+    if (!$self->TryToSkipWhitespace(\$index, \$lineNumber))
+        {  return undef;  };
+
+    my $name;
+
+    while ($tokens->[$index] =~ /^[a-z_\.\@]/i)
+        {
+        $name .= $tokens->[$index];
+        $index++;
+        };
+
+    if (!defined $name)
+        {  return undef;  };
+
+    $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+    if ($tokens->[$index] ne '{')
+        {  return undef;  };
+
+    $index++;
+
+
+    # We found a valid one if we made it this far.
+
+    my $autoTopic = NaturalDocs::Parser::ParsedTopic->New(::TOPIC_CLASS(), $name,
+                                                                                         $self->CurrentScope(), $self->CurrentUsing(),
+                                                                                         undef,
+                                                                                         undef, undef, $$lineNumberRef);
+
+    # We don't add an auto-topic for namespaces.  See the function documentation above.
+
+    NaturalDocs::Parser->OnClass($autoTopic->Package());
+
+    $self->StartScope('}', $lineNumber, $autoTopic->Package());
+
+    $$indexRef = $index;
+    $$lineNumberRef = $lineNumber;
+
+    return 1;
+    };
+
+
+#
+#   Function: TryToGetClass
+#
+#   Determines whether the position is at a class declaration statement, and if so, generates a topic for it, skips it, and
+#   returns true.
+#
+#   Supported Syntaxes:
+#
+#       - Classes
+#       - Structs
+#       - Interfaces
+#
+sub TryToGetClass #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $index = $$indexRef;
+    my $lineNumber = $$lineNumberRef;
+
+    my $startIndex = $index;
+    my $startLine = $lineNumber;
+    my $needsPrototype = 0;
+
+    if ($self->TryToSkipAttributes(\$index, \$lineNumber))
+        {  $self->TryToSkipWhitespace(\$index, \$lineNumber);  }
+
+    my @modifiers;
+
+    while ($tokens->[$index] =~ /^[a-z]/i &&
+              !exists $classKeywords{lc($tokens->[$index])} &&
+              exists $classModifiers{lc($tokens->[$index])} )
+        {
+        push @modifiers, lc($tokens->[$index]);
+        $index++;
+
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+        };
+
+    if (!exists $classKeywords{lc($tokens->[$index])})
+        {  return undef;  };
+
+    my $lcClassKeyword = lc($tokens->[$index]);
+
+    $index++;
+
+    if (!$self->TryToSkipWhitespace(\$index, \$lineNumber))
+        {  return undef;  };
+
+    my $name;
+
+    while ($tokens->[$index] =~ /^[a-z_\@]/i)
+        {
+        $name .= $tokens->[$index];
+        $index++;
+        };
+
+    if (!defined $name)
+        {  return undef;  };
+
+    $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+    if ($tokens->[$index] eq '<')
+    	{
+    	# XXX: This is half-assed.
+    	$index++;
+    	$needsPrototype = 1;
+
+    	while ($index < scalar @$tokens && $tokens->[$index] ne '>')
+    		{
+    		$index++;
+    		}
+
+    	if ($index < scalar @$tokens)
+    		{
+    		$index++;
+    		}
+    	else
+    		{  return undef;  }
+
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+    	}
+
+    my @parents;
+
+    if ($tokens->[$index] eq ':')
+        {
+        do
+            {
+            $index++;
+
+            $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+            my $parentName;
+
+            while ($tokens->[$index] =~ /^[a-z_\.\@]/i)
+                {
+                $parentName .= $tokens->[$index];
+                $index++;
+                };
+
+	        if ($tokens->[$index] eq '<')
+	            {
+	            # XXX: This is still half-assed.
+	            $index++;
+	            $needsPrototype = 1;
+
+	            while ($index < scalar @$tokens && $tokens->[$index] ne '>')
+	                {
+	                $index++;
+	                }
+
+	            if ($index < scalar @$tokens)
+	                {
+	                $index++;
+	                }
+	            else
+	                {  return undef;  }
+
+	            $self->TryToSkipWhitespace(\$index, \$lineNumber);
+	            }
+
+            if (!defined $parentName)
+                {  return undef;  };
+
+            push @parents, NaturalDocs::SymbolString->FromText($parentName);
+
+            $self->TryToSkipWhitespace(\$index, \$lineNumber);
+            }
+        while ($tokens->[$index] eq ',')
+        };
+
+    if (lc($tokens->[$index]) eq 'where')
+    	{
+    	# XXX: This is also half-assed
+    	$index++;
+
+    	while ($index < scalar @$tokens && $tokens->[$index] ne '{')
+    		{
+    		$index++;
+    		}
+    	}
+
+    if ($tokens->[$index] ne '{')
+        {  return undef;  };
+
+
+    # If we made it this far, we have a valid class declaration.
+
+    my @scopeIdentifiers = NaturalDocs::SymbolString->IdentifiersOf($self->CurrentScope());
+    $name = join('.', @scopeIdentifiers, $name);
+
+    my $topicType;
+
+    if ($lcClassKeyword eq 'interface')
+        {  $topicType = ::TOPIC_INTERFACE();  }
+    else
+        {  $topicType = ::TOPIC_CLASS();  };
+
+    my $prototype;
+
+    if ($needsPrototype)
+    	{
+    	$prototype = $self->CreateString($startIndex, $index);
+    	}
+
+    my $autoTopic = NaturalDocs::Parser::ParsedTopic->New($topicType, $name,
+                                                                                         undef, $self->CurrentUsing(),
+                                                                                         $prototype,
+                                                                                         undef, undef, $$lineNumberRef);
+
+    $self->AddAutoTopic($autoTopic);
+    NaturalDocs::Parser->OnClass($autoTopic->Package());
+
+    foreach my $parent (@parents)
+        {
+        NaturalDocs::Parser->OnClassParent($autoTopic->Package(), $parent, $self->CurrentScope(), undef,
+                                                               ::RESOLVE_RELATIVE());
+        };
+
+    $self->StartScope('}', $lineNumber, $autoTopic->Package());
+
+    $index++;
+
+    $$indexRef = $index;
+    $$lineNumberRef = $lineNumber;
+
+    return 1;
+    };
+
+
+#
+#   Function: TryToGetUsing
+#
+#   Determines whether the position is at a using statement, and if so, adds it to the current scope, skips it, and returns
+#	true.
+#
+#	Supported:
+#
+#       - Using
+#
+#	Unsupported:
+#
+#		- Using with alias
+#
+sub TryToGetUsing #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $index = $$indexRef;
+    my $lineNumber = $$lineNumberRef;
+
+    if (lc($tokens->[$index]) ne 'using')
+        {  return undef;  };
+
+    $index++;
+    $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+    my $name;
+
+    while ($tokens->[$index] =~ /^[a-z_\@\.]/i)
+        {
+        $name .= $tokens->[$index];
+        $index++;
+        };
+
+    if ($tokens->[$index] ne ';' ||
+		!defined $name)
+        {  return undef;  };
+
+    $index++;
+
+
+    $self->AddUsing( NaturalDocs::SymbolString->FromText($name) );
+
+    $$indexRef = $index;
+    $$lineNumberRef = $lineNumber;
+
+    return 1;
+    };
+
+
+
+#
+#   Function: TryToGetFunction
+#
+#   Determines if the position is on a function declaration, and if so, generates a topic for it, skips it, and returns true.
+#
+#   Supported Syntaxes:
+#
+#       - Functions
+#       - Constructors
+#       - Destructors
+#       - Properties
+#       - Indexers
+#       - Delegates
+#       - Events
+#
+sub TryToGetFunction #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $index = $$indexRef;
+    my $lineNumber = $$lineNumberRef;
+
+    if ($self->TryToSkipAttributes(\$index, \$lineNumber))
+        {  $self->TryToSkipWhitespace(\$index, \$lineNumber);  };
+
+    my $startIndex = $index;
+    my $startLine = $lineNumber;
+
+    my @modifiers;
+
+    while ($tokens->[$index] =~ /^[a-z]/i &&
+              exists $functionModifiers{lc($tokens->[$index])} )
+        {
+        push @modifiers, lc($tokens->[$index]);
+        $index++;
+
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+        };
+
+    my $isDelegate;
+    my $isEvent;
+
+    if (lc($tokens->[$index]) eq 'delegate')
+        {
+        $isDelegate = 1;
+        $index++;
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+        }
+    elsif (lc($tokens->[$index]) eq 'event')
+        {
+        $isEvent = 1;
+        $index++;
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+        };
+
+    my $returnType = $self->TryToGetType(\$index, \$lineNumber);
+
+    $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+    my $name;
+    my $lastNameWord;
+
+    while ($tokens->[$index] =~ /^[a-z\_\@\.\~\<]/i)
+        {
+        $name .= $tokens->[$index];
+
+        # Ugly hack, but what else is new?  For explicit generic interface definitions, such as:
+        # IDObjectType System.Collections.Generic.IEnumerator<IDObjectType>.Current
+
+        if ($tokens->[$index] eq '<')
+        	{
+        	do
+        		{
+	        	$index++;
+        		$name .= $tokens->[$index];
+        		}
+        	while ($index < @$tokens && $tokens->[$index] ne '>');
+        	}
+
+        $lastNameWord = $tokens->[$index];
+        $index++;
+        };
+
+    if (!defined $name)
+        {
+        # Constructors and destructors don't have return types.  It's possible their names were mistaken for the return type.
+        if (defined $returnType)
+            {
+            $name = $returnType;
+            $returnType = undef;
+
+            $name =~ /([a-z0-9_]+)$/i;
+            $lastNameWord = $1;
+            }
+        else
+            {  return undef;  };
+        };
+
+    # If there's no return type, make sure it's a constructor or destructor.
+    if (!defined $returnType)
+        {
+        my @identifiers = NaturalDocs::SymbolString->IdentifiersOf( $self->CurrentScope() );
+
+        if ($lastNameWord ne $identifiers[-1])
+            {  return undef;  };
+        };
+
+    $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+
+    # Skip the brackets on indexers.
+    if ($tokens->[$index] eq '[' && lc($lastNameWord) eq 'this')
+        {
+        # This should jump the brackets completely.
+        $self->GenericSkip(\$index, \$lineNumber);
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+        $name .= '[]';
+        };
+
+
+    # Properties, indexers, events with braces
+
+    if ($tokens->[$index] eq '{')
+        {
+        my $prototype = $self->CreateString($startIndex, $index);
+
+        $index++;
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+        my ($aWord, $bWord, $hasA, $hasB);
+
+        if ($isEvent)
+            {
+            $aWord = 'add';
+            $bWord = 'remove';
+            }
+        else
+            {
+            $aWord = 'get';
+            $bWord = 'set';
+            };
+
+        while ($index < scalar @$tokens)
+            {
+            if ($self->TryToSkipAttributes(\$index, \$lineNumber))
+                {  $self->TryToSkipWhitespace(\$index, \$lineNumber);  };
+
+            if (lc($tokens->[$index]) eq $aWord)
+                {  $hasA = 1;  }
+            elsif (lc($tokens->[$index]) eq $bWord)
+                {  $hasB = 1;  }
+            elsif ($tokens->[$index] eq '}')
+                {
+                $index++;
+                last;
+                };
+
+            $self->SkipRestOfStatement(\$index, \$lineNumber);
+            $self->TryToSkipWhitespace(\$index, \$lineNumber);
+            };
+
+        if ($hasA && $hasB)
+            {  $prototype .= ' { ' . $aWord . ', ' . $bWord . ' }';  }
+        elsif ($hasA)
+            {  $prototype .= ' { ' . $aWord . ' }';  }
+        elsif ($hasB)
+            {  $prototype .= ' { ' . $bWord . ' }';  };
+
+        $prototype = $self->NormalizePrototype($prototype);
+
+        my $topicType = ( $isEvent ? ::TOPIC_EVENT() : ::TOPIC_PROPERTY() );
+
+        $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New($topicType, $name,
+                                                                                                  $self->CurrentScope(), $self->CurrentUsing(),
+                                                                                                  $prototype,
+                                                                                                  undef, undef, $startLine));
+        }
+
+
+    # Functions, constructors, destructors, delegates.
+
+    elsif ($tokens->[$index] eq '(')
+        {
+        # This should jump the parenthesis completely.
+        $self->GenericSkip(\$index, \$lineNumber);
+
+        my $topicType = ( $isDelegate ? ::TOPIC_DELEGATE() : ::TOPIC_FUNCTION() );
+        my $prototype = $self->NormalizePrototype( $self->CreateString($startIndex, $index) );
+
+        $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New($topicType, $name,
+                                                                                                  $self->CurrentScope(), $self->CurrentUsing(),
+                                                                                                  $prototype,
+                                                                                                  undef, undef, $startLine));
+
+        $self->SkipRestOfStatement(\$index, \$lineNumber);
+        }
+
+
+    # Events without braces
+
+    elsif ($isEvent && $tokens->[$index] eq ';')
+        {
+        my $prototype = $self->NormalizePrototype( $self->CreateString($startIndex, $index) );
+
+        $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New(::TOPIC_EVENT(), $name,
+                                                                                                  $self->CurrentScope(), $self->CurrentUsing(),
+                                                                                                  $prototype,
+                                                                                                  undef, undef, $startLine));
+        $index++;
+        }
+
+    else
+        {  return undef;  };
+
+
+    # We succeeded if we got this far.
+
+    $$indexRef = $index;
+    $$lineNumberRef = $lineNumber;
+
+    return 1;
+    };
+
+
+#
+#   Function: TryToGetOverloadedOperator
+#
+#   Determines if the position is on an operator overload declaration, and if so, generates a topic for it, skips it, and returns true.
+#
+sub TryToGetOverloadedOperator #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $index = $$indexRef;
+    my $lineNumber = $$lineNumberRef;
+
+    if ($self->TryToSkipAttributes(\$index, \$lineNumber))
+        {  $self->TryToSkipWhitespace(\$index, \$lineNumber);  };
+
+    my $startIndex = $index;
+    my $startLine = $lineNumber;
+
+    my @modifiers;
+
+    while ($tokens->[$index] =~ /^[a-z]/i &&
+              exists $functionModifiers{lc($tokens->[$index])} )
+        {
+        push @modifiers, lc($tokens->[$index]);
+        $index++;
+
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+        };
+
+
+    my $name;
+
+
+    # Casting operators.
+
+    if (lc($tokens->[$index]) eq 'implicit' || lc($tokens->[$index]) eq 'explicit')
+        {
+        $index++;
+
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+        if (lc($tokens->[$index]) ne 'operator')
+            {  return undef;  };
+
+        $index++;
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+        $name = $self->TryToGetType(\$index, \$lineNumber);
+
+        if (!defined $name)
+            {  return undef;  };
+        }
+
+
+    # Symbol operators.
+
+    else
+        {
+        if (!$self->TryToGetType(\$index, \$lineNumber))
+            {  return undef;  };
+
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+        if (lc($tokens->[$index]) ne 'operator')
+            {  return undef;  };
+
+        $index++;
+
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+        if (lc($tokens->[$index]) eq 'true' || lc($tokens->[$index]) eq 'false')
+            {
+            $name = $tokens->[$index];
+            $index++;
+            }
+        else
+            {
+            while ($tokens->[$index] =~ /^[\+\-\!\~\*\/\%\&\|\^\<\>\=]$/)
+                {
+                $name .= $tokens->[$index];
+                $index++;
+                };
+            };
+        };
+
+    $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+    if ($tokens->[$index] ne '(')
+        {  return undef;  };
+
+    # This should skip the parenthesis completely.
+    $self->GenericSkip(\$index, \$lineNumber);
+
+    my $prototype = $self->NormalizePrototype( $self->CreateString($startIndex, $index) );
+
+    $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New(::TOPIC_FUNCTION(), 'operator ' . $name,
+                                                                                              $self->CurrentScope(), $self->CurrentUsing(),
+                                                                                              $prototype,
+                                                                                              undef, undef, $startLine));
+
+    $self->SkipRestOfStatement(\$index, \$lineNumber);
+
+
+    # We succeeded if we got this far.
+
+    $$indexRef = $index;
+    $$lineNumberRef = $lineNumber;
+
+    return 1;
+    };
+
+
+#
+#   Function: TryToGetVariable
+#
+#   Determines if the position is on a variable declaration statement, and if so, generates a topic for each variable, skips the
+#   statement, and returns true.
+#
+#   Supported Syntaxes:
+#
+#       - Variables
+#       - Constants
+#
+sub TryToGetVariable #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $index = $$indexRef;
+    my $lineNumber = $$lineNumberRef;
+
+    if ($self->TryToSkipAttributes(\$index, \$lineNumber))
+        {  $self->TryToSkipWhitespace(\$index, \$lineNumber);  };
+
+    my $startIndex = $index;
+    my $startLine = $lineNumber;
+
+    my @modifiers;
+
+    while ($tokens->[$index] =~ /^[a-z]/i &&
+              exists $variableModifiers{lc($tokens->[$index])} )
+        {
+        push @modifiers, lc($tokens->[$index]);
+        $index++;
+
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+        };
+
+    my $type;
+    if (lc($tokens->[$index]) eq 'const')
+        {
+        $type = ::TOPIC_CONSTANT();
+        $index++;
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+        }
+    else
+        {
+        $type = ::TOPIC_VARIABLE();
+        };
+
+    if (!$self->TryToGetType(\$index, \$lineNumber))
+        {  return undef;  };
+
+    my $endTypeIndex = $index;
+
+    $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+    my @names;
+
+    for (;;)
+        {
+        my $name;
+
+        while ($tokens->[$index] =~ /^[a-z\@\_]/i)
+            {
+            $name .= $tokens->[$index];
+            $index++;
+            };
+
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+        if ($tokens->[$index] eq '=')
+            {
+            do
+                {
+                $self->GenericSkip(\$index, \$lineNumber);
+                }
+            while ($tokens->[$index] ne ',' && $tokens->[$index] ne ';');
+            };
+
+        push @names, $name;
+
+        if ($tokens->[$index] eq ';')
+            {
+            $index++;
+            last;
+            }
+        elsif ($tokens->[$index] eq ',')
+            {
+            $index++;
+            $self->TryToSkipWhitespace(\$index, \$lineNumber);
+            }
+        else
+            {  return undef;  };
+        };
+
+
+    # We succeeded if we got this far.
+
+    my $prototypePrefix = $self->CreateString($startIndex, $endTypeIndex);
+
+    foreach my $name (@names)
+        {
+        my $prototype = $self->NormalizePrototype( $prototypePrefix . ' ' . $name );
+
+        $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New($type, $name,
+                                                                                                  $self->CurrentScope(), $self->CurrentUsing(),
+                                                                                                  $prototype,
+                                                                                                  undef, undef, $startLine));
+        };
+
+    $$indexRef = $index;
+    $$lineNumberRef = $lineNumber;
+
+    return 1;
+    };
+
+
+#
+#   Function: TryToGetEnum
+#
+#   Determines if the position is on an enum declaration statement, and if so, generates a topic for it.
+#
+#   Supported Syntaxes:
+#
+#       - Enums
+#       - Enums with declared types
+#
+#   Unsupported:
+#
+#       - Documenting the members automatically
+#
+sub TryToGetEnum #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $index = $$indexRef;
+    my $lineNumber = $$lineNumberRef;
+
+    if ($self->TryToSkipAttributes(\$index, \$lineNumber))
+        {  $self->TryToSkipWhitespace(\$index, \$lineNumber);  };
+
+    my $startIndex = $index;
+    my $startLine = $lineNumber;
+
+    my @modifiers;
+
+    while ($tokens->[$index] =~ /^[a-z]/i &&
+              exists $variableModifiers{lc($tokens->[$index])} )
+        {
+        push @modifiers, lc($tokens->[$index]);
+        $index++;
+
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+        };
+
+    if (lc($tokens->[$index]) ne 'enum')
+        {  return undef;  }
+
+    $index++;
+    $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+    my $name;
+
+    while ($tokens->[$index] =~ /^[a-z\@\_]/i)
+        {
+        $name .= $tokens->[$index];
+        $index++;
+        };
+
+    $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+    if ($tokens->[$index] eq ':')
+        {
+        $index++;
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+        if (!exists $enumTypes{ lc($tokens->[$index]) })
+            {  return undef;  }
+
+        $index++;
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+        }
+
+    if ($tokens->[$index] ne '{')
+        {  return undef;  }
+
+    # We succeeded if we got this far.
+
+    my $prototype = $self->CreateString($startIndex, $index);
+    $prototype = $self->NormalizePrototype( $prototype );
+
+    $self->SkipRestOfStatement(\$index, \$lineNumber);
+
+    $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New(::TOPIC_ENUMERATION(), $name,
+                                                                                              $self->CurrentScope(), $self->CurrentUsing(),
+                                                                                              $prototype,
+                                                                                              undef, undef, $startLine));
+
+    $$indexRef = $index;
+    $$lineNumberRef = $lineNumber;
+
+    return 1;
+    };
+
+
+#
+#   Function: TryToGetType
+#
+#   Determines if the position is on a type identifier, and if so, skips it and returns it as a string.  This function does _not_ allow
+#   modifiers.  You must take care of those beforehand.
+#
+sub TryToGetType #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $name;
+    my $index = $$indexRef;
+    my $lineNumber = $$lineNumberRef;
+
+    while ($tokens->[$index] =~ /^[a-z\@\.\_]/i)
+        {
+        if (exists $impossibleTypeWords{ lc($tokens->[$index]) } && $name !~ /\@$/)
+            {  return undef;  };
+
+        $name .= $tokens->[$index];
+        $index++;
+        };
+
+    if (!defined $name)
+        {  return undef;  };
+
+	$self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+	if ($tokens->[$index] eq '?')
+		{
+		$name .= '?';
+		$index++;
+
+		$self->TryToSkipWhitespace(\$index, \$lineNumber);
+		}
+
+    if ($tokens->[$index] eq '<')
+    	{
+    	# XXX: This is half-assed.
+    	$name .= '<';
+    	$index++;
+
+    	while ($index < scalar @$tokens && $tokens->[$index] ne '>')
+    		{
+    		$name .= $tokens->[$index];
+    		$index++;
+    		}
+
+    	if ($index < scalar @$tokens)
+    		{
+    		$name .= '>';
+    		$index++;
+    		}
+    	else
+    		{  return undef;  }
+
+		$self->TryToSkipWhitespace(\$index, \$lineNumber);
+    	}
+
+    while ($tokens->[$index] eq '[')
+        {
+        $name .= '[';
+        $index++;
+
+        while ($tokens->[$index] eq ',')
+            {
+            $name .= ',';
+            $index++;
+            };
+
+        if ($tokens->[$index] eq ']')
+            {
+            $name .= ']';
+            $index++;
+            }
+        else
+            {  return undef;  }
+        };
+
+    $$indexRef = $index;
+    $$lineNumberRef = $lineNumber;
+
+    return $name;
+    };
+
+
+
+###############################################################################
+# Group: Low Level Parsing Functions
+
+
+#
+#   Function: GenericSkip
+#
+#   Advances the position one place through general code.
+#
+#   - If the position is on a string, it will skip it completely.
+#   - If the position is on an opening symbol, it will skip until the past the closing symbol.
+#   - If the position is on whitespace (including comments and preprocessing directives), it will skip it completely.
+#   - Otherwise it skips one token.
+#
+#   Parameters:
+#
+#       indexRef - A reference to the current index.
+#       lineNumberRef - A reference to the current line number.
+#
+sub GenericSkip #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    # We can ignore the scope stack because we're just skipping everything without parsing, and we need recursion anyway.
+    if ($tokens->[$$indexRef] eq '{')
+        {
+        $$indexRef++;
+        $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}');
+        }
+    elsif ($tokens->[$$indexRef] eq '(')
+        {
+        $$indexRef++;
+        $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ')');
+        }
+    elsif ($tokens->[$$indexRef] eq '[')
+        {
+        $$indexRef++;
+        $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ']');
+        }
+
+    elsif ($self->TryToSkipWhitespace($indexRef, $lineNumberRef) ||
+            $self->TryToSkipString($indexRef, $lineNumberRef))
+        {
+        }
+
+    else
+        {  $$indexRef++;  };
+    };
+
+
+#
+#   Function: GenericSkipUntilAfter
+#
+#   Advances the position via <GenericSkip()> until a specific token is reached and passed.
+#
+sub GenericSkipUntilAfter #(indexRef, lineNumberRef, token)
+    {
+    my ($self, $indexRef, $lineNumberRef, $token) = @_;
+    my $tokens = $self->Tokens();
+
+    while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] ne $token)
+        {  $self->GenericSkip($indexRef, $lineNumberRef);  };
+
+    if ($tokens->[$$indexRef] eq "\n")
+        {  $$lineNumberRef++;  };
+    $$indexRef++;
+    };
+
+
+#
+#   Function: SkipRestOfStatement
+#
+#   Advances the position via <GenericSkip()> until after the end of the current statement, which is defined as a semicolon or
+#   a brace group.  Of course, either of those appearing inside parenthesis, a nested brace group, etc. don't count.
+#
+sub SkipRestOfStatement #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    while ($$indexRef < scalar @$tokens &&
+             $tokens->[$$indexRef] ne ';' &&
+             $tokens->[$$indexRef] ne '{')
+        {
+        $self->GenericSkip($indexRef, $lineNumberRef);
+        };
+
+    if ($tokens->[$$indexRef] eq ';')
+        {  $$indexRef++;  }
+    elsif ($tokens->[$$indexRef] eq '{')
+        {  $self->GenericSkip($indexRef, $lineNumberRef);  };
+    };
+
+
+#
+#   Function: TryToSkipString
+#   If the current position is on a string delimiter, skip past the string and return true.
+#
+#   Parameters:
+#
+#       indexRef - A reference to the index of the position to start at.
+#       lineNumberRef - A reference to the line number of the position.
+#
+#   Returns:
+#
+#       Whether the position was at a string.
+#
+#   Syntax Support:
+#
+#       - Supports quotes, apostrophes, and at-quotes.
+#
+sub TryToSkipString #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    # The three string delimiters.  All three are Perl variables when preceded by a dollar sign.
+    if ($self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '\'') ||
+        $self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '"') )
+        {
+        return 1;
+        }
+    elsif ($tokens->[$$indexRef] eq '@' && $tokens->[$$indexRef+1] eq '"')
+        {
+        $$indexRef += 2;
+
+        # We need to do at-strings manually because backslash characters are accepted as regular characters, and two consecutive
+        # quotes are accepted as well.
+
+        while ($$indexRef < scalar @$tokens && !($tokens->[$$indexRef] eq '"' && $tokens->[$$indexRef+1] ne '"') )
+            {
+            if ($tokens->[$$indexRef] eq '"')
+                {
+                # This is safe because the while condition will only let through quote pairs.
+                $$indexRef += 2;
+                }
+            elsif ($tokens->[$$indexRef] eq "\n")
+                {
+                $$indexRef++;
+                $$lineNumberRef++;
+                }
+            else
+                {
+                $$indexRef++;
+                };
+            };
+
+        # Skip the closing quote.
+        if ($$indexRef < scalar @$tokens)
+            {  $$indexRef++;  };
+
+        return 1;
+        }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: TryToSkipAttributes
+#   If the current position is on an attribute section, skip it and return true.  Skips multiple attribute sections if they appear
+#   consecutively.
+#
+sub TryToSkipAttributes #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $success;
+
+    while ($tokens->[$$indexRef] eq '[')
+        {
+        $success = 1;
+        $$indexRef++;
+        $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ']');
+        $self->TryToSkipWhitespace($indexRef, $lineNumberRef);
+        };
+
+    return $success;
+    };
+
+
+#
+#   Function: TryToSkipWhitespace
+#   If the current position is on a whitespace token, a line break token, a comment, or a preprocessing directive, it skips them
+#   and returns true.  If there are a number of these in a row, it skips them all.
+#
+sub TryToSkipWhitespace #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $result;
+
+    while ($$indexRef < scalar @$tokens)
+        {
+        if ($tokens->[$$indexRef] =~ /^[ \t]/)
+            {
+            $$indexRef++;
+            $result = 1;
+            }
+        elsif ($tokens->[$$indexRef] eq "\n")
+            {
+            $$indexRef++;
+            $$lineNumberRef++;
+            $result = 1;
+            }
+        elsif ($self->TryToSkipComment($indexRef, $lineNumberRef) ||
+                $self->TryToSkipPreprocessingDirective($indexRef, $lineNumberRef))
+            {
+            $result = 1;
+            }
+        else
+            {  last;  };
+        };
+
+    return $result;
+    };
+
+
+#
+#   Function: TryToSkipComment
+#   If the current position is on a comment, skip past it and return true.
+#
+sub TryToSkipComment #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+
+    return ( $self->TryToSkipLineComment($indexRef, $lineNumberRef) ||
+                $self->TryToSkipMultilineComment($indexRef, $lineNumberRef) );
+    };
+
+
+#
+#   Function: TryToSkipLineComment
+#   If the current position is on a line comment symbol, skip past it and return true.
+#
+sub TryToSkipLineComment #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    if ($tokens->[$$indexRef] eq '/' && $tokens->[$$indexRef+1] eq '/')
+        {
+        $self->SkipRestOfLine($indexRef, $lineNumberRef);
+        return 1;
+        }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: TryToSkipMultilineComment
+#   If the current position is on an opening comment symbol, skip past it and return true.
+#
+sub TryToSkipMultilineComment #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    if ($tokens->[$$indexRef] eq '/' && $tokens->[$$indexRef+1] eq '*')
+        {
+        $self->SkipUntilAfter($indexRef, $lineNumberRef, '*', '/');
+        return 1;
+        }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: TryToSkipPreprocessingDirective
+#   If the current position is on a preprocessing directive, skip past it and return true.
+#
+sub TryToSkipPreprocessingDirective #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    if ($tokens->[$$indexRef] eq '#' && $self->IsFirstLineToken($$indexRef))
+        {
+        $self->SkipRestOfLine($indexRef, $lineNumberRef);
+        return 1;
+        }
+    else
+        {  return undef;  };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Languages/PLSQL.pm b/docs/tool/Modules/NaturalDocs/Languages/PLSQL.pm
new file mode 100644
index 00000000..4c3df998
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Languages/PLSQL.pm
@@ -0,0 +1,319 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Languages::PLSQL
+#
+###############################################################################
+#
+#   A subclass to handle the language variations of PL/SQL.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Languages::PLSQL;
+
+use base 'NaturalDocs::Languages::Simple';
+
+
+#
+#   Function: OnPrototypeEnd
+#
+#   Microsoft's SQL specifies parameters as shown below.
+#
+#   > CREATE PROCEDURE Test @as int, @foo int AS ...
+#
+#   Having a parameter @is or @as is perfectly valid even though those words are also used to end the prototype.  We need to
+#   ignore text-based enders preceded by an at sign.  Also note that it does not have parenthesis for parameter lists.  We need to
+#   skip all commas if the prototype doesn't have parenthesis but does have @ characters.
+#
+#	Identifiers such as function names may contain the characters $, #, and _, so if "as" or "is" appears directly after one of them
+#	we need to ignore the ender there as well.
+#
+#	> FUNCTION Something_is_something ...
+#
+#   Parameters:
+#
+#       type - The <TopicType> of the prototype.
+#       prototypeRef - A reference to the prototype so far, minus the ender in dispute.
+#       ender - The ender symbol.
+#
+#   Returns:
+#
+#       ENDER_ACCEPT - The ender is accepted and the prototype is finished.
+#       ENDER_IGNORE - The ender is rejected and parsing should continue.  Note that the prototype will be rejected as a whole
+#                                  if all enders are ignored before reaching the end of the code.
+#       ENDER_ACCEPT_AND_CONTINUE - The ender is accepted so the prototype may stand as is.  However, the prototype might
+#                                                          also continue on so continue parsing.  If there is no accepted ender between here and
+#                                                          the end of the code this version will be accepted instead.
+#       ENDER_REVERT_TO_ACCEPTED - The expedition from ENDER_ACCEPT_AND_CONTINUE failed.  Use the last accepted
+#                                                        version and end parsing.
+#
+sub OnPrototypeEnd #(type, prototypeRef, ender)
+    {
+    my ($self, $type, $prototypeRef, $ender) = @_;
+
+    # _ should be handled already.
+    if ($ender =~ /^[a-z]+$/i && substr($$prototypeRef, -1) =~ /^[\@\$\#]$/)
+        {  return ::ENDER_IGNORE();  }
+
+    elsif ($type eq ::TOPIC_FUNCTION() && $ender eq ',')
+        {
+        if ($$prototypeRef =~ /^[^\(]*\@/)
+            {  return ::ENDER_IGNORE();  }
+        else
+            {  return ::ENDER_ACCEPT();  };
+        }
+
+    else
+        {  return ::ENDER_ACCEPT();  };
+    };
+
+
+#
+#   Function: ParsePrototype
+#
+#   Overridden to handle Microsoft's parenthesisless version.  Otherwise just throws to the parent.
+#
+#   Parameters:
+#
+#       type - The <TopicType>.
+#       prototype - The text prototype.
+#
+#   Returns:
+#
+#       A <NaturalDocs::Languages::Prototype> object.
+#
+sub ParsePrototype #(type, prototype)
+    {
+    my ($self, $type, $prototype) = @_;
+
+    my $noParenthesisParameters = ($type eq ::TOPIC_FUNCTION() && $prototype =~ /^[^\(]*\@/);
+
+    if ($prototype !~ /\(.*[^ ].*\)/ && !$noParenthesisParameters)
+        {  return $self->SUPER::ParsePrototype($type, $prototype);  };
+
+
+
+    my ($beforeParameters, $afterParameters, $isAfterParameters);
+
+    if ($noParenthesisParameters)
+        {
+        ($beforeParameters, $prototype) = split(/\@/, $prototype, 2);
+        $prototype = '@' . $prototype;
+        };
+
+    my @tokens = $prototype =~ /([^\(\)\[\]\{\}\<\>\'\"\,]+|.)/g;
+
+    my $parameter;
+    my @parameterLines;
+
+    my @symbolStack;
+
+    foreach my $token (@tokens)
+        {
+        if ($isAfterParameters)
+            {  $afterParameters .= $token;  }
+
+        elsif ($symbolStack[-1] eq '\'' || $symbolStack[-1] eq '"')
+            {
+            if ($noParenthesisParameters || $symbolStack[0] eq '(')
+                {  $parameter .= $token;  }
+            else
+                {  $beforeParameters .= $token;  };
+
+            if ($token eq $symbolStack[-1])
+                {  pop @symbolStack;  };
+            }
+
+        elsif ($token =~ /^[\(\[\{\<\'\"]$/)
+            {
+            if ($noParenthesisParameters || $symbolStack[0] eq '(')
+                {  $parameter .= $token;  }
+            else
+                {  $beforeParameters .= $token;  };
+
+            push @symbolStack, $token;
+            }
+
+        elsif ( ($token eq ')' && $symbolStack[-1] eq '(') ||
+                 ($token eq ']' && $symbolStack[-1] eq '[') ||
+                 ($token eq '}' && $symbolStack[-1] eq '{') ||
+                 ($token eq '>' && $symbolStack[-1] eq '<') )
+            {
+            if (!$noParenthesisParameters && $token eq ')' && scalar @symbolStack == 1 && $symbolStack[0] eq '(')
+                {
+                $afterParameters .= $token;
+                $isAfterParameters = 1;
+                }
+            else
+                {  $parameter .= $token;  };
+
+            pop @symbolStack;
+            }
+
+        elsif ($token eq ',')
+            {
+            if (!scalar @symbolStack)
+                {
+                if ($noParenthesisParameters)
+                    {
+                    push @parameterLines, $parameter . $token;
+                    $parameter = undef;
+                    }
+                else
+                    {
+                    $beforeParameters .= $token;
+                    };
+                }
+            else
+                {
+                if (scalar @symbolStack == 1 && $symbolStack[0] eq '(' && !$noParenthesisParameters)
+                    {
+                    push @parameterLines, $parameter . $token;
+                    $parameter = undef;
+                    }
+                else
+                    {
+                    $parameter .= $token;
+                    };
+                };
+            }
+
+        else
+            {
+            if ($noParenthesisParameters || $symbolStack[0] eq '(')
+                {  $parameter .= $token;  }
+            else
+                {  $beforeParameters .= $token;  };
+            };
+        };
+
+    push @parameterLines, $parameter;
+
+    foreach my $item (\$beforeParameters, \$afterParameters)
+        {
+        $$item =~ s/^ //;
+        $$item =~ s/ $//;
+        }
+
+    my $prototypeObject = NaturalDocs::Languages::Prototype->New($beforeParameters, $afterParameters);
+
+
+    # Parse the actual parameters.
+
+    foreach my $parameterLine (@parameterLines)
+        {
+        $prototypeObject->AddParameter( $self->ParseParameterLine($parameterLine) );
+        };
+
+    return $prototypeObject;
+    };
+
+
+#
+#   Function: ParseParameterLine
+#
+#   Parses a prototype parameter line and returns it as a <NaturalDocs::Languages::Prototype::Parameter> object.
+#
+sub ParseParameterLine #(line)
+    {
+    my ($self, $line) = @_;
+
+    $line =~ s/^ //;
+    $line =~ s/ $//;
+
+    my @tokens = $line =~ /([^\(\)\[\]\{\}\<\>\'\"\:\=\ ]+|\:\=|.)/g;
+
+    my ($name, $type, $defaultValue, $defaultValuePrefix, $inType, $inDefaultValue);
+
+
+    my @symbolStack;
+
+    foreach my $token (@tokens)
+        {
+        if ($inDefaultValue)
+            {  $defaultValue .= $token;  }
+
+        elsif ($symbolStack[-1] eq '\'' || $symbolStack[-1] eq '"')
+            {
+            if ($inType)
+                {  $type .= $token;  }
+            else
+                {  $name .= $token;  };
+
+            if ($token eq $symbolStack[-1])
+                {  pop @symbolStack;  };
+            }
+
+        elsif ($token =~ /^[\(\[\{\<\'\"]$/)
+            {
+            if ($inType)
+                {  $type .= $token;  }
+            else
+                {  $name .= $token;  };
+
+            push @symbolStack, $token;
+            }
+
+        elsif ( ($token eq ')' && $symbolStack[-1] eq '(') ||
+                 ($token eq ']' && $symbolStack[-1] eq '[') ||
+                 ($token eq '}' && $symbolStack[-1] eq '{') ||
+                 ($token eq '>' && $symbolStack[-1] eq '<') )
+            {
+            if ($inType)
+                {  $type .= $token;  }
+            else
+                {  $name .= $token;  };
+
+            pop @symbolStack;
+            }
+
+        elsif ($token eq ' ')
+            {
+            if ($inType)
+                {  $type .= $token;  }
+            elsif (!scalar @symbolStack)
+                {  $inType = 1;  }
+            else
+                {  $name .= $token;  };
+            }
+
+        elsif ($token eq ':=' || $token eq '=')
+            {
+            if (!scalar @symbolStack)
+                {
+                $defaultValuePrefix = $token;
+                $inDefaultValue = 1;
+                }
+            elsif ($inType)
+                {  $type .= $token;  }
+            else
+                {  $name .= $token;  };
+            }
+
+        else
+            {
+            if ($inType)
+                {  $type .= $token;  }
+            else
+                {  $name .= $token;  };
+            };
+        };
+
+    foreach my $part (\$type, \$defaultValue)
+        {
+        $$part =~ s/ $//;
+        };
+
+    return NaturalDocs::Languages::Prototype::Parameter->New($type, undef, $name, undef, $defaultValue, $defaultValuePrefix);
+    };
+
+
+sub TypeBeforeParameter
+    {  return 0;  };
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Languages/Pascal.pm b/docs/tool/Modules/NaturalDocs/Languages/Pascal.pm
new file mode 100644
index 00000000..e0242dec
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Languages/Pascal.pm
@@ -0,0 +1,143 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Languages::Pascal
+#
+###############################################################################
+#
+#   A subclass to handle the language variations of Pascal and Delphi.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Languages::Pascal;
+
+use base 'NaturalDocs::Languages::Simple';
+
+
+#
+#   hash: prototypeDirectives
+#
+#   An existence hash of all the directives that can appear after a function prototype and will be included.  The keys are the all
+#   lowercase keywords.
+#
+my %prototypeDirectives = ( 'overload' => 1,
+                                           'override' => 1,
+                                           'virtual' => 1,
+                                           'abstract' => 1,
+                                           'reintroduce' => 1,
+                                           'export' => 1,
+                                           'public' => 1,
+                                           'interrupt' => 1,
+                                           'register' => 1,
+                                           'pascal' => 1,
+                                           'cdecl' => 1,
+                                           'stdcall' => 1,
+                                           'popstack' => 1,
+                                           'saveregisters' => 1,
+                                           'inline' => 1,
+                                           'safecall' => 1 );
+
+#
+#   hash: longPrototypeDirectives
+#
+#   An existence hash of all the directives with parameters that can appear after a function prototype and will be included.  The
+#   keys are the all lowercase keywords.
+#
+my %longPrototypeDirectives = ( 'alias' => 1,
+                                                 'external' => 1 );
+
+#
+#   bool: checkingForDirectives
+#
+#   Set after the first function semicolon, which means we're in directives mode.
+#
+my $checkingForDirectives;
+
+
+#
+#   Function: OnCode
+#
+#   Just overridden to reset <checkingForDirectives>.
+#
+sub OnCode #(...)
+    {
+    my ($self, @parameters) = @_;
+
+    $checkingForDirectives = 0;
+
+    return $self->SUPER::OnCode(@parameters);
+    };
+
+
+#
+#   Function: OnPrototypeEnd
+#
+#   Pascal's syntax has directives after the prototype that should be included.
+#
+#   > function MyFunction ( param1: type ); virtual; abstract;
+#
+#   Parameters:
+#
+#       type - The <TopicType> of the prototype.
+#       prototypeRef - A reference to the prototype so far, minus the ender in dispute.
+#       ender - The ender symbol.
+#
+#   Returns:
+#
+#       ENDER_ACCEPT - The ender is accepted and the prototype is finished.
+#       ENDER_IGNORE - The ender is rejected and parsing should continue.  Note that the prototype will be rejected as a whole
+#                                  if all enders are ignored before reaching the end of the code.
+#       ENDER_ACCEPT_AND_CONTINUE - The ender is accepted so the prototype may stand as is.  However, the prototype might
+#                                                          also continue on so continue parsing.  If there is no accepted ender between here and
+#                                                          the end of the code this version will be accepted instead.
+#       ENDER_REVERT_TO_ACCEPTED - The expedition from ENDER_ACCEPT_AND_CONTINUE failed.  Use the last accepted
+#                                                        version and end parsing.
+#
+sub OnPrototypeEnd #(type, prototypeRef, ender)
+    {
+    my ($self, $type, $prototypeRef, $ender) = @_;
+
+    if ($type eq ::TOPIC_FUNCTION() && $ender eq ';')
+        {
+        if (!$checkingForDirectives)
+            {
+            $checkingForDirectives = 1;
+            return ::ENDER_ACCEPT_AND_CONTINUE();
+            }
+        elsif ($$prototypeRef =~ /;[ \t]*([a-z]+)([^;]*)$/i)
+            {
+            my ($lastDirective, $extra) = (lc($1), $2);
+
+            if (exists $prototypeDirectives{$lastDirective} && $extra =~ /^[ \t]*$/)
+                {  return ::ENDER_ACCEPT_AND_CONTINUE();  }
+            elsif (exists $longPrototypeDirectives{$lastDirective})
+                {  return ::ENDER_ACCEPT_AND_CONTINUE();  }
+            else
+                {  return ::ENDER_REVERT_TO_ACCEPTED();  };
+            }
+        else
+            {  return ::ENDER_REVERT_TO_ACCEPTED();  };
+        }
+    else
+        {  return ::ENDER_ACCEPT();  };
+    };
+
+
+sub ParseParameterLine #(...)
+    {
+    my ($self, @params) = @_;
+    return $self->SUPER::ParsePascalParameterLine(@params);
+    };
+
+sub TypeBeforeParameter
+    {
+    return 0;
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Languages/Perl.pm b/docs/tool/Modules/NaturalDocs/Languages/Perl.pm
new file mode 100644
index 00000000..8817aadc
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Languages/Perl.pm
@@ -0,0 +1,1370 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Languages::Perl
+#
+###############################################################################
+#
+#   A subclass to handle the language variations of Perl.
+#
+#
+#   Topic: Language Support
+#
+#       Supported:
+#
+#       - Packages
+#       - Inheritance via "use base" and "@ISA =".
+#       - Functions
+#       - Variables
+#
+#       Not supported yet:
+#
+#       - Constants
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Languages::Perl;
+
+use base 'NaturalDocs::Languages::Advanced';
+
+
+#
+#   array: hereDocTerminators
+#   An array of active Here Doc terminators, or an empty array if not active.  Each entry is an arrayref of tokens.  The entries
+#   must appear in the order they must appear in the source.
+#
+my @hereDocTerminators;
+
+
+
+###############################################################################
+# Group: Interface Functions
+
+
+#
+#   Function: PackageSeparator
+#   Returns the package separator symbol.
+#
+sub PackageSeparator
+    {  return '::';  };
+
+#
+#   Function: EnumValues
+#   Returns the <EnumValuesType> that describes how the language handles enums.
+#
+sub EnumValues
+    {  return ::ENUM_GLOBAL();  };
+
+
+#
+#   Function: ParseFile
+#
+#   Parses the passed source file, sending comments acceptable for documentation to <NaturalDocs::Parser->OnComment()>.
+#
+#   Parameters:
+#
+#       sourceFile - The name of the source file to parse.
+#       topicList - A reference to the list of <NaturalDocs::Parser::ParsedTopics> being built by the file.
+#
+#   Returns:
+#
+#       The array ( autoTopics, scopeRecord ).
+#
+#       autoTopics - An arrayref of automatically generated topics from the file, or undef if none.
+#       scopeRecord - An arrayref of <NaturalDocs::Languages::Advanced::ScopeChanges>, or undef if none.
+#
+sub ParseFile #(sourceFile, topicsList)
+    {
+    my ($self, $sourceFile, $topicsList) = @_;
+
+    @hereDocTerminators = ( );
+
+    # The regular block comment symbols are undef because they're all potentially JavaDoc comments.  PreprocessFile() will
+    # handle translating things like =begin naturaldocs and =begin javadoc to =begin nd.
+    $self->ParseForCommentsAndTokens($sourceFile, [ '#' ], undef, [ '##' ], [ '=begin nd', '=end nd' ]);
+
+    my $tokens = $self->Tokens();
+    my $index = 0;
+    my $lineNumber = 1;
+
+    while ($index < scalar @$tokens)
+        {
+        if ($self->TryToSkipWhitespace(\$index, \$lineNumber) ||
+            $self->TryToGetPackage(\$index, \$lineNumber) ||
+            $self->TryToGetBase(\$index, \$lineNumber) ||
+            $self->TryToGetFunction(\$index, \$lineNumber) ||
+            $self->TryToGetVariable(\$index, \$lineNumber) )
+            {
+            # The functions above will handle everything.
+            }
+
+        elsif ($tokens->[$index] eq '{')
+            {
+            $self->StartScope('}', $lineNumber, undef);
+            $index++;
+            }
+
+        elsif ($tokens->[$index] eq '}')
+            {
+            if ($self->ClosingScopeSymbol() eq '}')
+                {  $self->EndScope($lineNumber);  };
+
+            $index++;
+            }
+
+        elsif (lc($tokens->[$index]) eq 'eval')
+            {
+            # We want to skip the token in this case instead of letting it fall to SkipRestOfStatement.  This allows evals with braces
+            # to be treated like normal floating braces.
+            $index++;
+            }
+
+        else
+            {
+            $self->SkipRestOfStatement(\$index, \$lineNumber);
+            };
+        };
+
+
+    # Don't need to keep these around.
+    $self->ClearTokens();
+
+    return ( $self->AutoTopics(), $self->ScopeRecord() );
+    };
+
+
+#
+#   Function: PreprocessFile
+#
+#   Overridden to support "=begin nd" and similar.
+#
+#   - "=begin [nd|naturaldocs|natural docs|jd|javadoc|java doc]" all translate to "=begin nd".
+#   - "=[nd|naturaldocs|natural docs]" also translate to "=begin nd".
+#   - "=end [nd|naturaldocs|natural docs|jd|javadoc]" all translate to "=end nd".
+#   - "=cut" from a ND block translates into "=end nd", but the next line will be altered to begin with "(NDPODBREAK)".  This is
+#     so if there is POD leading into ND which ends with a cut, the parser can still end the original POD because the end ND line
+#     would have been removed.  Remember, <NaturalDocs::Languages::Advanced->ParseForCommentsAndTokens()> removes
+#     Natural Docs-worthy comments to save parsing time.
+#   - "=pod begin nd" and "=pod end nd" are supported for compatibility with ND 1.32 and earlier, even though the syntax is a
+#     mistake.
+#   - It also supports the wrong plural forms, so naturaldoc/natural doc/javadocs/java docs will work.
+#
+sub PreprocessFile #(lines)
+    {
+    my ($self, $lines) = @_;
+
+    my $inNDPOD = 0;
+    my $mustBreakPOD = 0;
+
+    for (my $i = 0; $i < scalar @$lines; $i++)
+        {
+        if ($lines->[$i] =~ /^\=(?:(?:pod[ \t]+)?begin[ \t]+)?(?:nd|natural[ \t]*docs?|jd|java[ \t]*docs?)[ \t]*$/i)
+            {
+            $lines->[$i] = '=begin nd';
+            $inNDPOD = 1;
+            $mustBreakPOD = 0;
+            }
+        elsif ($lines->[$i] =~ /^\=(?:pod[ \t]+)end[ \t]+(?:nd|natural[ \t]*docs?|jd|javadocs?)[ \t]*$/i)
+            {
+            $lines->[$i] = '=end nd';
+            $inNDPOD = 0;
+            $mustBreakPOD = 0;
+            }
+        elsif ($lines->[$i] =~ /^\=cut[ \t]*$/i)
+            {
+            if ($inNDPOD)
+                {
+                $lines->[$i] = '=end nd';
+                $inNDPOD = 0;
+                $mustBreakPOD = 1;
+                };
+            }
+        elsif ($mustBreakPOD)
+            {
+            $lines->[$i] = '(NDPODBREAK)' . $lines->[$i];
+            $mustBreakPOD = 0;
+            };
+        };
+    };
+
+
+
+###############################################################################
+# Group: Statement Parsing Functions
+# All functions here assume that the current position is at the beginning of a statement.
+#
+# Note for developers: I am well aware that the code in these functions do not check if we're past the end of the tokens as
+# often as it should.  We're making use of the fact that Perl will always return undef in these cases to keep the code simpler.
+
+
+#
+#   Function: TryToGetPackage
+#
+#   Determines whether the position is at a package declaration statement, and if so, generates a topic for it, skips it, and
+#   returns true.
+#
+sub TryToGetPackage #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    if (lc($tokens->[$$indexRef]) eq 'package')
+        {
+        my $index = $$indexRef + 1;
+        my $lineNumber = $$lineNumberRef;
+
+        if (!$self->TryToSkipWhitespace(\$index, \$lineNumber))
+            {  return undef;  };
+
+        my $name;
+
+        while ($tokens->[$index] =~ /^[a-z_\:]/i)
+            {
+            $name .= $tokens->[$index];
+            $index++;
+            };
+
+        if (!defined $name)
+            {  return undef;  };
+
+        my $autoTopic = NaturalDocs::Parser::ParsedTopic->New(::TOPIC_CLASS(), $name,
+                                                                                             undef, undef,
+                                                                                             undef,
+                                                                                             undef, undef, $$lineNumberRef);
+        $self->AddAutoTopic($autoTopic);
+
+        NaturalDocs::Parser->OnClass($autoTopic->Symbol());
+
+        $self->SetPackage($autoTopic->Symbol(), $$lineNumberRef);
+
+        $$indexRef = $index;
+        $$lineNumberRef = $lineNumber;
+        $self->SkipRestOfStatement($indexRef, $lineNumberRef);
+
+        return 1;
+        };
+
+    return undef;
+    };
+
+
+#
+#   Function: TryToGetBase
+#
+#   Determines whether the position is at a package base declaration statement, and if so, calls
+#   <NaturalDocs::Parser->OnClassParent()>.
+#
+#   Supported Syntaxes:
+#
+#   > use base [list of strings]
+#   > @ISA = [list of strings]
+#   > @[package]::ISA = [list of strings]
+#   > our @ISA = [list of strings]
+#
+sub TryToGetBase #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my ($index, $lineNumber, $class, $parents);
+
+    if (lc($tokens->[$$indexRef]) eq 'use')
+        {
+        $index = $$indexRef + 1;
+        $lineNumber = $$lineNumberRef;
+
+        if (!$self->TryToSkipWhitespace(\$index, \$lineNumber) ||
+           lc($tokens->[$index]) ne 'base')
+            {  return undef;  }
+
+        $index++;
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+        $parents = $self->TryToGetListOfStrings(\$index, \$lineNumber);
+        }
+
+    else
+        {
+        $index = $$indexRef;
+        $lineNumber = $$lineNumberRef;
+
+        if (lc($tokens->[$index]) eq 'our')
+            {
+            $index++;
+            $self->TryToSkipWhitespace(\$index, \$lineNumber);
+            };
+
+        if ($tokens->[$index] eq '@')
+            {
+            $index++;
+
+            while ($index < scalar @$tokens)
+                {
+                if ($tokens->[$index] eq 'ISA')
+                    {
+                    $index++;
+                    $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+                    if ($tokens->[$index] eq '=')
+                        {
+                        $index++;
+                        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+                        $parents = $self->TryToGetListOfStrings(\$index, \$lineNumber);
+                        }
+                    else
+                        {  last;  };
+                    }
+
+                # If token isn't ISA...
+                elsif ($tokens->[$index] =~ /^[a-z0-9_:]/i)
+                    {
+                    $class .= $tokens->[$index];
+                    $index++;
+                    }
+                else
+                    {  last;  };
+                };
+            };
+        };
+
+    if (defined $parents)
+        {
+        if (defined $class)
+            {
+            $class =~ s/::$//;
+            my @classIdentifiers = split(/::/, $class);
+            $class = NaturalDocs::SymbolString->Join(@classIdentifiers);
+            }
+        else
+            {  $class = $self->CurrentScope();  };
+
+        foreach my $parent (@$parents)
+            {
+            my @parentIdentifiers = split(/::/, $parent);
+            my $parentSymbol = NaturalDocs::SymbolString->Join(@parentIdentifiers);
+
+            NaturalDocs::Parser->OnClassParent($class, $parentSymbol, undef, undef, ::RESOLVE_ABSOLUTE());
+            };
+
+        $$indexRef = $index;
+        $$lineNumberRef = $lineNumber;
+        $self->SkipRestOfStatement($indexRef, $lineNumberRef);
+
+        return 1;
+        }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: TryToGetFunction
+#
+#   Determines whether the position is at a function declaration statement, and if so, generates a topic for it, skips it, and
+#   returns true.
+#
+sub TryToGetFunction #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    if ( lc($tokens->[$$indexRef]) eq 'sub')
+        {
+        my $prototypeStart = $$indexRef;
+        my $prototypeStartLine = $$lineNumberRef;
+        my $prototypeEnd = $$indexRef + 1;
+        my $prototypeEndLine = $$lineNumberRef;
+
+        if ( !$self->TryToSkipWhitespace(\$prototypeEnd, \$prototypeEndLine) ||
+             $tokens->[$prototypeEnd] !~ /^[a-z_]/i )
+            {  return undef;  };
+
+        my $name = $tokens->[$prototypeEnd];
+        $prototypeEnd++;
+
+        # We parsed 'sub [name]'.  Now keep going until we find a semicolon or a brace.
+
+        for (;;)
+            {
+            if ($prototypeEnd >= scalar @$tokens)
+                {  return undef;  }
+
+            # End if we find a semicolon, since it means we found a predeclaration rather than an actual function.
+            elsif ($tokens->[$prototypeEnd] eq ';')
+                {  return undef;  }
+
+            elsif ($tokens->[$prototypeEnd] eq '{')
+                {
+                # Found it!
+
+                my $prototype = $self->NormalizePrototype( $self->CreateString($prototypeStart, $prototypeEnd) );
+
+                $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New(::TOPIC_FUNCTION(), $name,
+                                                                                                          $self->CurrentScope(), undef,
+                                                                                                          $prototype,
+                                                                                                          undef, undef, $prototypeStartLine));
+
+                $$indexRef = $prototypeEnd;
+                $$lineNumberRef = $prototypeEndLine;
+
+                $self->SkipRestOfStatement($indexRef, $lineNumberRef);
+
+                return 1;
+                }
+
+            else
+                {  $self->GenericSkip(\$prototypeEnd, \$prototypeEndLine, 0, 1);  };
+            };
+        }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: TryToGetVariable
+#
+#   Determines if the position is at a variable declaration statement, and if so, generates a topic for it, skips it, and returns
+#   true.
+#
+#   Supported Syntaxes:
+#
+#   - Supports variables declared with "my", "our", and "local".
+#   - Supports multiple declarations in one statement, such as "my ($x, $y);".
+#   - Supports types and attributes.
+#
+sub TryToGetVariable #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $firstToken = lc( $tokens->[$$indexRef] );
+
+    if ($firstToken eq 'my' || $firstToken eq 'our' || $firstToken eq 'local')
+        {
+        my $prototypeStart = $$indexRef;
+        my $prototypeStartLine = $$lineNumberRef;
+        my $prototypeEnd = $$indexRef + 1;
+        my $prototypeEndLine = $$lineNumberRef;
+
+        $self->TryToSkipWhitespace(\$prototypeEnd, \$prototypeEndLine);
+
+
+        # Get the type if present.
+
+        my $type;
+
+        if ($tokens->[$prototypeEnd] =~ /^[a-z\:]/i)
+            {
+            do
+                {
+                $type .= $tokens->[$prototypeEnd];
+                $prototypeEnd++;
+                }
+            while ($tokens->[$prototypeEnd] =~ /^[a-z\:]/i);
+
+            if (!$self->TryToSkipWhitespace(\$prototypeEnd, \$prototypeEndLine))
+                {  return undef;  };
+            };
+
+
+        # Get the name, or possibly names.
+
+        if ($tokens->[$prototypeEnd] eq '(')
+            {
+            # If there's multiple variables, we'll need to build a custom prototype for each one.  $firstToken already has the
+            # declaring word.  We're going to store each name in @names, and we're going to use $prototypeStart and
+            # $prototypeEnd to capture any properties appearing after the list.
+
+            my $name;
+            my @names;
+            my $hasComma = 0;
+
+            $prototypeStart = $prototypeEnd + 1;
+            $prototypeStartLine = $prototypeEndLine;
+
+            for (;;)
+                {
+                $self->TryToSkipWhitespace(\$prototypeStart, \$prototypeStartLine);
+
+                $name = $self->TryToGetVariableName(\$prototypeStart, \$prototypeStartLine);
+
+                if (!defined $name)
+                    {  return undef;  };
+
+                push @names, $name;
+
+                $self->TryToSkipWhitespace(\$prototypeStart, \$prototypeStartLine);
+
+                # We can have multiple commas in a row.  We can also have trailing commas.  However, the parenthesis must
+                # not start with a comma or be empty, hence this logic does not appear earlier.
+                while ($tokens->[$prototypeStart] eq ',')
+                    {
+                    $prototypeStart++;
+                    $self->TryToSkipWhitespace(\$prototypeStart, \$prototypeStartLine);
+
+                    $hasComma = 1;
+                    }
+
+                if ($tokens->[$prototypeStart] eq ')')
+                    {
+                    $prototypeStart++;
+                    last;
+                    }
+                elsif (!$hasComma)
+                    {  return undef;  };
+                };
+
+
+            # Now find the end of the prototype.
+
+            $prototypeEnd = $prototypeStart;
+            $prototypeEndLine = $prototypeStartLine;
+
+            while ($prototypeEnd < scalar @$tokens &&
+                     $tokens->[$prototypeEnd] !~ /^[\;\=]/)
+                {
+                $prototypeEnd++;
+                };
+
+
+            my $prototypePrefix = $firstToken . ' ';
+            if (defined $type)
+                {  $prototypePrefix .= $type . ' ';  };
+
+            my $prototypeSuffix = ' ' . $self->CreateString($prototypeStart, $prototypeEnd);
+
+            foreach $name (@names)
+                {
+                my $prototype = $self->NormalizePrototype( $prototypePrefix . $name . $prototypeSuffix );
+
+                $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New(::TOPIC_VARIABLE(), $name,
+                                                                                                           $self->CurrentScope(), undef,
+                                                                                                           $prototype,
+                                                                                                           undef, undef, $prototypeStartLine));
+                };
+
+            $self->SkipRestOfStatement(\$prototypeEnd, \$prototypeEndLine);
+
+            $$indexRef = $prototypeEnd;
+            $$lineNumberRef = $prototypeEndLine;
+            }
+
+        else # no parenthesis
+            {
+            my $name = $self->TryToGetVariableName(\$prototypeEnd, \$prototypeEndLine);
+
+            if (!defined $name)
+                {  return undef;  };
+
+            while ($prototypeEnd < scalar @$tokens &&
+                     $tokens->[$prototypeEnd] !~ /^[\;\=]/)
+                {
+                $prototypeEnd++;
+                };
+
+            my $prototype = $self->NormalizePrototype( $self->CreateString($prototypeStart, $prototypeEnd) );
+
+            $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New(::TOPIC_VARIABLE(), $name,
+                                                                                                       $self->CurrentScope(), undef,
+                                                                                                       $prototype,
+                                                                                                       undef, undef, $prototypeStartLine));
+
+            $self->SkipRestOfStatement(\$prototypeEnd, \$prototypeEndLine);
+
+            $$indexRef = $prototypeEnd;
+            $$lineNumberRef = $prototypeEndLine;
+            };
+
+        return 1;
+        }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: TryToGetVariableName
+#
+#   Determines if the position is at a variable name, and if so, skips it and returns the name.
+#
+sub TryToGetVariableName #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $name;
+
+    if ($tokens->[$$indexRef] =~ /^[\$\@\%\*]/)
+        {
+        $name .= $tokens->[$$indexRef];
+        $$indexRef++;
+
+        $self->TryToSkipWhitespace($indexRef, $lineNumberRef);
+
+        if ($tokens->[$$indexRef] =~ /^[a-z_]/i)
+            {
+            $name .= $tokens->[$$indexRef];
+            $$indexRef++;
+            }
+        else
+            {  return undef;  };
+        };
+
+    return $name;
+    };
+
+
+#
+#   Function: TryToGetListOfStrings
+#
+#   Attempts to retrieve a list of strings from the current position.  Returns an arrayref of them if any are found, or undef if none.
+#   It stops the moment it reaches a non-string, so "string1, variable, string2" will only return string1.
+#
+#   Supported Syntaxes:
+#
+#   - Supports parenthesis.
+#   - Supports all string forms supported by <TryToSkipString()>.
+#   - Supports qw() string arrays.
+#
+sub TryToGetListOfStrings #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $parenthesis = 0;
+    my $strings;
+
+    while ($$indexRef < scalar @$tokens)
+        {
+        # We'll tolerate parenthesis.
+        if ($tokens->[$$indexRef] eq '(')
+            {
+            $$indexRef++;
+            $parenthesis++;
+            }
+        elsif ($tokens->[$$indexRef] eq ')')
+            {
+            if ($parenthesis == 0)
+                {  last;  };
+
+            $$indexRef++;
+            $parenthesis--;
+            }
+        elsif ($tokens->[$$indexRef] eq ',')
+            {
+            $$indexRef++;
+            }
+        else
+            {
+            my ($startContent, $endContent);
+            my $symbolIndex = $$indexRef;
+
+            if ($self->TryToSkipString($indexRef, $lineNumberRef, \$startContent, \$endContent))
+                {
+                my $content = $self->CreateString($startContent, $endContent);
+
+                if (!defined $strings)
+                    {  $strings = [ ];  };
+
+                if (lc($tokens->[$symbolIndex]) eq 'qw')
+                    {
+                    $content =~ tr/ \t\n/ /s;
+                    $content =~ s/^ //;
+
+                    my @qwStrings = split(/ /, $content);
+
+                    push @$strings, @qwStrings;
+                    }
+                else
+                    {
+                    push @$strings, $content;
+                    };
+                }
+            else
+                {  last;  };
+            };
+
+        $self->TryToSkipWhitespace($indexRef, $lineNumberRef);
+        };
+
+    return $strings;
+    };
+
+
+###############################################################################
+# Group: Low Level Parsing Functions
+
+
+#
+#   Function: GenericSkip
+#
+#   Advances the position one place through general code.
+#
+#   - If the position is on a comment or string, it will skip it completely.
+#   - If the position is on an opening symbol, it will skip until the past the closing symbol.
+#   - If the position is on a regexp or quote-like operator, it will skip it completely.
+#   - If the position is on a backslash, it will skip it and the following token.
+#   - If the position is on whitespace (including comments), it will skip it completely.
+#   - Otherwise it skips one token.
+#
+#   Parameters:
+#
+#       indexRef - A reference to the current index.
+#       lineNumberRef - A reference to the current line number.
+#       noRegExps - If set, does not test for regular expressions.
+#
+sub GenericSkip #(indexRef, lineNumberRef, noRegExps)
+    {
+    my ($self, $indexRef, $lineNumberRef, $noRegExps, $allowStringedClosingParens) = @_;
+    my $tokens = $self->Tokens();
+
+    if ($tokens->[$$indexRef] eq "\\" && $$indexRef + 1 < scalar @$tokens && $tokens->[$$indexRef+1] ne "\n")
+        {  $$indexRef += 2;  }
+
+    # Note that we don't want to count backslashed ()[]{} since they could be in regexps.  Also, ()[] are valid variable names
+    # when preceded by a string.
+
+    # We can ignore the scope stack because we're just skipping everything without parsing, and we need recursion anyway.
+    elsif ($tokens->[$$indexRef] eq '{' && !$self->IsBackslashed($$indexRef))
+        {
+        $$indexRef++;
+        $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}', $noRegExps, $allowStringedClosingParens);
+        }
+    elsif ($tokens->[$$indexRef] eq '(' && !$self->IsBackslashed($$indexRef) && !$self->IsStringed($$indexRef))
+        {
+        # Temporarily allow stringed closing parenthesis if it looks like we're in an anonymous function declaration with Perl's
+        # cheap version of prototypes, such as "my $_declare = sub($) {}".
+        my $tempAllowStringedClosingParens = $allowStringedClosingParens;
+        if (!$allowStringedClosingParens)
+        	{
+        	my $tempIndex = $$indexRef - 1;
+        	if ($tempIndex >= 0 && $tokens->[$tempIndex] =~ /^[ \t]/)
+        		{  $tempIndex--;  }
+        	if ($tempIndex >= 0 && $tokens->[$tempIndex] eq 'sub')
+        		{  $tempAllowStringedClosingParens = 1;  }
+        	}
+
+        $$indexRef++;
+
+        do
+            {  $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ')', $noRegExps, $tempAllowStringedClosingParens);  }
+        while ($$indexRef < scalar @$tokens && $self->IsStringed($$indexRef - 1) && !$tempAllowStringedClosingParens);
+        }
+    elsif ($tokens->[$$indexRef] eq '[' && !$self->IsBackslashed($$indexRef) && !$self->IsStringed($$indexRef))
+        {
+        $$indexRef++;
+
+        do
+            {  $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ']', $noRegExps, $allowStringedClosingParens);  }
+        while ($$indexRef < scalar @$tokens && $self->IsStringed($$indexRef - 1));
+        }
+
+    elsif ($self->TryToSkipWhitespace($indexRef, $lineNumberRef) ||
+            $self->TryToSkipString($indexRef, $lineNumberRef) ||
+            $self->TryToSkipHereDocDeclaration($indexRef, $lineNumberRef) ||
+            (!$noRegExps && $self->TryToSkipRegexp($indexRef, $lineNumberRef) ) )
+        {
+        }
+
+    else
+        {  $$indexRef++;  };
+    };
+
+
+#
+#   Function: GenericSkipUntilAfter
+#
+#   Advances the position via <GenericSkip()> until a specific token is reached and passed.
+#
+sub GenericSkipUntilAfter #(indexRef, lineNumberRef, token, noRegExps, allowStringedClosingParens)
+    {
+    my ($self, $indexRef, $lineNumberRef, $token, $noRegExps, $allowStringedClosingParens) = @_;
+    my $tokens = $self->Tokens();
+
+    while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] ne $token)
+        {  $self->GenericSkip($indexRef, $lineNumberRef, $noRegExps, $allowStringedClosingParens);  };
+
+    if ($tokens->[$$indexRef] eq "\n")
+        {  $$lineNumberRef++;  };
+    $$indexRef++;
+    };
+
+
+#
+#   Function: GenericRegexpSkip
+#
+#   Advances the position one place through regexp code.
+#
+#   - If the position is on an opening symbol, it will skip until the past the closing symbol.
+#   - If the position is on a backslash, it will skip it and the following token.
+#   - If the position is on whitespace (not including comments), it will skip it completely.
+#   - Otherwise it skips one token.
+#
+#   Also differs from <GenericSkip()> in that the parenthesis in $( and $) do count against the scope, where they wouldn't
+#   normally.
+#
+#   Parameters:
+#
+#       indexRef - A reference to the current index.
+#       lineNumberRef - A reference to the current line number.
+#       inBrackets - Whether we're in brackets or not.  If true, we don't care about matching braces and parenthesis.
+#
+sub GenericRegexpSkip #(indexRef, lineNumberRef, inBrackets)
+    {
+    my ($self, $indexRef, $lineNumberRef, $inBrackets) = @_;
+    my $tokens = $self->Tokens();
+
+    if ($tokens->[$$indexRef] eq "\\" && $$indexRef + 1 < scalar @$tokens && $tokens->[$$indexRef+1] ne "\n")
+        {  $$indexRef += 2;  }
+
+    # We can ignore the scope stack because we're just skipping everything without parsing, and we need recursion anyway.
+    elsif ($tokens->[$$indexRef] eq '{' && !$self->IsBackslashed($$indexRef) && !$inBrackets)
+        {
+        $$indexRef++;
+        $self->GenericRegexpSkipUntilAfter($indexRef, $lineNumberRef, '}');
+        }
+    elsif ($tokens->[$$indexRef] eq '(' && !$self->IsBackslashed($$indexRef) && !$inBrackets)
+        {
+        $$indexRef++;
+        $self->GenericRegexpSkipUntilAfter($indexRef, $lineNumberRef, ')');
+        }
+    elsif ($tokens->[$$indexRef] eq '[' && !$self->IsBackslashed($$indexRef) && !$self->IsStringed($$indexRef))
+        {
+        $$indexRef++;
+
+        do
+            {  $self->GenericRegexpSkipUntilAfter($indexRef, $lineNumberRef, ']');  }
+        while ($$indexRef < scalar @$tokens && $self->IsStringed($$indexRef - 1));
+        }
+
+    elsif ($tokens->[$$indexRef] eq "\n")
+        {
+        $$lineNumberRef++;
+        $$indexRef++;
+        }
+
+    else
+        {  $$indexRef++;  };
+    };
+
+
+#
+#   Function: GenericRegexpSkipUntilAfter
+#
+#   Advances the position via <GenericRegexpSkip()> until a specific token is reached and passed.
+#
+sub GenericRegexpSkipUntilAfter #(indexRef, lineNumberRef, token)
+    {
+    my ($self, $indexRef, $lineNumberRef, $token) = @_;
+    my $tokens = $self->Tokens();
+
+    my $inBrackets = ( $token eq ']' );
+
+    while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] ne $token)
+        {  $self->GenericRegexpSkip($indexRef, $lineNumberRef, $inBrackets);  };
+
+    if ($tokens->[$$indexRef] eq "\n")
+        {  $$lineNumberRef++;  };
+    $$indexRef++;
+    };
+
+
+#
+#   Function: SkipRestOfStatement
+#
+#   Advances the position via <GenericSkip()> until after the end of the current statement, which is defined as a semicolon or
+#   a brace group.  Of course, either of those appearing inside parenthesis, a nested brace group, etc. don't count.
+#
+sub SkipRestOfStatement #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    while ($$indexRef < scalar @$tokens &&
+             $tokens->[$$indexRef] ne ';' &&
+             !($tokens->[$$indexRef] eq '{' && !$self->IsStringed($$indexRef)) )
+        {
+        $self->GenericSkip($indexRef, $lineNumberRef);
+        };
+
+    if ($tokens->[$$indexRef] eq ';')
+        {  $$indexRef++;  }
+    elsif ($tokens->[$$indexRef] eq '{')
+        {  $self->GenericSkip($indexRef, $lineNumberRef);  };
+    };
+
+
+#
+#   Function: TryToSkipWhitespace
+#
+#   If the current position is on whitespace it skips them and returns true.  If there are a number of these in a row, it skips them
+#   all.
+#
+#   Supported Syntax:
+#
+#       - Whitespace
+#       - Line break
+#       - All comment forms supported by <TryToSkipComment()>
+#       - Here Doc content
+#
+sub TryToSkipWhitespace #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $result;
+
+    while ($$indexRef < scalar @$tokens)
+        {
+        if ($self->TryToSkipHereDocContent($indexRef, $lineNumberRef) ||
+            $self->TryToSkipComment($indexRef, $lineNumberRef))
+            {
+            $result = 1;
+            }
+        elsif ($tokens->[$$indexRef] =~ /^[ \t]/)
+            {
+            $$indexRef++;
+            $result = 1;
+            }
+        elsif ($tokens->[$$indexRef] eq "\n")
+            {
+            $$indexRef++;
+            $$lineNumberRef++;
+            $result = 1;
+            }
+        else
+            {  last;  };
+        };
+
+    return $result;
+    };
+
+
+#
+#   Function: TryToSkipComment
+#   If the current position is on a comment, skip past it and return true.
+#
+sub TryToSkipComment #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+
+    return ( $self->TryToSkipLineComment($indexRef, $lineNumberRef) ||
+                $self->TryToSkipPODComment($indexRef, $lineNumberRef) );
+    };
+
+
+#
+#   Function: TryToSkipLineComment
+#   If the current position is on a line comment symbol, skip past it and return true.
+#
+sub TryToSkipLineComment #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    # Note that $#var is not a comment.
+    if ($tokens->[$$indexRef] eq '#' && !$self->IsStringed($$indexRef))
+        {
+        $self->SkipRestOfLine($indexRef, $lineNumberRef);
+        return 1;
+        }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: TryToSkipPODComment
+#   If the current position is on a POD comment symbol, skip past it and return true.
+#
+sub TryToSkipPODComment #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    # Note that whitespace is not allowed before the equals sign.  It must directly start a line.
+    if ($tokens->[$$indexRef] eq '=' &&
+        ( $$indexRef == 0 || $tokens->[$$indexRef - 1] eq "\n" ) &&
+        $tokens->[$$indexRef + 1] =~ /^[a-z]/i )
+        {
+        # Skip until =cut or (NDPODBREAK).  Note that it's theoretically possible for =cut to appear without a prior POD directive.
+
+        do
+            {
+            if ($tokens->[$$indexRef] eq '=' && lc( $tokens->[$$indexRef + 1] ) eq 'cut')
+                {
+                $self->SkipRestOfLine($indexRef, $lineNumberRef);
+                last;
+                }
+            elsif ($tokens->[$$indexRef] eq '(' && $$indexRef + 2 < scalar @$tokens &&
+                    $tokens->[$$indexRef+1] eq 'NDPODBREAK' && $tokens->[$$indexRef+2] eq ')')
+                {
+                $$indexRef += 3;
+                last;
+                }
+            else
+                {
+                $self->SkipRestOfLine($indexRef, $lineNumberRef);
+                };
+            }
+        while ($$indexRef < scalar @$tokens);
+
+        return 1;
+        }
+
+    # It's also possible that (NDPODBREAK) will appear without any opening pod statement because "=begin nd" and "=cut" will
+    # still result in one.  We need to pick off the stray (NDPODBREAK).
+    elsif ($tokens->[$$indexRef] eq '(' && $$indexRef + 2 < scalar @$tokens &&
+            $tokens->[$$indexRef+1] eq 'NDPODBREAK' && $tokens->[$$indexRef+2] eq ')')
+        {
+        $$indexRef += 3;
+        return 1;
+        }
+
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: TryToSkipString
+#   If the current position is on a string delimiter, skip past the string and return true.
+#
+#   Parameters:
+#
+#       indexRef - A reference to the index of the position to start at.
+#       lineNumberRef - A reference to the line number of the position.
+#       startContentIndexRef - A reference to the variable in which to store the index of the first content token.  May be undef.
+#       endContentIndexRef - A reference to the variable in which to store the index of the end of the content, which is one past
+#                                        the last content token.  may be undef.
+#
+#   Returns:
+#
+#       Whether the position was at a string.  The index, line number, and content index variabls will only be changed if true.
+#
+#   Syntax Support:
+#
+#       - Supports quotes, apostrophes, backticks, q(), qq(), qx(), and qw().
+#       - All symbols are supported for the letter forms.
+#
+sub TryToSkipString #(indexRef, lineNumberRef, startContentIndexRef, endContentIndexRef)
+    {
+    my ($self, $indexRef, $lineNumberRef, $startContentIndexRef, $endContentIndexRef) = @_;
+    my $tokens = $self->Tokens();
+
+    # The three string delimiters.  All three are Perl variables when preceded by a dollar sign.
+    if (!$self->IsStringed($$indexRef) &&
+        ( $self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '\'', '\'', $startContentIndexRef, $endContentIndexRef) ||
+          $self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '"', '"', $startContentIndexRef, $endContentIndexRef) ||
+          $self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '`', '`', $startContentIndexRef, $endContentIndexRef) ) )
+        {
+        return 1;
+        }
+    elsif ($tokens->[$$indexRef] =~ /^(?:q|qq|qx|qw)$/i &&
+            ($$indexRef == 0 || $tokens->[$$indexRef - 1] !~ /^[\$\%\@\*]$/))
+        {
+        $$indexRef++;
+
+        $self->TryToSkipWhitespace($indexRef, $lineNumberRef);
+
+        my $openingSymbol = $tokens->[$$indexRef];
+        my $closingSymbol;
+
+        if ($openingSymbol eq '{')
+            {  $closingSymbol = '}';  }
+        elsif ($openingSymbol eq '(')
+            {  $closingSymbol = ')';  }
+        elsif ($openingSymbol eq '[')
+            {  $closingSymbol = ']';  }
+        elsif ($openingSymbol eq '<')
+            {  $closingSymbol = '>';  }
+        else
+            {  $closingSymbol = $openingSymbol;  };
+
+        $self->SUPER::TryToSkipString($indexRef, $lineNumberRef, $openingSymbol, $closingSymbol,
+                                                      $startContentIndexRef, $endContentIndexRef);
+
+        return 1;
+        }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: TryToSkipHereDocDeclaration
+#
+#   If the current position is on a Here Doc declaration, add its terminators to <hereDocTerminators> and skip it.
+#
+#   Syntax Support:
+#
+#       - Supports <<EOF
+#       - Supports << "String" with all string forms supported by <TryToSkipString()>.
+#
+sub TryToSkipHereDocDeclaration #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $index = $$indexRef;
+    my $lineNumber = $$lineNumberRef;
+
+    if ($tokens->[$index] eq '<' && $tokens->[$index + 1] eq '<')
+        {
+        $index += 2;
+        my $success;
+
+        # No whitespace allowed with the bare word.
+        if ($tokens->[$index] =~ /^[a-z0-9_]/i)
+            {
+            push @hereDocTerminators, [ $tokens->[$index] ];
+            $index++;
+            $success = 1;
+            }
+        else
+            {
+            $self->TryToSkipWhitespace(\$index, \$lineNumber);
+
+            my ($contentStart, $contentEnd);
+            if ($self->TryToSkipString(\$index, \$lineNumber, \$contentStart, \$contentEnd))
+                {
+                push @hereDocTerminators, [ @{$tokens}[$contentStart..$contentEnd - 1] ];
+                $success = 1;
+                };
+            };
+
+        if ($success)
+            {
+            $$indexRef = $index;
+            $$lineNumberRef = $lineNumber;
+
+            return 1;
+            };
+        };
+
+    return 0;
+    };
+
+
+#
+#   Function: TryToSkipHereDocContent
+#
+#   If the current position is at the beginning of a line and there are entries in <hereDocTerminators>, skips lines until all the
+#   terminators are exhausted or we reach the end of the file.
+#
+#   Returns:
+#
+#       Whether the position was on Here Doc content.
+#
+sub TryToSkipHereDocContent #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    # We don't use IsFirstLineToken() because it really needs to be the first line token.  Whitespace is not allowed.
+    if ($$indexRef > 0 && $tokens->[$$indexRef - 1] eq "\n")
+        {
+        my $success = (scalar @hereDocTerminators > 0);
+
+        while (scalar @hereDocTerminators && $$indexRef < scalar @$tokens)
+            {
+            my $terminatorIndex = 0;
+
+            while ($hereDocTerminators[0]->[$terminatorIndex] eq $tokens->[$$indexRef])
+                {
+                $terminatorIndex++;
+                $$indexRef++;
+                };
+
+            if ($terminatorIndex == scalar @{$hereDocTerminators[0]} &&
+                ($tokens->[$$indexRef] eq "\n" || ($tokens->[$$indexRef] =~ /^[ \t]/ && $tokens->[$$indexRef + 1] eq "\n")) )
+                {
+                shift @hereDocTerminators;
+                $$indexRef++;
+                $$lineNumberRef++;
+                }
+            else
+                {  $self->SkipRestOfLine($indexRef, $lineNumberRef);  };
+            };
+
+        return $success;
+        }
+
+    else
+        {  return 0;  };
+    };
+
+
+#
+#   Function: TryToSkipRegexp
+#   If the current position is on a regular expression or a quote-like operator, skip past it and return true.
+#
+#   Syntax Support:
+#
+#       - Supports //, ??, m//, qr//, s///, tr///, and y///.
+#       - All symbols are supported for the letter forms.
+#       - ?? is *not* supported because it could cause problems with ?: statements.  The generic parser has a good chance of
+#         successfully stumbling through a regex, whereas the regex code will almost certainly see the rest of the file as part of it.
+#
+sub TryToSkipRegexp #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $isRegexp;
+
+    # If it's a supported character sequence that's not a variable (ex $qr)...
+    if ($tokens->[$$indexRef] =~ /^(?:m|qr|s|tr|y)$/i &&
+         ($$indexRef == 0 || $tokens->[$$indexRef - 1] !~ /^[\$\%\@\*\-]$/) )
+        {  $isRegexp = 1;  }
+
+    elsif ($tokens->[$$indexRef] eq '/' && !$self->IsStringed($$indexRef))
+        {
+        # This is a bit of a hack.  If we find a random slash, it could be a divide operator or a bare regexp.  Find the first previous
+        # non-whitespace token and if it's text, a closing brace, or a string, assume it's a divide operator.  (Strings don't make
+        # much pratical sense there but a regexp would be impossible.)  Otherwise assume it's a regexp.
+
+        # We make a special consideration for split() appearing without parenthesis.  If the previous token is split and it's not a
+        # variable, assume it is a regexp even though it fails the above test.
+
+        my $index = $$indexRef - 1;
+
+        while ($index >= 0 && $tokens->[$index] =~ /^(?: |\t|\n)/)
+            {  $index--;  };
+
+        if ($index < 0 || $tokens->[$index] !~ /^[a-zA-Z0-9_\)\]\}\'\"\`]/ ||
+            ($tokens->[$index] =~ /^split|grep$/ && $index > 0 && $tokens->[$index-1] !~ /^[\$\%\@\*]$/) )
+            {  $isRegexp = 1;  };
+        };
+
+    if ($isRegexp)
+        {
+        my $operator = lc($tokens->[$$indexRef]);
+        my $index = $$indexRef;
+        my $lineNumber = $$lineNumberRef;
+
+        if ($operator =~ /^[\?\/]/)
+            {  $operator = 'm';  }
+        else
+            {
+            $index++;
+
+            # Believe it or not, s#...# is allowed.  We can't pass over number signs here.
+            if ($tokens->[$index] ne '#')
+                {  $self->TryToSkipWhitespace(\$index, \$lineNumber);  };
+            };
+
+        if ($tokens->[$index] =~ /^\w/)
+            {  return undef;  };
+        if ($tokens->[$index] eq '=' && $tokens->[$index+1] eq '>')
+        	{  return undef;  };
+
+        my $openingSymbol = $tokens->[$index];
+        my $closingSymbol;
+
+        if ($openingSymbol eq '{')
+            {  $closingSymbol = '}';  }
+        elsif ($openingSymbol eq '(')
+            {  $closingSymbol = ')';  }
+        elsif ($openingSymbol eq '[')
+            {  $closingSymbol = ']';  }
+        elsif ($openingSymbol eq '<')
+            {  $closingSymbol = '>';  }
+        else
+            {  $closingSymbol = $openingSymbol;  };
+
+        $index++;
+
+        $self->GenericRegexpSkipUntilAfter(\$index, \$lineNumber, $closingSymbol);
+
+        $$indexRef = $index;
+        $$lineNumberRef = $lineNumber;
+
+        if ($operator =~ /^(?:s|tr|y)$/)
+            {
+            if ($openingSymbol ne $closingSymbol)
+                {
+                $self->TryToSkipWhitespace($indexRef, $lineNumberRef);
+
+                $openingSymbol = $tokens->[$index];
+
+                if ($openingSymbol eq '{')
+                    {  $closingSymbol = '}';  }
+                elsif ($openingSymbol eq '(')
+                    {  $closingSymbol = ')';  }
+                elsif ($openingSymbol eq '[')
+                    {  $closingSymbol = ']';  }
+                elsif ($openingSymbol eq '<')
+                    {  $closingSymbol = '>';  }
+                else
+                    {  $closingSymbol = $openingSymbol;  };
+
+                $$indexRef++;
+                };
+
+            if ($operator eq 's')
+                {
+                $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, $closingSymbol, 1);
+                }
+            else # ($operator eq 'tr' || $operator eq 'y')
+                {
+                while ($$indexRef < scalar @$tokens &&
+                          ($tokens->[$$indexRef] ne $closingSymbol || $self->IsBackslashed($$indexRef)) )
+                    {
+                    if ($tokens->[$$indexRef] eq "\n")
+                        {  $$lineNumberRef++;  };
+                    $$indexRef++;
+                    };
+
+                $$indexRef++;
+                };
+            };
+
+        # We want to skip any letters after the regexp.  Otherwise something like tr/a/b/s; could have the trailing s; interpreted
+        # as another regexp.  Whitespace is not allowed between the closing symbol and the letters.
+
+        if ($tokens->[$$indexRef] =~ /^[a-z]/i)
+            {  $$indexRef++;  };
+
+        return 1;
+        };
+
+    return undef;
+    };
+
+
+
+###############################################################################
+# Group: Support Functions
+
+
+#
+#   Function: IsStringed
+#
+#   Returns whether the position is after a string (dollar sign) character.  Returns false if it's preceded by two dollar signs so
+#   "if ($x == $$)" doesn't skip the closing parenthesis as stringed.
+#
+#   Parameters:
+#
+#       index - The index of the postition.
+#
+sub IsStringed #(index)
+    {
+    my ($self, $index) = @_;
+    my $tokens = $self->Tokens();
+
+    if ($index > 0 && $tokens->[$index - 1] eq '$' && !($index > 1 && $tokens->[$index - 2] eq '$'))
+        {  return 1;  }
+    else
+        {  return undef;  };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Languages/Prototype.pm b/docs/tool/Modules/NaturalDocs/Languages/Prototype.pm
new file mode 100644
index 00000000..3a038513
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Languages/Prototype.pm
@@ -0,0 +1,92 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Languages::Prototype
+#
+###############################################################################
+#
+#   A data class for storing parsed prototypes.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+use NaturalDocs::Languages::Prototype::Parameter;
+
+
+package NaturalDocs::Languages::Prototype;
+
+use NaturalDocs::DefineMembers 'BEFORE_PARAMETERS', 'BeforeParameters()', 'SetBeforeParameters()',
+                                                 'AFTER_PARAMETERS', 'AfterParameters()', 'SetAfterParameters()',
+                                                 'PARAMETERS', 'Parameters()';
+# Dependency: New(), constant order, no parents.
+
+
+#
+#   Function: New
+#
+#   Creates and returns a new prototype object.
+#
+#   Parameters:
+#
+#       beforeParameters - The part of the prototype before the parameter list.
+#       afterParameters - The part of the prototype after the parameter list.
+#
+#   You cannot set the parameters from here.  Use <AddParameter()>.
+#
+sub New #(beforeParameters, afterParameters)
+    {
+    my ($package, @params) = @_;
+
+    # Dependency: Constant order, no parents.
+
+    my $object = [ @params ];
+    bless $object, $package;
+
+    return $object;
+    };
+
+
+#
+#   Functions: Members
+#
+#   BeforeParameters - Returns the part of the prototype before the parameter list.  If there is no parameter list, this will be the
+#                                only thing that returns content.
+#   SetBeforeParameters - Replaces the part of the prototype before the parameter list.
+#   AfterParameters - Returns the part of the prototype after the parameter list, if any.
+#   SetAfterParameters - Replaces the part of the prototype after the parameter list.
+#   Parameters - Returns the parameter list as an arrayref of <NaturalDocs::Languages::Prototype::Parameters>, or undef if none.
+#
+
+#
+#   Function: AddParameter
+#
+#   Adds a <NaturalDocs::Languages::Prototype::Parameter> to the list.
+#
+sub AddParameter #(parameter)
+    {
+    my ($self, $parameter) = @_;
+
+    if (!defined $self->[PARAMETERS])
+        {  $self->[PARAMETERS] = [ ];  };
+
+    push @{$self->[PARAMETERS]}, $parameter;
+    };
+
+
+#
+#   Function: OnlyBeforeParameters
+#
+#   Returns whether <BeforeParameters()> is the only thing set.
+#
+sub OnlyBeforeParameters
+    {
+    my $self = shift;
+    return (!defined $self->[PARAMETERS] && !defined $self->[AFTER_PARAMETERS]);
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Languages/Prototype/Parameter.pm b/docs/tool/Modules/NaturalDocs/Languages/Prototype/Parameter.pm
new file mode 100644
index 00000000..2d8f6bec
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Languages/Prototype/Parameter.pm
@@ -0,0 +1,87 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Languages::Prototype::Parameter
+#
+###############################################################################
+#
+#   A data class for storing parsed prototype parameters.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Languages::Prototype::Parameter;
+
+use NaturalDocs::DefineMembers 'TYPE', 'Type()', 'SetType()',
+                                                 'TYPE_PREFIX', 'TypePrefix()', 'SetTypePrefix()',
+                                                 'NAME', 'Name()', 'SetName()',
+                                                 'NAME_PREFIX', 'NamePrefix()', 'SetNamePrefix()',
+                                                 'DEFAULT_VALUE', 'DefaultValue()', 'SetDefaultValue()',
+                                                 'DEFAULT_VALUE_PREFIX', 'DefaultValuePrefix()', 'SetDefaultValuePrefix()';
+# Dependency: New() depends on the order of these constants and that they don't inherit from another class.
+
+
+#
+#   Constants: Members
+#
+#   The object is implemented as a blessed arrayref, with the following constants as its indexes.
+#
+#       TYPE - The parameter type, if any.
+#       TYPE_PREFIX - The parameter type prefix which should be aligned separately, if any.
+#       NAME - The parameter name.
+#       NAME_PREFIX - The parameter name prefix which should be aligned separately, if any.
+#       DEFAULT_VALUE - The default value expression, if any.
+#       DEFAULT_VALUE_PREFIX - The default value prefix which should be aligned separately, if any.
+#
+
+#
+#   Function: New
+#
+#   Creates and returns a new prototype object.
+#
+#   Parameters:
+#
+#       type - The parameter type, if any.
+#       typePrefix - The parameter type prefix which should be aligned separately, if any.
+#       name - The parameter name.
+#       namePrefix - The parameter name prefix which should be aligned separately, if any.
+#       defaultValue - The default value expression, if any.
+#       defaultValuePrefix - The default value prefix which should be aligned separately, if any.
+#
+sub New #(type, typePrefix, name, namePrefix, defaultValue, defaultValuePrefix)
+    {
+    my ($package, @params) = @_;
+
+    # Dependency: This depends on the order of the parameters being the same as the order of the constants, and that the
+    # constants don't inherit from another class.
+
+    my $object = [ @params ];
+    bless $object, $package;
+
+    return $object;
+    };
+
+
+#
+#   Functions: Members
+#
+#   Type - The parameter type, if any.
+#   SetType - Replaces the parameter type.
+#   TypePrefix - The parameter type prefix, which should be aligned separately, if any.
+#   SetTypePrefix - Replaces the parameter type prefix.
+#   Name - The parameter name.
+#   SetName - Replaces the parameter name.
+#   NamePrefix - The parameter name prefix, which should be aligned separately, if any.
+#   SetNamePrefix - Replaces the parameter name prefix.
+#   DefaultValue - The default value expression, if any.
+#   SetDefaultValue - Replaces the default value expression.
+#   DefaultValuePrefix - The default value prefix, which should be aligned separately, if any.
+#   SetDefaultValuePrefix - Replaces the default value prefix.
+#
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Languages/Simple.pm b/docs/tool/Modules/NaturalDocs/Languages/Simple.pm
new file mode 100644
index 00000000..9d962b1c
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Languages/Simple.pm
@@ -0,0 +1,503 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Languages::Simple
+#
+###############################################################################
+#
+#   A class containing the characteristics of a particular programming language for basic support within Natural Docs.
+#   Also serves as a base class for languages that break from general conventions, such as not having parameter lists use
+#   parenthesis and commas.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Languages::Simple;
+
+use base 'NaturalDocs::Languages::Base';
+use base 'Exporter';
+
+our @EXPORT = ( 'ENDER_ACCEPT', 'ENDER_IGNORE', 'ENDER_ACCEPT_AND_CONTINUE', 'ENDER_REVERT_TO_ACCEPTED' );
+
+
+use NaturalDocs::DefineMembers 'LINE_COMMENT_SYMBOLS', 'LineCommentSymbols()', 'SetLineCommentSymbols() duparrayref',
+                                                 'BLOCK_COMMENT_SYMBOLS', 'BlockCommentSymbols()',
+                                                                                              'SetBlockCommentSymbols() duparrayref',
+                                                 'PROTOTYPE_ENDERS',
+                                                 'LINE_EXTENDER', 'LineExtender()', 'SetLineExtender()',
+                                                 'PACKAGE_SEPARATOR', 'PackageSeparator()',
+                                                 'PACKAGE_SEPARATOR_WAS_SET', 'PackageSeparatorWasSet()',
+                                                 'ENUM_VALUES', 'EnumValues()',
+                                                 'ENUM_VALUES_WAS_SET', 'EnumValuesWasSet()';
+
+#
+#   Function: New
+#
+#   Creates and returns a new object.
+#
+#   Parameters:
+#
+#       name - The name of the language.
+#
+sub New #(name)
+    {
+    my ($selfPackage, $name) = @_;
+
+    my $object = $selfPackage->SUPER::New($name);
+
+    $object->[ENUM_VALUES] = ::ENUM_GLOBAL();
+    $object->[PACKAGE_SEPARATOR] = '.';
+
+    return $object;
+    };
+
+
+#
+#   Functions: Members
+#
+#   LineCommentSymbols - Returns an arrayref of symbols that start a line comment, or undef if none.
+#   SetLineCommentSymbols - Replaces the arrayref of symbols that start a line comment.
+#   BlockCommentSymbols - Returns an arrayref of start/end symbol pairs that specify a block comment, or undef if none.  Pairs
+#                                        are specified with two consecutive array entries.
+#   SetBlockCommentSymbols - Replaces the arrayref of start/end symbol pairs that specify a block comment.  Pairs are
+#                                             specified with two consecutive array entries.
+#   LineExtender - Returns the symbol to ignore a line break in languages where line breaks are significant.
+#   SetLineExtender - Replaces the symbol to ignore a line break in languages where line breaks are significant.
+#   PackageSeparator - Returns the package separator symbol.
+#   PackageSeparatorWasSet - Returns whether the package separator symbol was ever changed from the default.
+#
+
+#
+#   Function: SetPackageSeparator
+#   Replaces the language's package separator string.
+#
+sub SetPackageSeparator #(separator)
+    {
+    my ($self, $separator) = @_;
+    $self->[PACKAGE_SEPARATOR] = $separator;
+    $self->[PACKAGE_SEPARATOR_WAS_SET] = 1;
+    };
+
+
+#
+#   Functions: Members
+#
+#   EnumValues - Returns the <EnumValuesType> that describes how the language handles enums.
+#   EnumValuesWasSet - Returns whether <EnumValues> was ever changed from the default.
+
+
+#
+#   Function: SetEnumValues
+#   Replaces the <EnumValuesType> that describes how the language handles enums.
+#
+sub SetEnumValues #(EnumValuesType newBehavior)
+    {
+    my ($self, $behavior) = @_;
+    $self->[ENUM_VALUES] = $behavior;
+    $self->[ENUM_VALUES_WAS_SET] = 1;
+    };
+
+
+#
+#   Function: PrototypeEndersFor
+#
+#   Returns an arrayref of prototype ender symbols for the passed <TopicType>, or undef if none.
+#
+sub PrototypeEndersFor #(type)
+    {
+    my ($self, $type) = @_;
+
+    if (defined $self->[PROTOTYPE_ENDERS])
+        {  return $self->[PROTOTYPE_ENDERS]->{$type};  }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: SetPrototypeEndersFor
+#
+#   Replaces the arrayref of prototype ender symbols for the passed <TopicType>.
+#
+sub SetPrototypeEndersFor #(type, enders)
+    {
+    my ($self, $type, $enders) = @_;
+
+    if (!defined $self->[PROTOTYPE_ENDERS])
+        {  $self->[PROTOTYPE_ENDERS] = { };  };
+
+    if (!defined $enders)
+        {  delete $self->[PROTOTYPE_ENDERS]->{$type};  }
+    else
+        {
+        $self->[PROTOTYPE_ENDERS]->{$type} = [ @$enders ];
+        };
+    };
+
+
+
+
+###############################################################################
+# Group: Parsing Functions
+
+
+#
+#   Function: ParseFile
+#
+#   Parses the passed source file, sending comments acceptable for documentation to <NaturalDocs::Parser->OnComment()>
+#   and all other sections to <OnCode()>.
+#
+#   Parameters:
+#
+#       sourceFile - The <FileName> of the source file to parse.
+#       topicList - A reference to the list of <NaturalDocs::Parser::ParsedTopics> being built by the file.
+#
+#   Returns:
+#
+#       Since this class cannot automatically document the code or generate a scope record, it always returns ( undef, undef ).
+#
+sub ParseFile #(sourceFile, topicsList)
+    {
+    my ($self, $sourceFile, $topicsList) = @_;
+
+    open(SOURCEFILEHANDLE, '<' . $sourceFile)
+        or die "Couldn't open input file " . $sourceFile . "\n";
+
+    my @commentLines;
+    my @codeLines;
+    my $lastCommentTopicCount = 0;
+
+    if ($self->Name() eq 'Text File')
+        {
+        my $line = <SOURCEFILEHANDLE>;
+
+        # On the very first line, remove a Unicode BOM if present.  Information on it available at:
+        # http://www.unicode.org/faq/utf_bom.html#BOM
+        $line =~ s/^\xEF\xBB\xBF//;
+
+        while ($line)
+            {
+            ::XChomp(\$line);
+            push @commentLines, $line;
+            $line = <SOURCEFILEHANDLE>;
+            };
+
+        NaturalDocs::Parser->OnComment(\@commentLines, 1);
+        }
+
+    else
+        {
+        my $line = <SOURCEFILEHANDLE>;
+        my $lineNumber = 1;
+
+        # On the very first line, remove a Unicode BOM if present.  Information on it available at:
+        # http://www.unicode.org/faq/utf_bom.html#BOM
+        $line =~ s/^\xEF\xBB\xBF//;
+
+        while (defined $line)
+            {
+            ::XChomp(\$line);
+            my $originalLine = $line;
+
+
+            # Retrieve single line comments.  This leaves $line at the next line.
+
+            if ($self->StripOpeningSymbols(\$line, $self->LineCommentSymbols()))
+                {
+                do
+                    {
+                    push @commentLines, $line;
+                    $line = <SOURCEFILEHANDLE>;
+
+                    if (!defined $line)
+                        {  goto EndDo;  };
+
+                    ::XChomp(\$line);
+                    }
+                while ($self->StripOpeningSymbols(\$line, $self->LineCommentSymbols()));
+
+                EndDo:  # I hate Perl sometimes.
+                }
+
+
+            # Retrieve multiline comments.  This leaves $line at the next line.
+
+            elsif (my $closingSymbol = $self->StripOpeningBlockSymbols(\$line, $self->BlockCommentSymbols()))
+                {
+                # Note that it is possible for a multiline comment to start correctly but not end so.  We want those comments to stay in
+                # the code.  For example, look at this prototype with this splint annotation:
+                #
+                # int get_array(integer_t id,
+                #                    /*@out@*/ array_t array);
+                #
+                # The annotation starts correctly but doesn't end so because it is followed by code on the same line.
+
+                my $lineRemainder;
+
+                for (;;)
+                    {
+                    $lineRemainder = $self->StripClosingSymbol(\$line, $closingSymbol);
+
+                    push @commentLines, $line;
+
+                    #  If we found an end comment symbol...
+                    if (defined $lineRemainder)
+                        {  last;  };
+
+                    $line = <SOURCEFILEHANDLE>;
+
+                    if (!defined $line)
+                        {  last;  };
+
+                    ::XChomp(\$line);
+                    };
+
+                if ($lineRemainder !~ /^[ \t]*$/)
+                    {
+                    # If there was something past the closing symbol this wasn't an acceptable comment, so move the lines to code.
+                    push @codeLines, @commentLines;
+                    @commentLines = ( );
+                    };
+
+                $line = <SOURCEFILEHANDLE>;
+                }
+
+
+            # Otherwise just add it to the code.
+
+            else
+                {
+                push @codeLines, $line;
+                $line = <SOURCEFILEHANDLE>;
+                };
+
+
+            # If there were comments, send them to Parser->OnComment().
+
+            if (scalar @commentLines)
+                {
+                # First process any code lines before the comment.
+                if (scalar @codeLines)
+                    {
+                    $self->OnCode(\@codeLines, $lineNumber, $topicsList, $lastCommentTopicCount);
+                    $lineNumber += scalar @codeLines;
+                    @codeLines = ( );
+                    };
+
+                $lastCommentTopicCount = NaturalDocs::Parser->OnComment(\@commentLines, $lineNumber);
+                $lineNumber += scalar @commentLines;
+                @commentLines = ( );
+                };
+
+            };  # while (defined $line)
+
+
+        # Clean up any remaining code.
+        if (scalar @codeLines)
+            {
+            $self->OnCode(\@codeLines, $lineNumber, $topicsList, $lastCommentTopicCount);
+            @codeLines = ( );
+            };
+
+        };
+
+    close(SOURCEFILEHANDLE);
+
+    return ( undef, undef );
+    };
+
+
+#
+#   Function: OnCode
+#
+#   Called whenever a section of code is encountered by the parser.  Is used to find the prototype of the last topic created.
+#
+#   Parameters:
+#
+#       codeLines - The source code as an arrayref of lines.
+#       codeLineNumber - The line number of the first line of code.
+#       topicList - A reference to the list of <NaturalDocs::Parser::ParsedTopics> being built by the file.
+#       lastCommentTopicCount - The number of Natural Docs topics that were created by the last comment.
+#
+sub OnCode #(codeLines, codeLineNumber, topicList, lastCommentTopicCount)
+    {
+    my ($self, $codeLines, $codeLineNumber, $topicList, $lastCommentTopicCount) = @_;
+
+    if ($lastCommentTopicCount && defined $self->PrototypeEndersFor($topicList->[-1]->Type()))
+        {
+        my $lineIndex = 0;
+        my $prototype;
+
+        # Skip all blank lines before a prototype.
+        while ($lineIndex < scalar @$codeLines && $codeLines->[$lineIndex] =~ /^[ \t]*$/)
+            {  $lineIndex++;  };
+
+        my @tokens;
+        my $tokenIndex = 0;
+
+        my @brackets;
+        my $enders = $self->PrototypeEndersFor($topicList->[-1]->Type());
+
+        # Add prototype lines until we reach the end of the prototype or the end of the code lines.
+        while ($lineIndex < scalar @$codeLines)
+            {
+            my $line = $self->RemoveLineExtender($codeLines->[$lineIndex] . "\n");
+
+            push @tokens, $line =~ /([^\(\)\[\]\{\}\<\>]+|.)/g;
+
+            while ($tokenIndex < scalar @tokens)
+                {
+                # If we're not inside brackets, check for ender symbols.
+                if (!scalar @brackets)
+                    {
+                    my $startingIndex = 0;
+                    my $testPrototype;
+
+                    for (;;)
+                        {
+                        my ($enderIndex, $ender) = ::FindFirstSymbol($tokens[$tokenIndex], $enders, $startingIndex);
+
+                        if ($enderIndex == -1)
+                            {  last;  }
+                        else
+                            {
+                            # We do this here so we don't duplicate prototype for every single token.  Just the first time an ender symbol
+                            # is found in one.
+                            if (!defined $testPrototype)
+                                {  $testPrototype = $prototype;  };
+
+                            $testPrototype .= substr($tokens[$tokenIndex], $startingIndex, $enderIndex - $startingIndex);
+
+                            my $enderResult;
+
+                            # If the ender is all text and the character preceding or following it is as well, ignore it.
+                            if ($ender =~ /^[a-z0-9]+$/i &&
+                                ( ($enderIndex > 0 && substr($tokens[$tokenIndex], $enderIndex - 1, 1) =~ /^[a-z0-9_]$/i) ||
+                                   substr($tokens[$tokenIndex], $enderIndex + length($ender), 1) =~ /^[a-z0-9_]$/i ) )
+                                {  $enderResult = ENDER_IGNORE();  }
+                            else
+                                {  $enderResult = $self->OnPrototypeEnd($topicList->[-1]->Type(), \$testPrototype, $ender);  }
+
+                            if ($enderResult == ENDER_IGNORE())
+                                {
+                                $testPrototype .= $ender;
+                                $startingIndex = $enderIndex + length($ender);
+                                }
+                            elsif ($enderResult == ENDER_REVERT_TO_ACCEPTED())
+                                {
+                                return;
+                                }
+                            else # ENDER_ACCEPT || ENDER_ACCEPT_AND_CONTINUE
+                                {
+                                my $titleInPrototype = $topicList->[-1]->Title();
+
+                                # Strip parenthesis so Function(2) and Function(int, int) will still match Function(anything).
+                                $titleInPrototype =~ s/[\t ]*\([^\(]*$//;
+
+                                if (index($testPrototype, $titleInPrototype) != -1)
+                                    {
+                                    $topicList->[-1]->SetPrototype( $self->NormalizePrototype($testPrototype) );
+                                    };
+
+                                if ($enderResult == ENDER_ACCEPT())
+                                    {  return;  }
+                                else # ENDER_ACCEPT_AND_CONTINUE
+                                    {
+                                    $testPrototype .= $ender;
+                                    $startingIndex = $enderIndex + length($ender);
+                                    };
+                                };
+                            };
+                        };
+                    }
+
+                # If we are inside brackets, check for closing symbols.
+                elsif ( ($tokens[$tokenIndex] eq ')' && $brackets[-1] eq '(') ||
+                         ($tokens[$tokenIndex] eq ']' && $brackets[-1] eq '[') ||
+                         ($tokens[$tokenIndex] eq '}' && $brackets[-1] eq '{') ||
+                         ($tokens[$tokenIndex] eq '>' && $brackets[-1] eq '<') )
+                    {
+                    pop @brackets;
+                    };
+
+                # Check for opening brackets.
+                if ($tokens[$tokenIndex] =~ /^[\(\[\{\<]$/)
+                    {
+                    push @brackets, $tokens[$tokenIndex];
+                    };
+
+                $prototype .= $tokens[$tokenIndex];
+                $tokenIndex++;
+                };
+
+            $lineIndex++;
+            };
+
+        # If we got out of that while loop by running out of lines, there was no prototype.
+        };
+    };
+
+
+use constant ENDER_ACCEPT => 1;
+use constant ENDER_IGNORE => 2;
+use constant ENDER_ACCEPT_AND_CONTINUE => 3;
+use constant ENDER_REVERT_TO_ACCEPTED => 4;
+
+#
+#   Function: OnPrototypeEnd
+#
+#   Called whenever the end of a prototype is found so that there's a chance for derived classes to mark false positives.
+#
+#   Parameters:
+#
+#       type - The <TopicType> of the prototype.
+#       prototypeRef - A reference to the prototype so far, minus the ender in dispute.
+#       ender - The ender symbol.
+#
+#   Returns:
+#
+#       ENDER_ACCEPT - The ender is accepted and the prototype is finished.
+#       ENDER_IGNORE - The ender is rejected and parsing should continue.  Note that the prototype will be rejected as a whole
+#                                  if all enders are ignored before reaching the end of the code.
+#       ENDER_ACCEPT_AND_CONTINUE - The ender is accepted so the prototype may stand as is.  However, the prototype might
+#                                                          also continue on so continue parsing.  If there is no accepted ender between here and
+#                                                          the end of the code this version will be accepted instead.
+#       ENDER_REVERT_TO_ACCEPTED - The expedition from ENDER_ACCEPT_AND_CONTINUE failed.  Use the last accepted
+#                                                        version and end parsing.
+#
+sub OnPrototypeEnd #(type, prototypeRef, ender)
+    {
+    return ENDER_ACCEPT();
+    };
+
+
+#
+#   Function: RemoveLineExtender
+#
+#   If the passed line has a line extender, returns it without the extender or the line break that follows.  If it doesn't, or there are
+#   no line extenders defined, returns the passed line unchanged.
+#
+sub RemoveLineExtender #(line)
+    {
+    my ($self, $line) = @_;
+
+    if (defined $self->LineExtender())
+        {
+        my $lineExtenderIndex = rindex($line, $self->LineExtender());
+
+        if ($lineExtenderIndex != -1 &&
+            substr($line, $lineExtenderIndex + length($self->LineExtender())) =~ /^[ \t]*\n$/)
+            {
+            $line = substr($line, 0, $lineExtenderIndex) . ' ';
+            };
+        };
+
+    return $line;
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Languages/Tcl.pm b/docs/tool/Modules/NaturalDocs/Languages/Tcl.pm
new file mode 100644
index 00000000..bd6b5a0d
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Languages/Tcl.pm
@@ -0,0 +1,219 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Languages::Tcl
+#
+###############################################################################
+#
+#   A subclass to handle the language variations of Tcl.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Languages::Tcl;
+
+use base 'NaturalDocs::Languages::Simple';
+
+
+#
+#   bool: pastFirstBrace
+#
+#   Whether we've past the first brace in a function prototype or not.
+#
+my $pastFirstBrace;
+
+
+#
+#   Function: OnCode
+#
+#   This is just overridden to reset <pastFirstBrace>.
+#
+sub OnCode #(...)
+    {
+    my ($self, @params) = @_;
+
+    $pastFirstBrace = 0;
+
+    return $self->SUPER::OnCode(@params);
+    };
+
+
+#
+#   Function: OnPrototypeEnd
+#
+#   Tcl's function syntax is shown below.
+#
+#   > proc [name] { [params] } { [code] }
+#
+#   The opening brace is one of the prototype enders.  We need to allow the first opening brace because it contains the
+#   parameters.
+#
+#   Also, the parameters may have braces within them.  I've seen one that used { seconds 20 } as a parameter.
+#
+#   Parameters:
+#
+#       type - The <TopicType> of the prototype.
+#       prototypeRef - A reference to the prototype so far, minus the ender in dispute.
+#       ender - The ender symbol.
+#
+#   Returns:
+#
+#       ENDER_ACCEPT - The ender is accepted and the prototype is finished.
+#       ENDER_IGNORE - The ender is rejected and parsing should continue.  Note that the prototype will be rejected as a whole
+#                                  if all enders are ignored before reaching the end of the code.
+#       ENDER_ACCEPT_AND_CONTINUE - The ender is accepted so the prototype may stand as is.  However, the prototype might
+#                                                          also continue on so continue parsing.  If there is no accepted ender between here and
+#                                                          the end of the code this version will be accepted instead.
+#       ENDER_REVERT_TO_ACCEPTED - The expedition from ENDER_ACCEPT_AND_CONTINUE failed.  Use the last accepted
+#                                                        version and end parsing.
+#
+sub OnPrototypeEnd #(type, prototypeRef, ender)
+    {
+    my ($self, $type, $prototypeRef, $ender) = @_;
+
+    if ($type eq ::TOPIC_FUNCTION() && $ender eq '{' && !$pastFirstBrace)
+        {
+        $pastFirstBrace = 1;
+        return ::ENDER_IGNORE();
+        }
+    else
+        {  return ::ENDER_ACCEPT();  };
+    };
+
+
+#
+#   Function: ParsePrototype
+#
+#   Parses the prototype and returns it as a <NaturalDocs::Languages::Prototype> object.
+#
+#   Parameters:
+#
+#       type - The <TopicType>.
+#       prototype - The text prototype.
+#
+#   Returns:
+#
+#       A <NaturalDocs::Languages::Prototype> object.
+#
+sub ParsePrototype #(type, prototype)
+    {
+    my ($self, $type, $prototype) = @_;
+
+    if ($type ne ::TOPIC_FUNCTION())
+        {
+        my $object = NaturalDocs::Languages::Prototype->New($prototype);
+        return $object;
+        };
+
+
+    # Parse the parameters out of the prototype.
+
+    my @tokens = $prototype =~ /([^\{\}\ ]+|.)/g;
+
+    my $parameter;
+    my @parameterLines;
+
+    my $braceLevel = 0;
+
+    my ($beforeParameters, $afterParameters, $finishedParameters);
+
+    foreach my $token (@tokens)
+        {
+        if ($finishedParameters)
+            {  $afterParameters .= $token;  }
+
+        elsif ($token eq '{')
+            {
+            if ($braceLevel == 0)
+                {  $beforeParameters .= $token;  }
+
+            else # braceLevel > 0
+                {  $parameter .= $token;   };
+
+            $braceLevel++;
+            }
+
+        elsif ($token eq '}')
+            {
+            if ($braceLevel == 1)
+                {
+                if ($parameter && $parameter ne ' ')
+                    {  push @parameterLines, $parameter;  };
+
+                $finishedParameters = 1;
+                $afterParameters .= $token;
+
+                $braceLevel--;
+                }
+            elsif ($braceLevel > 1)
+                {
+                $parameter .= $token;
+                $braceLevel--;
+                };
+            }
+
+        elsif ($token eq ' ')
+            {
+            if ($braceLevel == 1)
+                {
+                if ($parameter)
+                    {  push @parameterLines, $parameter;  };
+
+                $parameter = undef;
+                }
+            elsif ($braceLevel > 1)
+                {
+                $parameter .= $token;
+                }
+            else
+                {
+                $beforeParameters .= $token;
+                };
+            }
+
+        else
+            {
+            if ($braceLevel > 0)
+                {  $parameter .= $token;  }
+            else
+                {  $beforeParameters .= $token;  };
+            };
+        };
+
+    foreach my $part (\$beforeParameters, \$afterParameters)
+        {
+        $$part =~ s/^ //;
+        $$part =~ s/ $//;
+        };
+
+    my $prototypeObject = NaturalDocs::Languages::Prototype->New($beforeParameters, $afterParameters);
+
+
+    # Parse the actual parameters.
+
+    foreach my $parameterLine (@parameterLines)
+        {
+        $prototypeObject->AddParameter( $self->ParseParameterLine($parameterLine) );
+        };
+
+    return $prototypeObject;
+    };
+
+
+#
+#   Function: ParseParameterLine
+#
+#   Parses a prototype parameter line and returns it as a <NaturalDocs::Languages::Prototype::Parameter> object.
+#
+sub ParseParameterLine #(line)
+    {
+    my ($self, $line) = @_;
+    return NaturalDocs::Languages::Prototype::Parameter->New(undef, undef, $line, undef, undef, undef);
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Menu.pm b/docs/tool/Modules/NaturalDocs/Menu.pm
new file mode 100644
index 00000000..e8b696a6
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Menu.pm
@@ -0,0 +1,3406 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Menu
+#
+###############################################################################
+#
+#   A package handling the menu's contents and state.
+#
+#   Usage and Dependencies:
+#
+#       - The <Event Handlers> can be called by <NaturalDocs::Project> immediately.
+#
+#       - Prior to initialization, <NaturalDocs::Project> must be initialized, and all files that have been changed must be run
+#         through <NaturalDocs::Parser->ParseForInformation()>.
+#
+#       - To initialize, call <LoadAndUpdate()>.  Afterwards, all other functions are available.  Also, <LoadAndUpdate()> will
+#         call <NaturalDocs::Settings->GenerateDirectoryNames()>.
+#
+#       - To save the changes back to disk, call <Save()>.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use Tie::RefHash;
+
+use NaturalDocs::Menu::Entry;
+
+use strict;
+use integer;
+
+package NaturalDocs::Menu;
+
+
+#
+#   Constants: Constants
+#
+#   MAXFILESINGROUP - The maximum number of file entries that can be present in a group before it becomes a candidate for
+#                                  sub-grouping.
+#   MINFILESINNEWGROUP - The minimum number of file entries that must be present in a group before it will be automatically
+#                                        created.  This is *not* the number of files that must be in a group before it's deleted.
+#
+use constant MAXFILESINGROUP => 6;
+use constant MINFILESINNEWGROUP => 3;
+
+
+###############################################################################
+# Group: Variables
+
+
+#
+#   bool: hasChanged
+#
+#   Whether the menu changed or not, regardless of why.
+#
+my $hasChanged;
+
+
+#
+#   Object: menu
+#
+#   The parsed menu file.  Is stored as a <MENU_GROUP> <NaturalDocs::Menu::Entry> object, with the top-level entries being
+#   stored as the group's content.  This is done because it makes a number of functions simpler to implement, plus it allows group
+#   flags to be set on the top-level.  However, it is exposed externally via <Content()> as an arrayref.
+#
+#   This structure will only contain objects for <MENU_FILE>, <MENU_GROUP>, <MENU_TEXT>, <MENU_LINK>, and
+#   <MENU_INDEX> entries.  Other types, such as <MENU_TITLE>, are stored in variables such as <title>.
+#
+my $menu;
+
+#
+#   hash: defaultTitlesChanged
+#
+#   An existence hash of default titles that have changed, since <OnDefaultTitleChange()> will be called before
+#   <LoadAndUpdate()>.  Collects them to be applied later.  The keys are the <FileNames>.
+#
+my %defaultTitlesChanged;
+
+#
+#   String: title
+#
+#   The title of the menu.
+#
+my $title;
+
+#
+#   String: subTitle
+#
+#   The sub-title of the menu.
+#
+my $subTitle;
+
+#
+#   String: footer
+#
+#   The footer for the documentation.
+#
+my $footer;
+
+#
+#   String: timestampText
+#
+#   The timestamp for the documentation, stored as the final output text.
+#
+my $timestampText;
+
+#
+#   String: timestampCode
+#
+#   The timestamp for the documentation, storted as the symbolic code.
+#
+my $timestampCode;
+
+#
+#   hash: indexes
+#
+#   An existence hash of all the defined index <TopicTypes> appearing in the menu.
+#
+my %indexes;
+
+#
+#   hash: previousIndexes
+#
+#   An existence hash of all the index <TopicTypes> that appeared in the menu last time.
+#
+my %previousIndexes;
+
+#
+#   hash: bannedIndexes
+#
+#   An existence hash of all the index <TopicTypes> that the user has manually deleted, and thus should not be added back to
+#   the menu automatically.
+#
+my %bannedIndexes;
+
+
+###############################################################################
+# Group: Files
+
+#
+#   File: Menu.txt
+#
+#   The file used to generate the menu.
+#
+#   Format:
+#
+#       The file is plain text.  Blank lines can appear anywhere and are ignored.  Tags and their content must be completely
+#       contained on one line with the exception of Group's braces.  All values in brackets below are encoded with entity characters.
+#
+#       > # [comment]
+#
+#       The file supports single-line comments via #.  They can appear alone on a line or after content.
+#
+#       > Format: [version]
+#       > Title: [title]
+#       > SubTitle: [subtitle]
+#       > Footer: [footer]
+#       > Timestamp: [timestamp code]
+#
+#       The file format version, menu title, subtitle, footer, and timestamp are specified as above.  Each can only be specified once,
+#       with subsequent ones being ignored.  Subtitle is ignored if Title is not present.  Format must be the first entry in the file.  If
+#       it's not present, it's assumed the menu is from version 0.95 or earlier, since it was added with 1.0.
+#
+#       The timestamp code is as follows.
+#
+#           m - Single digit month, where applicable.  January is "1".
+#           mm - Always double digit month.  January is "01".
+#           mon - Short month word.  January is "Jan".
+#           month - Long month word.  January is "January".
+#           d - Single digit day, where applicable.  1 is "1".
+#           dd - Always double digit day.  1 is "01".
+#           day - Day with text extension.  1 is "1st".
+#           yy - Double digit year.  2006 is "06".
+#           yyyy - Four digit year.  2006 is "2006".
+#           year - Four digit year.  2006 is "2006".
+#
+#       Anything else is left literal in the output.
+#
+#       > File: [title] ([file name])
+#       > File: [title] (auto-title, [file name])
+#       > File: [title] (no auto-title, [file name])
+#
+#       Files are specified as above.  If there is only one input directory, file names are relative.  Otherwise they are absolute.
+#       If "no auto-title" is specified, the title on the line is used.  If not, the title is ignored and the
+#       default file title is used instead.  Auto-title defaults to on, so specifying "auto-title" is for compatibility only.
+#
+#       > Group: [title]
+#       > Group: [title] { ... }
+#
+#       Groups are specified as above.  If no braces are specified, the group's content is everything that follows until the end of the
+#       file, the next group (braced or unbraced), or the closing brace of a parent group.  Group braces are the only things in this
+#       file that can span multiple lines.
+#
+#       There is no limitations on where the braces can appear.  The opening brace can appear after the group tag, on its own line,
+#       or preceding another tag on a line.  Similarly, the closing brace can appear after another tag or on its own line.  Being
+#       bitchy here would just get in the way of quick and dirty editing; the package will clean it up automatically when it writes it
+#       back to disk.
+#
+#       > Text: [text]
+#
+#       Arbitrary text is specified as above.  As with other tags, everything must be contained on the same line.
+#
+#       > Link: [URL]
+#       > Link: [title] ([URL])
+#
+#       External links can be specified as above.  If the titled form is not used, the URL is used as the title.
+#
+#       > Index: [name]
+#       > [topic type name] Index: [name]
+#
+#       Indexes are specified as above.  The topic type names can be either singular or plural.  General is assumed if not specified.
+#
+#       > Don't Index: [topic type name]
+#       > Don't Index: [topic type name], [topic type name], ...
+#
+#       The option above prevents indexes that exist but are not on the menu from being automatically added.
+#
+#       > Data: [number]([obscured data])
+#
+#       Used to store non-user editable data.
+#
+#       > Data: 1([obscured: [directory name]///[input directory]])
+#
+#       When there is more than one directory, these lines store the input directories used in the last run and their names.  This
+#       allows menu files to be shared across machines since the names will be consistent and the directories can be used to convert
+#       filenames to the local machine's paths.  We don't want this user-editable because they may think changing it changes the
+#       input directories, when it doesn't.  Also, changing it without changing all the paths screws up resolving.
+#
+#       > Data: 2([obscured: [directory name])
+#
+#       When there is only one directory and its name is not "default", this stores the name.
+#
+#
+#   Entities:
+#
+#       &amp; - Ampersand.
+#       &lparen; - Left parenthesis.
+#       &rparen; - Right parenthesis.
+#       &lbrace; - Left brace.
+#       &rbrace; - Right brace.
+#
+#
+#   Revisions:
+#
+#       1.4:
+#
+#           - Added Timestamp property.
+#           - Values are now encoded with entity characters.
+#
+#       1.3:
+#
+#           - File names are now relative again if there is only one input directory.
+#           - Data: 2(...) added.
+#           - Can't use synonyms like "copyright" for "footer" or "sub-title" for "subtitle".
+#           - "Don't Index" line now requires commas to separate them, whereas it tolerated just spaces before.
+#
+#       1.16:
+#
+#           - File names are now absolute instead of relative.  Prior to 1.16 only one input directory was allowed, so they could be
+#             relative.
+#           - Data keywords introduced to store input directories and their names.
+#
+#       1.14:
+#
+#           - Renamed this file from NaturalDocs_Menu.txt to Menu.txt.
+#
+#       1.1:
+#
+#           - Added the "don't index" line.
+#
+#           This is also the point where indexes were automatically added and removed, so all index entries from prior revisions
+#           were manually added and are not guaranteed to contain anything.
+#
+#       1.0:
+#
+#           - Added the format line.
+#           - Added the "no auto-title" attribute.
+#           - Changed the file entry default to auto-title.
+#
+#           This is also the point where auto-organization and better auto-titles were introduced.  All groups in prior revisions were
+#           manually added, with the exception of a top-level Other group where new files were automatically added if there were
+#           groups defined.
+#
+#       Break in support:
+#
+#           Releases prior to 1.0 are no longer supported.  Why?
+#
+#           - They don't have a Format: line, which is required by <NaturalDocs::ConfigFile>, although I could work around this
+#             if I needed to.
+#           - No significant number of downloads for pre-1.0 releases.
+#           - Code simplification.  I don't have to bridge the conversion from manual-only menu organization to automatic.
+#
+#       0.9:
+#
+#           - Added index entries.
+#
+
+#
+#   File: PreviousMenuState.nd
+#
+#   The file used to store the previous state of the menu so as to detect changes.
+#
+#
+#   Format:
+#
+#   > [BINARY_FORMAT]
+#   > [VersionInt: app version]
+#
+#   First is the standard <BINARY_FORMAT> <VersionInt> header.
+#
+#   > [UInt8: 0 (end group)]
+#   > [UInt8: MENU_FILE] [UInt8: noAutoTitle] [AString16: title] [AString16: target]
+#   > [UInt8: MENU_GROUP] [AString16: title]
+#   > [UInt8: MENU_INDEX] [AString16: title] [AString16: topic type]
+#   > [UInt8: MENU_LINK] [AString16: title] [AString16: url]
+#   > [UInt8: MENU_TEXT] [AString16: text]
+#
+#   The first UInt8 of each following line is either zero or one of the <Menu Entry Types>.  What follows is contextual.
+#
+#   There are no entries for title, subtitle, or footer.  Only the entries present in <menu>.
+#
+#   See Also:
+#
+#       <File Format Conventions>
+#
+#   Dependencies:
+#
+#       - Because the type is represented by a UInt8, the <Menu Entry Types> must all be <= 255.
+#
+#   Revisions:
+#
+#       1.3:
+#
+#           - The topic type following the <MENU_INDEX> entries were changed from UInt8s to AString16s, since <TopicTypes>
+#             were switched from integer constants to strings.  You can still convert the old to the new via
+#             <NaturalDocs::Topics->TypeFromLegacy()>.
+#
+#       1.16:
+#
+#           - The file targets are now absolute.  Prior to 1.16, they were relative to the input directory since only one was allowed.
+#
+#       1.14:
+#
+#           - The file was renamed from NaturalDocs.m to PreviousMenuState.nd and moved into the Data subdirectory.
+#
+#       1.0:
+#
+#           - The file's format was completely redone.  Prior to 1.0, the file was a text file consisting of the app version and a line
+#             which was a tab-separated list of the indexes present in the menu.  * meant the general index.
+#
+#       Break in support:
+#
+#           Pre-1.0 files are no longer supported.  There was no significant number of downloads for pre-1.0 releases, and this
+#           eliminates a separate code path for them.
+#
+#       0.95:
+#
+#           - Change the file version to match the app version.  Prior to 0.95, the version line was 1.  Test for "1" instead of "1.0" to
+#             distinguish.
+#
+#       0.9:
+#
+#           - The file was added to the project.  Prior to 0.9, it didn't exist.
+#
+
+
+###############################################################################
+# Group: File Functions
+
+#
+#   Function: LoadAndUpdate
+#
+#   Loads the menu file from disk and updates it.  Will add, remove, rearrange, and remove auto-titling from entries as
+#   necessary.  Will also call <NaturalDocs::Settings->GenerateDirectoryNames()>.
+#
+sub LoadAndUpdate
+    {
+    my ($self) = @_;
+
+    my ($inputDirectoryNames, $relativeFiles, $onlyDirectoryName) = $self->LoadMenuFile();
+
+    my $errorCount = NaturalDocs::ConfigFile->ErrorCount();
+    if ($errorCount)
+        {
+        NaturalDocs::ConfigFile->PrintErrorsAndAnnotateFile();
+        NaturalDocs::Error->SoftDeath('There ' . ($errorCount == 1 ? 'is an error' : 'are ' . $errorCount . ' errors')
+                                                    . ' in ' . NaturalDocs::Project->UserConfigFile('Menu.txt'));
+        };
+
+    # If the menu has a timestamp and today is a different day than the last time Natural Docs was run, we have to count it as the
+    # menu changing.
+    if (defined $timestampCode)
+        {
+        my (undef, undef, undef, $currentDay, $currentMonth, $currentYear) = localtime();
+        my (undef, undef, undef, $lastDay, $lastMonth, $lastYear) =
+            localtime( (stat( NaturalDocs::Project->DataFile('PreviousMenuState.nd') ))[9] );
+            # This should be okay if the previous menu state file doesn't exist.
+
+        if ($currentDay != $lastDay || $currentMonth != $lastMonth || $currentYear != $lastYear)
+            {  $hasChanged = 1;  };
+        };
+
+
+    if ($relativeFiles)
+        {
+        my $inputDirectory = $self->ResolveRelativeInputDirectories($onlyDirectoryName);
+
+        if ($onlyDirectoryName)
+            {  $inputDirectoryNames = { $inputDirectory => $onlyDirectoryName };  };
+        }
+    else
+        {  $self->ResolveInputDirectories($inputDirectoryNames);  };
+
+    NaturalDocs::Settings->GenerateDirectoryNames($inputDirectoryNames);
+
+    my $filesInMenu = $self->FilesInMenu();
+
+    my ($previousMenu, $previousIndexes, $previousFiles) = $self->LoadPreviousMenuStateFile();
+
+    if (defined $previousIndexes)
+        {  %previousIndexes = %$previousIndexes;  };
+
+    if (defined $previousFiles)
+        {  $self->LockUserTitleChanges($previousFiles);  };
+
+    # Don't need these anymore.  We keep this level of detail because it may be used more in the future.
+    $previousMenu = undef;
+    $previousFiles = undef;
+    $previousIndexes = undef;
+
+    # We flag title changes instead of actually performing them at this point for two reasons.  First, contents of groups are still
+    # subject to change, which would affect the generated titles.  Second, we haven't detected the sort order yet.  Changing titles
+    # could make groups appear unalphabetized when they were beforehand.
+
+    my $updateAllTitles;
+
+    # If the menu file changed, we can't be sure which groups changed and which didn't without a comparison, which really isn't
+    # worth the trouble.  So we regenerate all the titles instead.
+    if (NaturalDocs::Project->UserConfigFileStatus('Menu.txt') == ::FILE_CHANGED())
+        {  $updateAllTitles = 1;  }
+    else
+        {  $self->FlagAutoTitleChanges();  };
+
+    # We add new files before deleting old files so their presence still affects the grouping.  If we deleted old files first, it could
+    # throw off where to place the new ones.
+
+    $self->AutoPlaceNewFiles($filesInMenu);
+
+    my $numberRemoved = $self->RemoveDeadFiles();
+
+    $self->CheckForTrashedMenu(scalar keys %$filesInMenu, $numberRemoved);
+
+    # Don't ban indexes if they deleted Menu.txt.  They may have not deleted PreviousMenuState.nd and we don't want everything
+    # to be banned because of it.
+    if (NaturalDocs::Project->UserConfigFileStatus('Menu.txt') != ::FILE_DOESNTEXIST())
+        {  $self->BanAndUnbanIndexes();  };
+
+    # Index groups need to be detected before adding new ones.
+
+    $self->DetectIndexGroups();
+
+    $self->AddAndRemoveIndexes();
+
+   # We wait until after new files are placed to remove dead groups because a new file may save a group.
+
+    $self->RemoveDeadGroups();
+
+    $self->CreateDirectorySubGroups();
+
+    # We detect the sort before regenerating the titles so it doesn't get thrown off by changes.  However, we do it after deleting
+    # dead entries and moving things into subgroups because their removal may bump it into a stronger sort category (i.e.
+    # SORTFILESANDGROUPS instead of just SORTFILES.)  New additions don't factor into the sort.
+
+    $self->DetectOrder($updateAllTitles);
+
+    $self->GenerateAutoFileTitles($updateAllTitles);
+
+    $self->ResortGroups($updateAllTitles);
+
+
+    # Don't need this anymore.
+    %defaultTitlesChanged = ( );
+    };
+
+
+#
+#   Function: Save
+#
+#   Writes the changes to the menu files.
+#
+sub Save
+    {
+    my ($self) = @_;
+
+    if ($hasChanged)
+        {
+        $self->SaveMenuFile();
+        $self->SavePreviousMenuStateFile();
+        };
+    };
+
+
+###############################################################################
+# Group: Information Functions
+
+#
+#   Function: HasChanged
+#
+#   Returns whether the menu has changed or not.
+#
+sub HasChanged
+    {  return $hasChanged;  };
+
+#
+#   Function: Content
+#
+#   Returns the parsed menu as an arrayref of <NaturalDocs::Menu::Entry> objects.  Do not change the arrayref.
+#
+#   The arrayref will only contain <MENU_FILE>, <MENU_GROUP>, <MENU_INDEX>, <MENU_TEXT>, and <MENU_LINK>
+#   entries.  Entries such as <MENU_TITLE> are parsed out and are only accessible via functions such as <Title()>.
+#
+sub Content
+    {  return $menu->GroupContent();  };
+
+#
+#   Function: Title
+#
+#   Returns the title of the menu, or undef if none.
+#
+sub Title
+    {  return $title;  };
+
+#
+#   Function: SubTitle
+#
+#   Returns the sub-title of the menu, or undef if none.
+#
+sub SubTitle
+    {  return $subTitle;  };
+
+#
+#   Function: Footer
+#
+#   Returns the footer of the documentation, or undef if none.
+#
+sub Footer
+    {  return $footer;  };
+
+#
+#   Function: TimeStamp
+#
+#   Returns the timestamp text of the documentation, or undef if none.
+#
+sub TimeStamp
+    {  return $timestampText;  };
+
+#
+#   Function: Indexes
+#
+#   Returns an existence hashref of all the index <TopicTypes> appearing in the menu.  Do not change the hashref.
+#
+sub Indexes
+    {  return \%indexes;  };
+
+#
+#   Function: PreviousIndexes
+#
+#   Returns an existence hashref of all the index <TopicTypes> that previously appeared in the menu.  Do not change the
+#   hashref.
+#
+sub PreviousIndexes
+    {  return \%previousIndexes;  };
+
+
+#
+#   Function: FilesInMenu
+#
+#   Returns a hashref of all the files present in the menu.  The keys are the <FileNames>, and the values are references to their
+#   <NaturalDocs::Menu::Entry> objects.
+#
+sub FilesInMenu
+    {
+    my ($self) = @_;
+
+    my @groupStack = ( $menu );
+    my $filesInMenu = { };
+
+    while (scalar @groupStack)
+        {
+        my $currentGroup = pop @groupStack;
+        my $currentGroupContent = $currentGroup->GroupContent();
+
+        foreach my $entry (@$currentGroupContent)
+            {
+            if ($entry->Type() == ::MENU_GROUP())
+                {  push @groupStack, $entry;  }
+            elsif ($entry->Type() == ::MENU_FILE())
+                {  $filesInMenu->{ $entry->Target() } = $entry;  };
+            };
+        };
+
+    return $filesInMenu;
+    };
+
+
+
+###############################################################################
+# Group: Event Handlers
+#
+#   These functions are called by <NaturalDocs::Project> only.  You don't need to worry about calling them.  For example, when
+#   changing the default menu title of a file, you only need to call <NaturalDocs::Project->SetDefaultMenuTitle()>.  That function
+#   will handle calling <OnDefaultTitleChange()>.
+
+
+#
+#   Function: OnDefaultTitleChange
+#
+#   Called by <NaturalDocs::Project> if the default menu title of a source file has changed.
+#
+#   Parameters:
+#
+#       file    - The source <FileName> that had its default menu title changed.
+#
+sub OnDefaultTitleChange #(file)
+    {
+    my ($self, $file) = @_;
+
+    # Collect them for later.  We'll deal with them in LoadAndUpdate().
+
+    $defaultTitlesChanged{$file} = 1;
+    };
+
+
+
+###############################################################################
+# Group: Support Functions
+
+
+#
+#   Function: LoadMenuFile
+#
+#   Loads and parses the menu file <Menu.txt>.  This will fill <menu>, <title>, <subTitle>, <footer>, <timestampText>,
+#   <timestampCode>, <indexes>, and <bannedIndexes>.  If there are any errors in the file, they will be recorded with
+#   <NaturalDocs::ConfigFile->AddError()>.
+#
+#   Returns:
+#
+#       The array ( inputDirectories, relativeFiles, onlyDirectoryName ) or an empty array if the file doesn't exist.
+#
+#       inputDirectories - A hashref of all the input directories and their names stored in the menu file.  The keys are the
+#                                 directories and the values are their names.  Undef if none.
+#       relativeFiles - Whether the menu uses relative file names.
+#       onlyDirectoryName - The name of the input directory if there is only one.
+#
+sub LoadMenuFile
+    {
+    my ($self) = @_;
+
+    my $inputDirectories = { };
+    my $relativeFiles;
+    my $onlyDirectoryName;
+
+    # A stack of Menu::Entry object references as we move through the groups.
+    my @groupStack;
+
+    $menu = NaturalDocs::Menu::Entry->New(::MENU_GROUP(), undef, undef, undef);
+    my $currentGroup = $menu;
+
+    # Whether we're currently in a braceless group, since we'd have to find the implied end rather than an explicit one.
+    my $inBracelessGroup;
+
+    # Whether we're right after a group token, which is the only place there can be an opening brace.
+    my $afterGroupToken;
+
+    my $version;
+
+    if ($version = NaturalDocs::ConfigFile->Open(NaturalDocs::Project->UserConfigFile('Menu.txt'), 1))
+        {
+        # We don't check if the menu file is from a future version because we can't just throw it out and regenerate it like we can
+        # with other data files.  So we just keep going regardless.  Any syntactic differences will show up as errors.
+
+        while (my ($keyword, $value, $comment) = NaturalDocs::ConfigFile->GetLine())
+            {
+            # Check for an opening brace after a group token.  This has to be separate from the rest of the code because the flag
+            # needs to be reset after every line.
+            if ($afterGroupToken)
+                {
+                $afterGroupToken = undef;
+
+                if ($keyword eq '{')
+                    {
+                    $inBracelessGroup = undef;
+                    next;
+                    }
+                else
+                    {  $inBracelessGroup = 1;  };
+                };
+
+
+            # Now on to the real code.
+
+            if ($keyword eq 'file')
+                {
+                my $flags = 0;
+
+                if ($value =~ /^(.+)\(([^\(]+)\)$/)
+                    {
+                    my ($title, $file) = ($1, $2);
+
+                    $title =~ s/ +$//;
+
+                    # Check for auto-title modifier.
+                    if ($file =~ /^((?:no )?auto-title, ?)(.+)$/i)
+                        {
+                        my $modifier;
+                        ($modifier, $file) = ($1, $2);
+
+                        if ($modifier =~ /^no/i)
+                            {  $flags |= ::MENU_FILE_NOAUTOTITLE();  };
+                        };
+
+                    my $entry = NaturalDocs::Menu::Entry->New(::MENU_FILE(), $self->RestoreAmpChars($title),
+                                                                                       $self->RestoreAmpChars($file), $flags);
+
+                    $currentGroup->PushToGroup($entry);
+                    }
+                else
+                    {  NaturalDocs::ConfigFile->AddError('File lines must be in the format "File: [title] ([location])"');  };
+                }
+
+
+            elsif ($keyword eq 'group')
+                {
+                # End a braceless group, if we were in one.
+                if ($inBracelessGroup)
+                    {
+                    $currentGroup = pop @groupStack;
+                    $inBracelessGroup = undef;
+                    };
+
+                my $entry = NaturalDocs::Menu::Entry->New(::MENU_GROUP(), $self->RestoreAmpChars($value), undef, undef);
+
+                $currentGroup->PushToGroup($entry);
+
+                push @groupStack, $currentGroup;
+                $currentGroup = $entry;
+
+                $afterGroupToken = 1;
+                }
+
+
+            elsif ($keyword eq '{')
+                {
+                NaturalDocs::ConfigFile->AddError('Opening braces are only allowed after Group tags.');
+                }
+
+
+            elsif ($keyword eq '}')
+                {
+                # End a braceless group, if we were in one.
+                if ($inBracelessGroup)
+                    {
+                    $currentGroup = pop @groupStack;
+                    $inBracelessGroup = undef;
+                    };
+
+                # End a braced group too.
+                if (scalar @groupStack)
+                    {  $currentGroup = pop @groupStack;  }
+                else
+                    {  NaturalDocs::ConfigFile->AddError('Unmatched closing brace.');  };
+                }
+
+
+            elsif ($keyword eq 'title')
+                {
+                if (!defined $title)
+                    {  $title = $self->RestoreAmpChars($value);  }
+                else
+                    {  NaturalDocs::ConfigFile->AddError('Title can only be defined once.');  };
+                }
+
+
+            elsif ($keyword eq 'subtitle')
+                {
+                if (defined $title)
+                    {
+                    if (!defined $subTitle)
+                        {  $subTitle = $self->RestoreAmpChars($value);  }
+                    else
+                        {  NaturalDocs::ConfigFile->AddError('SubTitle can only be defined once.');  };
+                    }
+                else
+                    {  NaturalDocs::ConfigFile->AddError('Title must be defined before SubTitle.');  };
+                }
+
+
+            elsif ($keyword eq 'footer')
+                {
+                if (!defined $footer)
+                    {  $footer = $self->RestoreAmpChars($value);  }
+                else
+                    {  NaturalDocs::ConfigFile->AddError('Footer can only be defined once.');  };
+                }
+
+
+            elsif ($keyword eq 'timestamp')
+                {
+                if (!defined $timestampCode)
+                    {
+                    $timestampCode = $self->RestoreAmpChars($value);
+                    $self->GenerateTimestampText();
+                    }
+                else
+                    {  NaturalDocs::ConfigFile->AddError('Timestamp can only be defined once.');  };
+                }
+
+
+            elsif ($keyword eq 'text')
+                {
+                $currentGroup->PushToGroup( NaturalDocs::Menu::Entry->New(::MENU_TEXT(), $self->RestoreAmpChars($value),
+                                                                                                              undef, undef) );
+                }
+
+
+            elsif ($keyword eq 'link')
+                {
+                my ($title, $url);
+
+                if ($value =~ /^([^\(\)]+?) ?\(([^\)]+)\)$/)
+                    {
+                    ($title, $url) = ($1, $2);
+                    }
+                elsif (defined $comment)
+                    {
+                    $value .= $comment;
+
+                    if ($value =~ /^([^\(\)]+?) ?\(([^\)]+)\) ?(?:#.*)?$/)
+                        {
+                        ($title, $url) = ($1, $2);
+                        };
+                    };
+
+                if ($title)
+                    {
+                    $currentGroup->PushToGroup( NaturalDocs::Menu::Entry->New(::MENU_LINK(), $self->RestoreAmpChars($title),
+                                                                 $self->RestoreAmpChars($url), undef) );
+                    }
+                else
+                    {  NaturalDocs::ConfigFile->AddError('Link lines must be in the format "Link: [title] ([url])"');  };
+                }
+
+
+            elsif ($keyword eq 'data')
+                {
+                $value =~ /^(\d)\((.*)\)$/;
+                my ($number, $data) = ($1, $2);
+
+                $data = NaturalDocs::ConfigFile->Unobscure($data);
+
+                # The input directory naming convention changed with version 1.32, but NaturalDocs::Settings will handle that
+                # automatically.
+
+                if ($number == 1)
+                    {
+                    my ($dirName, $inputDir) = split(/\/\/\//, $data, 2);
+                    $inputDirectories->{$inputDir} = $dirName;
+                    }
+                elsif ($number == 2)
+                    {  $onlyDirectoryName = $data;  };
+                # Ignore other numbers because it may be from a future format and we don't want to make the user delete it
+                # manually.
+                }
+
+            elsif ($keyword eq "don't index")
+                {
+                my @indexes = split(/, ?/, $value);
+
+                foreach my $index (@indexes)
+                    {
+                    my $indexType = NaturalDocs::Topics->TypeFromName( $self->RestoreAmpChars($index) );
+
+                    if (defined $indexType)
+                        {  $bannedIndexes{$indexType} = 1;  };
+                    };
+                }
+
+            elsif ($keyword eq 'index')
+                {
+                my $entry = NaturalDocs::Menu::Entry->New(::MENU_INDEX(), $self->RestoreAmpChars($value),
+                                                                                   ::TOPIC_GENERAL(), undef);
+                $currentGroup->PushToGroup($entry);
+
+                $indexes{::TOPIC_GENERAL()} = 1;
+                }
+
+            elsif (substr($keyword, -6) eq ' index')
+                {
+                my $index = substr($keyword, 0, -6);
+                my ($indexType, $indexInfo) = NaturalDocs::Topics->NameInfo( $self->RestoreAmpChars($index) );
+
+                if (defined $indexType)
+                    {
+                    if ($indexInfo->Index())
+                        {
+                        $indexes{$indexType} = 1;
+                        $currentGroup->PushToGroup(
+                            NaturalDocs::Menu::Entry->New(::MENU_INDEX(), $self->RestoreAmpChars($value), $indexType, undef) );
+                        }
+                    else
+                        {
+                        # If it's on the menu but isn't indexable, the topic setting may have changed out from under it.
+                        $hasChanged = 1;
+                        };
+                    }
+                else
+                    {
+                    NaturalDocs::ConfigFile->AddError($index . ' is not a valid index type.');
+                    };
+                }
+
+            else
+                {
+                NaturalDocs::ConfigFile->AddError(ucfirst($keyword) . ' is not a valid keyword.');
+                };
+            };
+
+
+        # End a braceless group, if we were in one.
+        if ($inBracelessGroup)
+            {
+            $currentGroup = pop @groupStack;
+            $inBracelessGroup = undef;
+            };
+
+        # Close up all open groups.
+        my $openGroups = 0;
+        while (scalar @groupStack)
+            {
+            $currentGroup = pop @groupStack;
+            $openGroups++;
+            };
+
+        if ($openGroups == 1)
+            {  NaturalDocs::ConfigFile->AddError('There is an unclosed group.');  }
+        elsif ($openGroups > 1)
+            {  NaturalDocs::ConfigFile->AddError('There are ' . $openGroups . ' unclosed groups.');  };
+
+
+        if (!scalar keys %$inputDirectories)
+            {
+            $inputDirectories = undef;
+            $relativeFiles = 1;
+            };
+
+        NaturalDocs::ConfigFile->Close();
+
+        return ($inputDirectories, $relativeFiles, $onlyDirectoryName);
+        }
+
+    else
+        {  return ( );  };
+    };
+
+
+#
+#   Function: SaveMenuFile
+#
+#   Saves the current menu to <Menu.txt>.
+#
+sub SaveMenuFile
+    {
+    my ($self) = @_;
+
+    open(MENUFILEHANDLE, '>' . NaturalDocs::Project->UserConfigFile('Menu.txt'))
+        or die "Couldn't save menu file " . NaturalDocs::Project->UserConfigFile('Menu.txt') . "\n";
+
+
+    print MENUFILEHANDLE
+    "Format: " . NaturalDocs::Settings->TextAppVersion() . "\n\n\n";
+
+    my $inputDirs = NaturalDocs::Settings->InputDirectories();
+
+
+    if (defined $title)
+        {
+        print MENUFILEHANDLE 'Title: ' . $self->ConvertAmpChars($title) . "\n";
+
+        if (defined $subTitle)
+            {
+            print MENUFILEHANDLE 'SubTitle: ' . $self->ConvertAmpChars($subTitle) . "\n";
+            }
+        else
+            {
+            print MENUFILEHANDLE
+            "\n"
+            . "# You can also add a sub-title to your menu like this:\n"
+            . "# SubTitle: [subtitle]\n";
+            };
+        }
+    else
+        {
+        print MENUFILEHANDLE
+        "# You can add a title and sub-title to your menu like this:\n"
+        . "# Title: [project name]\n"
+        . "# SubTitle: [subtitle]\n";
+        };
+
+    print MENUFILEHANDLE "\n";
+
+    if (defined $footer)
+        {
+        print MENUFILEHANDLE 'Footer: ' . $self->ConvertAmpChars($footer) . "\n";
+        }
+    else
+        {
+        print MENUFILEHANDLE
+        "# You can add a footer to your documentation like this:\n"
+        . "# Footer: [text]\n"
+        . "# If you want to add a copyright notice, this would be the place to do it.\n";
+        };
+
+    if (defined $timestampCode)
+        {
+        print MENUFILEHANDLE 'Timestamp: ' . $self->ConvertAmpChars($timestampCode) . "\n";
+        }
+    else
+        {
+        print MENUFILEHANDLE
+        "\n"
+        . "# You can add a timestamp to your documentation like one of these:\n"
+        . "# Timestamp: Generated on month day, year\n"
+        . "# Timestamp: Updated mm/dd/yyyy\n"
+        . "# Timestamp: Last updated mon day\n"
+        . "#\n";
+        };
+
+    print MENUFILEHANDLE
+        qq{#   m     - One or two digit month.  January is "1"\n}
+        . qq{#   mm    - Always two digit month.  January is "01"\n}
+        . qq{#   mon   - Short month word.  January is "Jan"\n}
+        . qq{#   month - Long month word.  January is "January"\n}
+        . qq{#   d     - One or two digit day.  1 is "1"\n}
+        . qq{#   dd    - Always two digit day.  1 is "01"\n}
+        . qq{#   day   - Day with letter extension.  1 is "1st"\n}
+        . qq{#   yy    - Two digit year.  2006 is "06"\n}
+        . qq{#   yyyy  - Four digit year.  2006 is "2006"\n}
+        . qq{#   year  - Four digit year.  2006 is "2006"\n}
+
+        . "\n";
+
+    if (scalar keys %bannedIndexes)
+        {
+        print MENUFILEHANDLE
+
+        "# These are indexes you deleted, so Natural Docs will not add them again\n"
+        . "# unless you remove them from this line.\n"
+        . "\n"
+        . "Don't Index: ";
+
+        my $first = 1;
+
+        foreach my $index (keys %bannedIndexes)
+            {
+            if (!$first)
+                {  print MENUFILEHANDLE ', ';  }
+            else
+                {  $first = undef;  };
+
+            print MENUFILEHANDLE $self->ConvertAmpChars( NaturalDocs::Topics->NameOfType($index, 1), CONVERT_COMMAS() );
+            };
+
+        print MENUFILEHANDLE "\n\n";
+        };
+
+
+    # Remember to keep lines below eighty characters.
+
+    print MENUFILEHANDLE
+    "\n"
+    . "# --------------------------------------------------------------------------\n"
+    . "# \n"
+    . "# Cut and paste the lines below to change the order in which your files\n"
+    . "# appear on the menu.  Don't worry about adding or removing files, Natural\n"
+    . "# Docs will take care of that.\n"
+    . "# \n"
+    . "# You can further organize the menu by grouping the entries.  Add a\n"
+    . "# \"Group: [name] {\" line to start a group, and add a \"}\" to end it.\n"
+    . "# \n"
+    . "# You can add text and web links to the menu by adding \"Text: [text]\" and\n"
+    . "# \"Link: [name] ([URL])\" lines, respectively.\n"
+    . "# \n"
+    . "# The formatting and comments are auto-generated, so don't worry about\n"
+    . "# neatness when editing the file.  Natural Docs will clean it up the next\n"
+    . "# time it is run.  When working with groups, just deal with the braces and\n"
+    . "# forget about the indentation and comments.\n"
+    . "# \n";
+
+    if (scalar @$inputDirs > 1)
+        {
+        print MENUFILEHANDLE
+        "# You can use this file on other computers even if they use different\n"
+        . "# directories.  As long as the command line points to the same source files,\n"
+        . "# Natural Docs will be able to correct the locations automatically.\n"
+        . "# \n";
+        };
+
+    print MENUFILEHANDLE
+    "# --------------------------------------------------------------------------\n"
+
+    . "\n\n";
+
+
+    $self->WriteMenuEntries($menu->GroupContent(), \*MENUFILEHANDLE, undef, (scalar @$inputDirs == 1));
+
+
+    if (scalar @$inputDirs > 1)
+        {
+        print MENUFILEHANDLE
+        "\n\n##### Do not change or remove these lines. #####\n";
+
+        foreach my $inputDir (@$inputDirs)
+            {
+            print MENUFILEHANDLE
+            'Data: 1(' . NaturalDocs::ConfigFile->Obscure( NaturalDocs::Settings->InputDirectoryNameOf($inputDir)
+                                                                              . '///' . $inputDir ) . ")\n";
+            };
+        }
+    elsif (lc(NaturalDocs::Settings->InputDirectoryNameOf($inputDirs->[0])) != 1)
+        {
+        print MENUFILEHANDLE
+        "\n\n##### Do not change or remove this line. #####\n"
+        . 'Data: 2(' . NaturalDocs::ConfigFile->Obscure( NaturalDocs::Settings->InputDirectoryNameOf($inputDirs->[0]) ) . ")\n";
+        }
+
+    close(MENUFILEHANDLE);
+    };
+
+
+#
+#   Function: WriteMenuEntries
+#
+#   A recursive function to write the contents of an arrayref of <NaturalDocs::Menu::Entry> objects to disk.
+#
+#   Parameters:
+#
+#       entries          - The arrayref of menu entries to write.
+#       fileHandle      - The handle to the output file.
+#       indentChars   - The indentation _characters_ to add before each line.  It is not the number of characters, it is the characters
+#                              themselves.  Use undef for none.
+#       relativeFiles - Whether to use relative file names.
+#
+sub WriteMenuEntries #(entries, fileHandle, indentChars, relativeFiles)
+    {
+    my ($self, $entries, $fileHandle, $indentChars, $relativeFiles) = @_;
+    my $lastEntryType;
+
+    foreach my $entry (@$entries)
+        {
+        if ($entry->Type() == ::MENU_FILE())
+            {
+            my $fileName;
+
+            if ($relativeFiles)
+                {  $fileName = (NaturalDocs::Settings->SplitFromInputDirectory($entry->Target()))[1];  }
+            else
+                {  $fileName = $entry->Target();  };
+
+            print $fileHandle $indentChars . 'File: ' . $self->ConvertAmpChars( $entry->Title(), CONVERT_PARENTHESIS() )
+                                  . '  (' . ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE() ? 'no auto-title, ' : '')
+                                  . $self->ConvertAmpChars($fileName) . ")\n";
+            }
+        elsif ($entry->Type() == ::MENU_GROUP())
+            {
+            if (defined $lastEntryType && $lastEntryType != ::MENU_GROUP())
+                {  print $fileHandle "\n";  };
+
+            print $fileHandle $indentChars . 'Group: ' . $self->ConvertAmpChars( $entry->Title() ) . "  {\n\n";
+            $self->WriteMenuEntries($entry->GroupContent(), $fileHandle, '   ' . $indentChars, $relativeFiles);
+            print $fileHandle '   ' . $indentChars . '}  # Group: ' . $self->ConvertAmpChars( $entry->Title() ) . "\n\n";
+            }
+        elsif ($entry->Type() == ::MENU_TEXT())
+            {
+            print $fileHandle $indentChars . 'Text: ' . $self->ConvertAmpChars( $entry->Title() ) . "\n";
+            }
+        elsif ($entry->Type() == ::MENU_LINK())
+            {
+            print $fileHandle $indentChars . 'Link: ' . $self->ConvertAmpChars( $entry->Title() ) . '  '
+                                                        . '(' . $self->ConvertAmpChars( $entry->Target(), CONVERT_PARENTHESIS() ) . ')' . "\n";
+            }
+        elsif ($entry->Type() == ::MENU_INDEX())
+            {
+            my $type;
+            if ($entry->Target() ne ::TOPIC_GENERAL())
+                {
+                $type = NaturalDocs::Topics->NameOfType($entry->Target()) . ' ';
+                };
+
+            print $fileHandle $indentChars . $self->ConvertAmpChars($type, CONVERT_COLONS()) . 'Index: '
+                                                        . $self->ConvertAmpChars( $entry->Title() ) . "\n";
+            };
+
+        $lastEntryType = $entry->Type();
+        };
+    };
+
+
+#
+#   Function: LoadPreviousMenuStateFile
+#
+#   Loads and parses the previous menu state file.
+#
+#   Returns:
+#
+#       The array ( previousMenu, previousIndexes, previousFiles ) or an empty array if there was a problem with the file.
+#
+#       previousMenu - A <MENU_GROUP> <NaturalDocs::Menu::Entry> object, similar to <menu>, which contains the entire
+#                              previous menu.
+#       previousIndexes - An existence hashref of the index <TopicTypes> present in the previous menu.
+#       previousFiles - A hashref of the files present in the previous menu.  The keys are the <FileNames>, and the entries are
+#                             references to its object in previousMenu.
+#
+sub LoadPreviousMenuStateFile
+    {
+    my ($self) = @_;
+
+    my $fileIsOkay;
+    my $version;
+    my $previousStateFileName = NaturalDocs::Project->DataFile('PreviousMenuState.nd');
+
+    if (open(PREVIOUSSTATEFILEHANDLE, '<' . $previousStateFileName))
+        {
+        # See if it's binary.
+        binmode(PREVIOUSSTATEFILEHANDLE);
+
+        my $firstChar;
+        read(PREVIOUSSTATEFILEHANDLE, $firstChar, 1);
+
+        if ($firstChar == ::BINARY_FORMAT())
+            {
+            $version = NaturalDocs::Version->FromBinaryFile(\*PREVIOUSSTATEFILEHANDLE);
+
+            # Only the topic type format has changed since switching to binary, and we support both methods.
+
+            if (NaturalDocs::Version->CheckFileFormat($version))
+                {  $fileIsOkay = 1;  }
+            else
+                {  close(PREVIOUSSTATEFILEHANDLE);  };
+            }
+
+        else # it's not in binary
+            {  close(PREVIOUSSTATEFILEHANDLE);  };
+        };
+
+    if ($fileIsOkay)
+        {
+        if (NaturalDocs::Project->UserConfigFileStatus('Menu.txt') == ::FILE_CHANGED())
+            {  $hasChanged = 1;  };
+
+
+        my $menu = NaturalDocs::Menu::Entry->New(::MENU_GROUP(), undef, undef, undef);
+        my $indexes = { };
+        my $files = { };
+
+        my @groupStack;
+        my $currentGroup = $menu;
+        my $raw;
+
+        # [UInt8: type or 0 for end group]
+
+        while (read(PREVIOUSSTATEFILEHANDLE, $raw, 1))
+            {
+            my ($type, $flags, $title, $titleLength, $target, $targetLength);
+            $type = unpack('C', $raw);
+
+            if ($type == 0)
+                {  $currentGroup = pop @groupStack;  }
+
+            elsif ($type == ::MENU_FILE())
+                {
+                # [UInt8: noAutoTitle] [AString16: title] [AString16: target]
+
+                read(PREVIOUSSTATEFILEHANDLE, $raw, 3);
+                (my $noAutoTitle, $titleLength) = unpack('Cn', $raw);
+
+                if ($noAutoTitle)
+                    {  $flags = ::MENU_FILE_NOAUTOTITLE();  };
+
+                read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength);
+                read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
+
+                $targetLength = unpack('n', $raw);
+
+                read(PREVIOUSSTATEFILEHANDLE, $target, $targetLength);
+                }
+
+            elsif ($type == ::MENU_GROUP())
+                {
+                # [AString16: title]
+
+                read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
+                $titleLength = unpack('n', $raw);
+
+                read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength);
+                }
+
+            elsif ($type == ::MENU_INDEX())
+                {
+                # [AString16: title]
+
+                read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
+                $titleLength = unpack('n', $raw);
+
+                read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength);
+
+                if ($version >= NaturalDocs::Version->FromString('1.3'))
+                    {
+                    # [AString16: topic type]
+                    read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
+                    $targetLength = unpack('n', $raw);
+
+                    read(PREVIOUSSTATEFILEHANDLE, $target, $targetLength);
+                    }
+                else
+                    {
+                    # [UInt8: topic type (0 for general)]
+                    read(PREVIOUSSTATEFILEHANDLE, $raw, 1);
+                    $target = unpack('C', $raw);
+
+                    $target = NaturalDocs::Topics->TypeFromLegacy($target);
+                    };
+                }
+
+            elsif ($type == ::MENU_LINK())
+                {
+                # [AString16: title] [AString16: url]
+
+                read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
+                $titleLength = unpack('n', $raw);
+
+                read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength);
+                read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
+                $targetLength = unpack('n', $raw);
+
+                read(PREVIOUSSTATEFILEHANDLE, $target, $targetLength);
+                }
+
+            elsif ($type == ::MENU_TEXT())
+                {
+                # [AString16: text]
+
+                read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
+                $titleLength = unpack('n', $raw);
+
+                read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength);
+                };
+
+
+            # The topic type of the index may have been removed.
+
+            if ( !($type == ::MENU_INDEX() && !NaturalDocs::Topics->IsValidType($target)) )
+                {
+                my $entry = NaturalDocs::Menu::Entry->New($type, $title, $target, ($flags || 0));
+                $currentGroup->PushToGroup($entry);
+
+                if ($type == ::MENU_FILE())
+                    {
+                    $files->{$target} = $entry;
+                    }
+                elsif ($type == ::MENU_GROUP())
+                    {
+                    push @groupStack, $currentGroup;
+                    $currentGroup = $entry;
+                    }
+                elsif ($type == ::MENU_INDEX())
+                    {
+                    $indexes->{$target} = 1;
+                    };
+                };
+
+            };
+
+        close(PREVIOUSSTATEFILEHANDLE);
+
+        return ($menu, $indexes, $files);
+        }
+    else
+        {
+        $hasChanged = 1;
+        return ( );
+        };
+    };
+
+
+#
+#   Function: SavePreviousMenuStateFile
+#
+#   Saves changes to <PreviousMenuState.nd>.
+#
+sub SavePreviousMenuStateFile
+    {
+    my ($self) = @_;
+
+    open (PREVIOUSSTATEFILEHANDLE, '>' . NaturalDocs::Project->DataFile('PreviousMenuState.nd'))
+        or die "Couldn't save " . NaturalDocs::Project->DataFile('PreviousMenuState.nd') . ".\n";
+
+    binmode(PREVIOUSSTATEFILEHANDLE);
+
+    print PREVIOUSSTATEFILEHANDLE '' . ::BINARY_FORMAT();
+
+    NaturalDocs::Version->ToBinaryFile(\*PREVIOUSSTATEFILEHANDLE, NaturalDocs::Settings->AppVersion());
+
+    $self->WritePreviousMenuStateEntries($menu->GroupContent(), \*PREVIOUSSTATEFILEHANDLE);
+
+    close(PREVIOUSSTATEFILEHANDLE);
+    };
+
+
+#
+#   Function: WritePreviousMenuStateEntries
+#
+#   A recursive function to write the contents of an arrayref of <NaturalDocs::Menu::Entry> objects to disk.
+#
+#   Parameters:
+#
+#       entries          - The arrayref of menu entries to write.
+#       fileHandle      - The handle to the output file.
+#
+sub WritePreviousMenuStateEntries #(entries, fileHandle)
+    {
+    my ($self, $entries, $fileHandle) = @_;
+
+    foreach my $entry (@$entries)
+        {
+        if ($entry->Type() == ::MENU_FILE())
+            {
+            # We need to do length manually instead of using n/A in the template because it's not supported in earlier versions
+            # of Perl.
+
+            # [UInt8: MENU_FILE] [UInt8: noAutoTitle] [AString16: title] [AString16: target]
+            print $fileHandle pack('CCnA*nA*', ::MENU_FILE(), ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE() ? 1 : 0),
+                                                                length($entry->Title()), $entry->Title(),
+                                                                length($entry->Target()), $entry->Target());
+            }
+
+        elsif ($entry->Type() == ::MENU_GROUP())
+            {
+            # [UInt8: MENU_GROUP] [AString16: title]
+            print $fileHandle pack('CnA*', ::MENU_GROUP(), length($entry->Title()), $entry->Title());
+            $self->WritePreviousMenuStateEntries($entry->GroupContent(), $fileHandle);
+            print $fileHandle pack('C', 0);
+            }
+
+        elsif ($entry->Type() == ::MENU_INDEX())
+            {
+            # [UInt8: MENU_INDEX] [AString16: title] [AString16: topic type]
+            print $fileHandle pack('CnA*nA*', ::MENU_INDEX(), length($entry->Title()), $entry->Title(),
+                                                                                       length($entry->Target()), $entry->Target());
+            }
+
+        elsif ($entry->Type() == ::MENU_LINK())
+            {
+            # [UInt8: MENU_LINK] [AString16: title] [AString16: url]
+            print $fileHandle pack('CnA*nA*', ::MENU_LINK(), length($entry->Title()), $entry->Title(),
+                                                             length($entry->Target()), $entry->Target());
+            }
+
+        elsif ($entry->Type() == ::MENU_TEXT())
+            {
+            # [UInt8: MENU_TEXT] [AString16: hext]
+            print $fileHandle pack('CnA*', ::MENU_TEXT(), length($entry->Title()), $entry->Title());
+            };
+        };
+
+    };
+
+
+#
+#   Function: CheckForTrashedMenu
+#
+#   Checks the menu to see if a significant number of file entries didn't resolve to actual files, and if so, saves a backup of the
+#   menu and issues a warning.
+#
+#   Parameters:
+#
+#       numberOriginallyInMenu - A count of how many file entries were in the menu orignally.
+#       numberRemoved - A count of how many file entries were removed from the menu.
+#
+sub CheckForTrashedMenu #(numberOriginallyInMenu, numberRemoved)
+    {
+    my ($self, $numberOriginallyInMenu, $numberRemoved) = @_;
+
+    no integer;
+
+    if ( ($numberOriginallyInMenu >= 6 && $numberRemoved == $numberOriginallyInMenu) ||
+         ($numberOriginallyInMenu >= 12 && ($numberRemoved / $numberOriginallyInMenu) >= 0.4) ||
+         ($numberRemoved >= 15) )
+        {
+        my $backupFile = NaturalDocs::Project->UserConfigFile('Menu_Backup.txt');
+        my $backupFileNumber = 1;
+
+        while (-e $backupFile)
+            {
+            $backupFileNumber++;
+            $backupFile = NaturalDocs::Project->UserConfigFile('Menu_Backup_' . $backupFileNumber . '.txt');
+            };
+
+        NaturalDocs::File->Copy( NaturalDocs::Project->UserConfigFile('Menu.txt'), $backupFile );
+
+        print STDERR
+        "\n"
+        # GNU format.  See http://www.gnu.org/prep/standards_15.html
+        . "NaturalDocs: warning: possible trashed menu\n"
+        . "\n"
+        . "   Natural Docs has detected that a significant number file entries in the\n"
+        . "   menu did not resolve to actual files.  A backup of your original menu file\n"
+        . "   has been saved as\n"
+        . "\n"
+        . "   " . $backupFile . "\n"
+        . "\n"
+        . "   - If you recently deleted a lot of files from your project, you can safely\n"
+        . "     ignore this message.  They have been deleted from the menu as well.\n"
+        . "   - If you recently rearranged your source tree, you may want to restore your\n"
+        . "     menu from the backup and do a search and replace to preserve your layout.\n"
+        . "     Otherwise the position of any moved files will be reset.\n"
+        . "   - If neither of these is the case, you may have gotten the -i parameter\n"
+        . "     wrong in the command line.  You should definitely restore the backup and\n"
+        . "     try again, because otherwise every file in your menu will be reset.\n"
+        . "\n";
+        };
+
+    use integer;
+    };
+
+
+#
+#   Function: GenerateTimestampText
+#
+#   Generates <timestampText> from <timestampCode> with the current date.
+#
+sub GenerateTimestampText
+    {
+    my $self = shift;
+
+    my @longMonths = ( 'January', 'February', 'March', 'April', 'May', 'June',
+                                   'July', 'August', 'September', 'October', 'November', 'December' );
+    my @shortMonths = ( 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec' );
+
+    my (undef, undef, undef, $day, $month, $year) = localtime();
+    $year += 1900;
+
+    my $longDay;
+    if ($day % 10 == 1 && $day != 11)
+        {  $longDay = $day . 'st';  }
+    elsif ($day % 10 == 2 && $day != 12)
+        {  $longDay = $day . 'nd';  }
+    elsif ($day % 10 == 3 && $day != 13)
+        {  $longDay = $day . 'rd';  }
+    else
+        {  $longDay = $day . 'th';  };
+
+
+    $timestampText = $timestampCode;
+
+    $timestampText =~ s/(?<![a-z])month(?![a-z])/$longMonths[$month]/i;
+    $timestampText =~ s/(?<![a-z])mon(?![a-z])/$shortMonths[$month]/i;
+    $timestampText =~ s/(?<![a-z])mm(?![a-z])/sprintf('%02d', $month + 1)/ie;
+    $timestampText =~ s/(?<![a-z])m(?![a-z])/$month + 1/ie;
+
+    $timestampText =~ s/(?<![a-z])day(?![a-z])/$longDay/i;
+    $timestampText =~ s/(?<![a-z])dd(?![a-z])/sprintf('%02d', $day)/ie;
+    $timestampText =~ s/(?<![a-z])d(?![a-z])/$day/i;
+
+    $timestampText =~ s/(?<![a-z])(?:year|yyyy)(?![a-z])/$year/i;
+    $timestampText =~ s/(?<![a-z])(?:year|yyyy)(?![a-z])/$year/i; #XXX
+    $timestampText =~ s/(?<![a-z])yy(?![a-z])/sprintf('%02d', $year % 100)/ie;
+    };
+
+
+use constant CONVERT_PARENTHESIS => 0x01;
+use constant CONVERT_COMMAS => 0x02;
+use constant CONVERT_COLONS => 0x04;
+
+#
+#   Function: ConvertAmpChars
+#   Replaces certain characters in the string with their entities and returns it.
+#
+#   Parameters:
+#
+#       text - The text to convert.
+#       flags - The flags of any additional characters to convert.
+#
+#   Flags:
+#
+#       - CONVERT_PARENTHESIS
+#       - CONVERT_COMMAS
+#       - CONVERT_COLONS
+#
+#   Returns:
+#
+#       The string with the amp chars converted.
+#
+sub ConvertAmpChars #(string text, int flags) => string
+    {
+    my ($self, $text, $flags) = @_;
+
+    $text =~ s/&/&amp;/g;
+    $text =~ s/\{/&lbrace;/g;
+    $text =~ s/\}/&rbrace;/g;
+
+    if ($flags & CONVERT_PARENTHESIS())
+        {
+        $text =~ s/\(/&lparen;/g;
+        $text =~ s/\)/&rparen;/g;
+        };
+    if ($flags & CONVERT_COMMAS())
+        {
+        $text =~ s/\,/&comma;/g;
+        };
+    if ($flags & CONVERT_COLONS())
+        {
+        $text =~ s/\:/&colon;/g;
+        };
+
+    return $text;
+    };
+
+
+#
+#   Function: RestoreAmpChars
+#   Replaces entity characters in the string with their original characters and returns it.  This will restore all amp chars regardless
+#   of the flags passed to <ConvertAmpChars()>.
+#
+sub RestoreAmpChars #(string text) => string
+    {
+    my ($self, $text) = @_;
+
+    $text =~ s/&lparen;/(/gi;
+    $text =~ s/&rparen;/)/gi;
+    $text =~ s/&lbrace;/{/gi;
+    $text =~ s/&rbrace;/}/gi;
+    $text =~ s/&comma;/,/gi;
+    $text =~ s/&amp;/&/gi;
+    $text =~ s/&colon;/:/gi;
+
+    return $text;
+    };
+
+
+
+###############################################################################
+# Group: Auto-Adjustment Functions
+
+
+#
+#   Function: ResolveInputDirectories
+#
+#   Detects if the input directories in the menu file match those in the command line, and if not, tries to resolve them.  This allows
+#   menu files to work across machines, since the absolute paths won't be the same but the relative ones should be.
+#
+#   Parameters:
+#
+#       inputDirectoryNames - A hashref of the input directories appearing in the menu file, or undef if none.  The keys are the
+#                                        directories, and the values are their names.  May be undef.
+#
+sub ResolveInputDirectories #(inputDirectoryNames)
+    {
+    my ($self, $menuDirectoryNames) = @_;
+
+
+    # Determine which directories don't match the command line, if any.
+
+    my $inputDirectories = NaturalDocs::Settings->InputDirectories();
+    my @unresolvedMenuDirectories;
+
+    foreach my $menuDirectory (keys %$menuDirectoryNames)
+        {
+        my $found;
+
+        foreach my $inputDirectory (@$inputDirectories)
+            {
+            if ($menuDirectory eq $inputDirectory)
+                {
+                $found = 1;
+                last;
+                };
+            };
+
+        if (!$found)
+            {  push @unresolvedMenuDirectories, $menuDirectory;  };
+        };
+
+    # Quit if everything matches up, which should be the most common case.
+    if (!scalar @unresolvedMenuDirectories)
+        {  return;  };
+
+    # Poop.  See which input directories are still available.
+
+    my @unresolvedInputDirectories;
+
+    foreach my $inputDirectory (@$inputDirectories)
+        {
+        if (!exists $menuDirectoryNames->{$inputDirectory})
+            {  push @unresolvedInputDirectories, $inputDirectory;  };
+        };
+
+    # Quit if there are none.  This means an input directory is in the menu that isn't in the command line.  Natural Docs should
+    # proceed normally and let the files be deleted.
+    if (!scalar @unresolvedInputDirectories)
+        {
+        $hasChanged = 1;
+        return;
+        };
+
+    # The index into menuDirectoryScores is the same as in unresolvedMenuDirectories.  The index into each arrayref within it is
+    # the same as in unresolvedInputDirectories.
+    my @menuDirectoryScores;
+    for (my $i = 0; $i < scalar @unresolvedMenuDirectories; $i++)
+        {  push @menuDirectoryScores, [ ];  };
+
+
+    # Now plow through the menu, looking for files that have an unresolved base.
+
+    my @menuGroups = ( $menu );
+
+    while (scalar @menuGroups)
+        {
+        my $currentGroup = pop @menuGroups;
+        my $currentGroupContent = $currentGroup->GroupContent();
+
+        foreach my $entry (@$currentGroupContent)
+            {
+            if ($entry->Type() == ::MENU_GROUP())
+                {
+                push @menuGroups, $entry;
+                }
+            elsif ($entry->Type() == ::MENU_FILE())
+                {
+                # Check if it uses an unresolved base.
+                for (my $i = 0; $i < scalar @unresolvedMenuDirectories; $i++)
+                    {
+                    if (NaturalDocs::File->IsSubPathOf($unresolvedMenuDirectories[$i], $entry->Target()))
+                        {
+                        my $relativePath = NaturalDocs::File->MakeRelativePath($unresolvedMenuDirectories[$i], $entry->Target());
+                        $self->ResolveFile($relativePath, \@unresolvedInputDirectories, $menuDirectoryScores[$i]);
+                        last;
+                        };
+                    };
+                };
+            };
+        };
+
+
+    # Now, create an array of score objects.  Each score object is the three value arrayref [ from, to, score ].  From and To are the
+    # conversion options and are the indexes into unresolvedInput/MenuDirectories.  We'll sort this array by score to get the best
+    # possible conversions.  Yes, really.
+    my @scores;
+
+    for (my $menuIndex = 0; $menuIndex < scalar @unresolvedMenuDirectories; $menuIndex++)
+        {
+        for (my $inputIndex = 0; $inputIndex < scalar @unresolvedInputDirectories; $inputIndex++)
+            {
+            if ($menuDirectoryScores[$menuIndex]->[$inputIndex])
+                {
+                push @scores, [ $menuIndex, $inputIndex, $menuDirectoryScores[$menuIndex]->[$inputIndex] ];
+                };
+            };
+        };
+
+    @scores = sort { $b->[2] <=> $a->[2] } @scores;
+
+
+    # Now we determine what goes where.
+    my @menuDirectoryConversions;
+
+    foreach my $scoreObject (@scores)
+        {
+        if (!defined $menuDirectoryConversions[ $scoreObject->[0] ])
+            {
+            $menuDirectoryConversions[ $scoreObject->[0] ] = $unresolvedInputDirectories[ $scoreObject->[1] ];
+            };
+        };
+
+
+    # Now, FINALLY, we do the conversion.  Note that not every menu directory may have a conversion defined.
+
+    @menuGroups = ( $menu );
+
+    while (scalar @menuGroups)
+        {
+        my $currentGroup = pop @menuGroups;
+        my $currentGroupContent = $currentGroup->GroupContent();
+
+        foreach my $entry (@$currentGroupContent)
+            {
+            if ($entry->Type() == ::MENU_GROUP())
+                {
+                push @menuGroups, $entry;
+                }
+            elsif ($entry->Type() == ::MENU_FILE())
+                {
+                # Check if it uses an unresolved base.
+                for (my $i = 0; $i < scalar @unresolvedMenuDirectories; $i++)
+                    {
+                    if (NaturalDocs::File->IsSubPathOf($unresolvedMenuDirectories[$i], $entry->Target()) &&
+                        defined $menuDirectoryConversions[$i])
+                        {
+                        my $relativePath = NaturalDocs::File->MakeRelativePath($unresolvedMenuDirectories[$i], $entry->Target());
+                        $entry->SetTarget( NaturalDocs::File->JoinPaths($menuDirectoryConversions[$i], $relativePath) );
+                        last;
+                        };
+                    };
+                };
+            };
+        };
+
+
+    # Whew.
+
+    $hasChanged = 1;
+    };
+
+
+#
+#   Function: ResolveRelativeInputDirectories
+#
+#   Resolves relative input directories to the input directories available.
+#
+sub ResolveRelativeInputDirectories
+    {
+    my ($self) = @_;
+
+    my $inputDirectories = NaturalDocs::Settings->InputDirectories();
+    my $resolvedInputDirectory;
+
+    if (scalar @$inputDirectories == 1)
+        {  $resolvedInputDirectory = $inputDirectories->[0];  }
+    else
+        {
+        my @score;
+
+        # Plow through the menu, looking for files and scoring them.
+
+        my @menuGroups = ( $menu );
+
+        while (scalar @menuGroups)
+            {
+            my $currentGroup = pop @menuGroups;
+            my $currentGroupContent = $currentGroup->GroupContent();
+
+            foreach my $entry (@$currentGroupContent)
+                {
+                if ($entry->Type() == ::MENU_GROUP())
+                    {
+                    push @menuGroups, $entry;
+                    }
+                elsif ($entry->Type() == ::MENU_FILE())
+                    {
+                    $self->ResolveFile($entry->Target(), $inputDirectories, \@score);
+                    };
+                };
+            };
+
+        # Determine the best match.
+
+        my $bestScore = 0;
+        my $bestIndex = 0;
+
+        for (my $i = 0; $i < scalar @$inputDirectories; $i++)
+            {
+            if ($score[$i] > $bestScore)
+                {
+                $bestScore = $score[$i];
+                $bestIndex = $i;
+                };
+            };
+
+        $resolvedInputDirectory = $inputDirectories->[$bestIndex];
+        };
+
+
+    # Okay, now that we have our resolved directory, update everything.
+
+    my @menuGroups = ( $menu );
+
+    while (scalar @menuGroups)
+        {
+        my $currentGroup = pop @menuGroups;
+        my $currentGroupContent = $currentGroup->GroupContent();
+
+        foreach my $entry (@$currentGroupContent)
+            {
+            if ($entry->Type() == ::MENU_GROUP())
+                {  push @menuGroups, $entry;  }
+            elsif ($entry->Type() == ::MENU_FILE())
+                {
+                $entry->SetTarget( NaturalDocs::File->JoinPaths($resolvedInputDirectory, $entry->Target()) );
+                };
+            };
+        };
+
+    if (scalar @$inputDirectories > 1)
+        {  $hasChanged = 1;  };
+
+    return $resolvedInputDirectory;
+    };
+
+
+#
+#   Function: ResolveFile
+#
+#   Tests a relative path against a list of directories.  Adds one to the score of each base where there is a match.
+#
+#   Parameters:
+#
+#       relativePath - The relative file name to test.
+#       possibleBases - An arrayref of bases to test it against.
+#       possibleBaseScores - An arrayref of scores to adjust.  The score indexes should correspond to the base indexes.
+#
+sub ResolveFile #(relativePath, possibleBases, possibleBaseScores)
+    {
+    my ($self, $relativePath, $possibleBases, $possibleBaseScores) = @_;
+
+    for (my $i = 0; $i < scalar @$possibleBases; $i++)
+        {
+        if (-e NaturalDocs::File->JoinPaths($possibleBases->[$i], $relativePath))
+            {  $possibleBaseScores->[$i]++;  };
+        };
+    };
+
+
+#
+#   Function: LockUserTitleChanges
+#
+#   Detects if the user manually changed any file titles, and if so, automatically locks them with <MENU_FILE_NOAUTOTITLE>.
+#
+#   Parameters:
+#
+#       previousMenuFiles - A hashref of the files from the previous menu state.  The keys are the <FileNames>, and the values are
+#                                    references to their <NaturalDocs::Menu::Entry> objects.
+#
+sub LockUserTitleChanges #(previousMenuFiles)
+    {
+    my ($self, $previousMenuFiles) = @_;
+
+    my @groupStack = ( $menu );
+    my $groupEntry;
+
+    while (scalar @groupStack)
+        {
+        $groupEntry = pop @groupStack;
+
+        foreach my $entry (@{$groupEntry->GroupContent()})
+            {
+
+            # If it's an unlocked file entry
+            if ($entry->Type() == ::MENU_FILE() && ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0)
+                {
+                my $previousEntry = $previousMenuFiles->{$entry->Target()};
+
+                # If the previous entry was also unlocked and the titles are different, the user changed the title.  Automatically lock it.
+                if (defined $previousEntry && ($previousEntry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0 &&
+                    $entry->Title() ne $previousEntry->Title())
+                    {
+                    $entry->SetFlags($entry->Flags() | ::MENU_FILE_NOAUTOTITLE());
+                    $hasChanged = 1;
+                    };
+                }
+
+            elsif ($entry->Type() == ::MENU_GROUP())
+                {
+                push @groupStack, $entry;
+                };
+
+            };
+        };
+    };
+
+
+#
+#   Function: FlagAutoTitleChanges
+#
+#   Finds which files have auto-titles that changed and flags their groups for updating with <MENU_GROUP_UPDATETITLES> and
+#   <MENU_GROUP_UPDATEORDER>.
+#
+sub FlagAutoTitleChanges
+    {
+    my ($self) = @_;
+
+    my @groupStack = ( $menu );
+    my $groupEntry;
+
+    while (scalar @groupStack)
+        {
+        $groupEntry = pop @groupStack;
+
+        foreach my $entry (@{$groupEntry->GroupContent()})
+            {
+            if ($entry->Type() == ::MENU_FILE() && ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0 &&
+                exists $defaultTitlesChanged{$entry->Target()})
+                {
+                $groupEntry->SetFlags($groupEntry->Flags() | ::MENU_GROUP_UPDATETITLES() | ::MENU_GROUP_UPDATEORDER());
+                $hasChanged = 1;
+                }
+            elsif ($entry->Type() == ::MENU_GROUP())
+                {
+                push @groupStack, $entry;
+                };
+            };
+        };
+    };
+
+
+#
+#   Function: AutoPlaceNewFiles
+#
+#   Adds files to the menu that aren't already on it, attempting to guess where they belong.
+#
+#   New files are placed after a dummy <MENU_ENDOFORIGINAL> entry so that they don't affect the detected order.  Also, the
+#   groups they're placed in get <MENU_GROUP_UPDATETITLES>, <MENU_GROUP_UPDATESTRUCTURE>, and
+#   <MENU_GROUP_UPDATEORDER> flags.
+#
+#   Parameters:
+#
+#       filesInMenu - An existence hash of all the <FileNames> present in the menu.
+#
+sub AutoPlaceNewFiles #(fileInMenu)
+    {
+    my ($self, $filesInMenu) = @_;
+
+    my $files = NaturalDocs::Project->FilesWithContent();
+
+    my $directories;
+
+    foreach my $file (keys %$files)
+        {
+        if (!exists $filesInMenu->{$file})
+            {
+            # This is done on demand because new files shouldn't be added very often, so this will save time.
+            if (!defined $directories)
+                {  $directories = $self->MatchDirectoriesAndGroups();  };
+
+            my $targetGroup;
+            my $fileDirectoryString = (NaturalDocs::File->SplitPath($file))[1];
+
+            $targetGroup = $directories->{$fileDirectoryString};
+
+            if (!defined $targetGroup)
+                {
+                # Okay, if there's no exact match, work our way down.
+
+                my @fileDirectories = NaturalDocs::File->SplitDirectories($fileDirectoryString);
+
+                do
+                    {
+                    pop @fileDirectories;
+                    $targetGroup = $directories->{ NaturalDocs::File->JoinDirectories(@fileDirectories) };
+                    }
+                while (!defined $targetGroup && scalar @fileDirectories);
+
+                if (!defined $targetGroup)
+                    {  $targetGroup = $menu;  };
+                };
+
+            $targetGroup->MarkEndOfOriginal();
+            $targetGroup->PushToGroup( NaturalDocs::Menu::Entry->New(::MENU_FILE(), undef, $file, undef) );
+
+            $targetGroup->SetFlags( $targetGroup->Flags() | ::MENU_GROUP_UPDATETITLES() |
+                                                 ::MENU_GROUP_UPDATESTRUCTURE() | ::MENU_GROUP_UPDATEORDER() );
+
+            $hasChanged = 1;
+            };
+        };
+    };
+
+
+#
+#   Function: MatchDirectoriesAndGroups
+#
+#   Determines which groups files in certain directories should be placed in.
+#
+#   Returns:
+#
+#       A hashref.  The keys are the directory names, and the values are references to the group objects they should be placed in.
+#
+#       This only repreesents directories that currently have files on the menu, so it shouldn't be assumed that every possible
+#       directory will exist.  To match, you should first try to match the directory, and then strip the deepest directories one by
+#       one until there's a match or there's none left.  If there's none left, use the root group <menu>.
+#
+sub MatchDirectoriesAndGroups
+    {
+    my ($self) = @_;
+
+    # The keys are the directory names, and the values are hashrefs.  For the hashrefs, the keys are the group objects, and the
+    # values are the number of files in them from that directory.  In other words,
+    # $directories{$directory}->{$groupEntry} = $count;
+    my %directories;
+    # Note that we need to use Tie::RefHash to use references as keys.  Won't work otherwise.  Also, not every Perl distro comes
+    # with Tie::RefHash::Nestable, so we can't rely on that.
+
+    # We're using an index instead of pushing and popping because we want to save a list of the groups in the order they appear
+    # to break ties.
+    my @groups = ( $menu );
+    my $groupIndex = 0;
+
+
+    # Count the number of files in each group that appear in each directory.
+
+    while ($groupIndex < scalar @groups)
+        {
+        my $groupEntry = $groups[$groupIndex];
+
+        foreach my $entry (@{$groupEntry->GroupContent()})
+            {
+            if ($entry->Type() == ::MENU_GROUP())
+                {
+                push @groups, $entry;
+                }
+            elsif ($entry->Type() == ::MENU_FILE())
+                {
+                my $directory = (NaturalDocs::File->SplitPath($entry->Target()))[1];
+
+                if (!exists $directories{$directory})
+                    {
+                    my $subHash = { };
+                    tie %$subHash, 'Tie::RefHash';
+                    $directories{$directory} = $subHash;
+                    };
+
+                if (!exists $directories{$directory}->{$groupEntry})
+                    {  $directories{$directory}->{$groupEntry} = 1;  }
+                else
+                    {  $directories{$directory}->{$groupEntry}++;  };
+                };
+            };
+
+        $groupIndex++;
+        };
+
+
+    # Determine which group goes with which directory, breaking ties by using whichever group appears first.
+
+    my $finalDirectories = { };
+
+    while (my ($directory, $directoryGroups) = each %directories)
+        {
+        my $bestGroup;
+        my $bestCount = 0;
+        my %tiedGroups;  # Existence hash
+
+        while (my ($group, $count) = each %$directoryGroups)
+            {
+            if ($count > $bestCount)
+                {
+                $bestGroup = $group;
+                $bestCount = $count;
+                %tiedGroups = ( );
+                }
+            elsif ($count == $bestCount)
+                {
+                $tiedGroups{$group} = 1;
+                };
+            };
+
+        # Break ties.
+        if (scalar keys %tiedGroups)
+            {
+            $tiedGroups{$bestGroup} = 1;
+
+            foreach my $group (@groups)
+                {
+                if (exists $tiedGroups{$group})
+                    {
+                    $bestGroup = $group;
+                    last;
+                    };
+                };
+            };
+
+
+        $finalDirectories->{$directory} = $bestGroup;
+        };
+
+
+    return $finalDirectories;
+    };
+
+
+#
+#   Function: RemoveDeadFiles
+#
+#   Removes files from the menu that no longer exist or no longer have Natural Docs content.
+#
+#   Returns:
+#
+#       The number of file entries removed.
+#
+sub RemoveDeadFiles
+    {
+    my ($self) = @_;
+
+    my @groupStack = ( $menu );
+    my $numberRemoved = 0;
+
+    my $filesWithContent = NaturalDocs::Project->FilesWithContent();
+
+    while (scalar @groupStack)
+        {
+        my $groupEntry = pop @groupStack;
+        my $groupContent = $groupEntry->GroupContent();
+
+        my $index = 0;
+        while ($index < scalar @$groupContent)
+            {
+            if ($groupContent->[$index]->Type() == ::MENU_FILE() &&
+                !exists $filesWithContent->{ $groupContent->[$index]->Target() } )
+                {
+                $groupEntry->DeleteFromGroup($index);
+
+                $groupEntry->SetFlags( $groupEntry->Flags() | ::MENU_GROUP_UPDATETITLES() |
+                                                   ::MENU_GROUP_UPDATESTRUCTURE() );
+                $numberRemoved++;
+                $hasChanged = 1;
+                }
+
+            elsif ($groupContent->[$index]->Type() == ::MENU_GROUP())
+                {
+                push @groupStack, $groupContent->[$index];
+                $index++;
+                }
+
+            else
+                {  $index++;  };
+            };
+        };
+
+    return $numberRemoved;
+    };
+
+
+#
+#   Function: BanAndUnbanIndexes
+#
+#   Adjusts the indexes that are banned depending on if the user added or deleted any.
+#
+sub BanAndUnbanIndexes
+    {
+    my ($self) = @_;
+
+    # Unban any indexes that are present, meaning the user added them back manually without deleting the ban.
+    foreach my $index (keys %indexes)
+        {  delete $bannedIndexes{$index};  };
+
+    # Ban any indexes that were in the previous menu but not the current, meaning the user manually deleted them.  However,
+    # don't do this if the topic isn't indexable, meaning they changed the topic type rather than the menu.
+    foreach my $index (keys %previousIndexes)
+        {
+        if (!exists $indexes{$index} && NaturalDocs::Topics->TypeInfo($index)->Index())
+            {  $bannedIndexes{$index} = 1;  };
+        };
+    };
+
+
+#
+#   Function: AddAndRemoveIndexes
+#
+#   Automatically adds and removes index entries on the menu as necessary.  <DetectIndexGroups()> should be called
+#   beforehand.
+#
+sub AddAndRemoveIndexes
+    {
+    my ($self) = @_;
+
+    my %validIndexes;
+    my @allIndexes = NaturalDocs::Topics->AllIndexableTypes();
+
+    foreach my $index (@allIndexes)
+        {
+        # Strip the banned indexes first so it's potentially less work for SymbolTable.
+        if (!exists $bannedIndexes{$index})
+            {  $validIndexes{$index} = 1;  };
+        };
+
+    %validIndexes = %{NaturalDocs::SymbolTable->HasIndexes(\%validIndexes)};
+
+
+    # Delete dead indexes and find the best index group.
+
+    my @groupStack = ( $menu );
+
+    my $bestIndexGroup;
+    my $bestIndexCount = 0;
+
+    while (scalar @groupStack)
+        {
+        my $currentGroup = pop @groupStack;
+        my $index = 0;
+
+        my $currentIndexCount = 0;
+
+        while ($index < scalar @{$currentGroup->GroupContent()})
+            {
+            my $entry = $currentGroup->GroupContent()->[$index];
+
+            if ($entry->Type() == ::MENU_INDEX())
+                {
+                $currentIndexCount++;
+
+                if ($currentIndexCount > $bestIndexCount)
+                    {
+                    $bestIndexCount = $currentIndexCount;
+                    $bestIndexGroup = $currentGroup;
+                    };
+
+                # Remove it if it's dead.
+
+                if (!exists $validIndexes{ $entry->Target() })
+                    {
+                    $currentGroup->DeleteFromGroup($index);
+                    delete $indexes{ $entry->Target() };
+                    $hasChanged = 1;
+                    }
+                else
+                    {  $index++;  };
+                }
+
+            else
+                {
+                if ($entry->Type() == ::MENU_GROUP())
+                    {  push @groupStack, $entry;  };
+
+                $index++;
+                };
+            };
+        };
+
+
+    # Now add the new indexes.
+
+    foreach my $index (keys %indexes)
+        {  delete $validIndexes{$index};  };
+
+    if (scalar keys %validIndexes)
+        {
+        # Add a group if there are no indexes at all.
+
+        if ($bestIndexCount == 0)
+            {
+            $menu->MarkEndOfOriginal();
+
+            my $newIndexGroup = NaturalDocs::Menu::Entry->New(::MENU_GROUP(), 'Index', undef,
+                                                                                              ::MENU_GROUP_ISINDEXGROUP());
+            $menu->PushToGroup($newIndexGroup);
+
+            $bestIndexGroup = $newIndexGroup;
+            $menu->SetFlags( $menu->Flags() | ::MENU_GROUP_UPDATEORDER() | ::MENU_GROUP_UPDATESTRUCTURE() );
+            };
+
+        # Add the new indexes.
+
+        $bestIndexGroup->MarkEndOfOriginal();
+        my $isIndexGroup = $bestIndexGroup->Flags() & ::MENU_GROUP_ISINDEXGROUP();
+
+        foreach my $index (keys %validIndexes)
+            {
+            my $title;
+
+            if ($isIndexGroup)
+                {
+                if ($index eq ::TOPIC_GENERAL())
+                    {  $title = 'Everything';  }
+                else
+                    {  $title = NaturalDocs::Topics->NameOfType($index, 1);  };
+                }
+            else
+                {
+                $title = NaturalDocs::Topics->NameOfType($index) . ' Index';
+                };
+
+            my $newEntry = NaturalDocs::Menu::Entry->New(::MENU_INDEX(), $title, $index, undef);
+            $bestIndexGroup->PushToGroup($newEntry);
+
+            $indexes{$index} = 1;
+            };
+
+        $bestIndexGroup->SetFlags( $bestIndexGroup->Flags() |
+                                                   ::MENU_GROUP_UPDATEORDER() | ::MENU_GROUP_UPDATESTRUCTURE() );
+        $hasChanged = 1;
+        };
+    };
+
+
+#
+#   Function: RemoveDeadGroups
+#
+#   Removes groups with less than two entries.  It will always remove empty groups, and it will remove groups with one entry if it
+#   has the <MENU_GROUP_UPDATESTRUCTURE> flag.
+#
+sub RemoveDeadGroups
+    {
+    my ($self) = @_;
+
+    my $index = 0;
+
+    while ($index < scalar @{$menu->GroupContent()})
+        {
+        my $entry = $menu->GroupContent()->[$index];
+
+        if ($entry->Type() == ::MENU_GROUP())
+            {
+            my $removed = $self->RemoveIfDead($entry, $menu, $index);
+
+            if (!$removed)
+                {  $index++;  };
+            }
+        else
+            {  $index++;  };
+        };
+    };
+
+
+#
+#   Function: RemoveIfDead
+#
+#   Checks a group and all its sub-groups for life and remove any that are dead.  Empty groups are removed, and groups with one
+#   entry and the <MENU_GROUP_UPDATESTRUCTURE> flag have their entry moved to the parent group.
+#
+#   Parameters:
+#
+#       groupEntry - The group to check for possible deletion.
+#       parentGroupEntry - The parent group to move the single entry to if necessary.
+#       parentGroupIndex - The index of the group in its parent.
+#
+#   Returns:
+#
+#       Whether the group was removed or not.
+#
+sub RemoveIfDead #(groupEntry, parentGroupEntry, parentGroupIndex)
+    {
+    my ($self, $groupEntry, $parentGroupEntry, $parentGroupIndex) = @_;
+
+
+    # Do all sub-groups first, since their deletions will affect our UPDATESTRUCTURE flag and content count.
+
+    my $index = 0;
+    while ($index < scalar @{$groupEntry->GroupContent()})
+        {
+        my $entry = $groupEntry->GroupContent()->[$index];
+
+        if ($entry->Type() == ::MENU_GROUP())
+            {
+            my $removed = $self->RemoveIfDead($entry, $groupEntry, $index);
+
+            if (!$removed)
+                {  $index++;  };
+            }
+        else
+            {  $index++;  };
+        };
+
+
+    # Now check ourself.
+
+    my $count = scalar @{$groupEntry->GroupContent()};
+    if ($groupEntry->Flags() & ::MENU_GROUP_HASENDOFORIGINAL())
+        {  $count--;  };
+
+    if ($count == 0)
+        {
+        $parentGroupEntry->DeleteFromGroup($parentGroupIndex);
+
+        $parentGroupEntry->SetFlags( $parentGroupEntry->Flags() | ::MENU_GROUP_UPDATESTRUCTURE() );
+
+        $hasChanged = 1;
+        return 1;
+        }
+    elsif ($count == 1 && ($groupEntry->Flags() & ::MENU_GROUP_UPDATESTRUCTURE()) )
+        {
+        my $onlyEntry = $groupEntry->GroupContent()->[0];
+        if ($onlyEntry->Type() == ::MENU_ENDOFORIGINAL())
+            {  $onlyEntry = $groupEntry->GroupContent()->[1];  };
+
+        $parentGroupEntry->DeleteFromGroup($parentGroupIndex);
+
+        $parentGroupEntry->MarkEndOfOriginal();
+        $parentGroupEntry->PushToGroup($onlyEntry);
+
+        $parentGroupEntry->SetFlags( $parentGroupEntry->Flags() | ::MENU_GROUP_UPDATETITLES() |
+                                                     ::MENU_GROUP_UPDATEORDER() | ::MENU_GROUP_UPDATESTRUCTURE() );
+
+        $hasChanged = 1;
+        return 1;
+        }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: DetectIndexGroups
+#
+#   Finds groups that are primarily used for indexes and gives them the <MENU_GROUP_ISINDEXGROUP> flag.
+#
+sub DetectIndexGroups
+    {
+    my ($self) = @_;
+
+    my @groupStack = ( $menu );
+
+    while (scalar @groupStack)
+        {
+        my $groupEntry = pop @groupStack;
+
+        my $isIndexGroup = -1;  # -1: Can't tell yet.  0: Can't be an index group.  1: Is an index group so far.
+
+        foreach my $entry (@{$groupEntry->GroupContent()})
+            {
+            if ($entry->Type() == ::MENU_INDEX())
+                {
+                if ($isIndexGroup == -1)
+                    {  $isIndexGroup = 1;  };
+                }
+
+            # Text is tolerated, but it still needs at least one index entry.
+            elsif ($entry->Type() != ::MENU_TEXT())
+                {
+                $isIndexGroup = 0;
+
+                if ($entry->Type() == ::MENU_GROUP())
+                    {  push @groupStack, $entry;  };
+                };
+            };
+
+        if ($isIndexGroup == 1)
+            {
+            $groupEntry->SetFlags( $groupEntry->Flags() | ::MENU_GROUP_ISINDEXGROUP() );
+            };
+        };
+    };
+
+
+#
+#   Function: CreateDirectorySubGroups
+#
+#   Where possible, creates sub-groups based on directories for any long groups that have <MENU_GROUP_UPDATESTRUCTURE>
+#   set.  Clears the flag afterwards on groups that are short enough to not need any more sub-groups, but leaves it for the rest.
+#
+sub CreateDirectorySubGroups
+    {
+    my ($self) = @_;
+
+    my @groupStack = ( $menu );
+
+    foreach my $groupEntry (@groupStack)
+        {
+        if ($groupEntry->Flags() & ::MENU_GROUP_UPDATESTRUCTURE())
+            {
+            # Count the number of files.
+
+            my $fileCount = 0;
+
+            foreach my $entry (@{$groupEntry->GroupContent()})
+                {
+                if ($entry->Type() == ::MENU_FILE())
+                    {  $fileCount++;  };
+                };
+
+
+            if ($fileCount > MAXFILESINGROUP)
+                {
+                my @sharedDirectories = $self->SharedDirectoriesOf($groupEntry);
+                my $unsharedIndex = scalar @sharedDirectories;
+
+                # The keys are the first directory entries after the shared ones, and the values are the number of files that are in
+                # that directory.  Files that don't have subdirectories after the shared directories aren't included because they shouldn't
+                # be put in a subgroup.
+                my %directoryCounts;
+
+                foreach my $entry (@{$groupEntry->GroupContent()})
+                    {
+                    if ($entry->Type() == ::MENU_FILE())
+                        {
+                        my @entryDirectories = NaturalDocs::File->SplitDirectories( (NaturalDocs::File->SplitPath($entry->Target()))[1] );
+
+                        if (scalar @entryDirectories > $unsharedIndex)
+                            {
+                            my $unsharedDirectory = $entryDirectories[$unsharedIndex];
+
+                            if (!exists $directoryCounts{$unsharedDirectory})
+                                {  $directoryCounts{$unsharedDirectory} = 1;  }
+                            else
+                                {  $directoryCounts{$unsharedDirectory}++;  };
+                            };
+                        };
+                    };
+
+
+                # Now create the subgroups.
+
+                # The keys are the first directory entries after the shared ones, and the values are the groups for those files to be
+                # put in.  There will only be entries for the groups with at least MINFILESINNEWGROUP files.
+                my %directoryGroups;
+
+                while (my ($directory, $count) = each %directoryCounts)
+                    {
+                    if ($count >= MINFILESINNEWGROUP)
+                        {
+                        my $newGroup = NaturalDocs::Menu::Entry->New( ::MENU_GROUP(), ucfirst($directory), undef,
+                                                                                                   ::MENU_GROUP_UPDATETITLES() |
+                                                                                                   ::MENU_GROUP_UPDATEORDER() );
+
+                        if ($count > MAXFILESINGROUP)
+                            {  $newGroup->SetFlags( $newGroup->Flags() | ::MENU_GROUP_UPDATESTRUCTURE());  };
+
+                        $groupEntry->MarkEndOfOriginal();
+                        push @{$groupEntry->GroupContent()}, $newGroup;
+
+                        $directoryGroups{$directory} = $newGroup;
+                        $fileCount -= $count;
+                        };
+                    };
+
+
+                # Now fill the subgroups.
+
+                if (scalar keys %directoryGroups)
+                    {
+                    my $afterOriginal;
+                    my $index = 0;
+
+                    while ($index < scalar @{$groupEntry->GroupContent()})
+                        {
+                        my $entry = $groupEntry->GroupContent()->[$index];
+
+                        if ($entry->Type() == ::MENU_FILE())
+                            {
+                            my @entryDirectories =
+                                NaturalDocs::File->SplitDirectories( (NaturalDocs::File->SplitPath($entry->Target()))[1] );
+
+                            my $unsharedDirectory = $entryDirectories[$unsharedIndex];
+
+                            if (exists $directoryGroups{$unsharedDirectory})
+                                {
+                                my $targetGroup = $directoryGroups{$unsharedDirectory};
+
+                                if ($afterOriginal)
+                                    {  $targetGroup->MarkEndOfOriginal();  };
+                                $targetGroup->PushToGroup($entry);
+
+                                $groupEntry->DeleteFromGroup($index);
+                                }
+                            else
+                                {  $index++;  };
+                            }
+
+                        elsif ($entry->Type() == ::MENU_ENDOFORIGINAL())
+                            {
+                            $afterOriginal = 1;
+                            $index++;
+                            }
+
+                        elsif ($entry->Type() == ::MENU_GROUP())
+                            {
+                            # See if we need to relocate this group.
+
+                            my @groupDirectories = $self->SharedDirectoriesOf($entry);
+
+                            # The group's shared directories must be at least two levels deeper than the current.  If the first level deeper
+                            # is a new group, move it there because it's a subdirectory of that one.
+                            if (scalar @groupDirectories - scalar @sharedDirectories >= 2)
+                                {
+                                my $unsharedDirectory = $groupDirectories[$unsharedIndex];
+
+                                if (exists $directoryGroups{$unsharedDirectory} &&
+                                    $directoryGroups{$unsharedDirectory} != $entry)
+                                    {
+                                    my $targetGroup = $directoryGroups{$unsharedDirectory};
+
+                                    if ($afterOriginal)
+                                        {  $targetGroup->MarkEndOfOriginal();  };
+                                    $targetGroup->PushToGroup($entry);
+
+                                    $groupEntry->DeleteFromGroup($index);
+
+                                    # We need to retitle the group if it has the name of the unshared directory.
+
+                                    my $oldTitle = $entry->Title();
+                                    $oldTitle =~ s/ +//g;
+                                    $unsharedDirectory =~ s/ +//g;
+
+                                    if (lc($oldTitle) eq lc($unsharedDirectory))
+                                        {
+                                        $entry->SetTitle($groupDirectories[$unsharedIndex + 1]);
+                                        };
+                                    }
+                                else
+                                    {  $index++;  };
+                                }
+                            else
+                                {  $index++;  };
+                            }
+
+                        else
+                            {  $index++;  };
+                        };
+
+                    $hasChanged = 1;
+
+                    if ($fileCount <= MAXFILESINGROUP)
+                        {  $groupEntry->SetFlags( $groupEntry->Flags() & ~::MENU_GROUP_UPDATESTRUCTURE() );  };
+
+                    $groupEntry->SetFlags( $groupEntry->Flags() | ::MENU_GROUP_UPDATETITLES() |
+                                                                                         ::MENU_GROUP_UPDATEORDER() );
+                    };
+
+                };  # If group has >MAXFILESINGROUP files
+            };  # If group has UPDATESTRUCTURE
+
+
+        # Okay, now go through all the subgroups.  We do this after the above so that newly created groups can get subgrouped
+        # further.
+
+        foreach my $entry (@{$groupEntry->GroupContent()})
+            {
+            if ($entry->Type() == ::MENU_GROUP())
+                {  push @groupStack, $entry;  };
+            };
+
+        };  # For each group entry
+    };
+
+
+#
+#   Function: DetectOrder
+#
+#   Detects the order of the entries in all groups that have the <MENU_GROUP_UPDATEORDER> flag set.  Will set one of the
+#   <MENU_GROUP_FILESSORTED>, <MENU_GROUP_FILESANDGROUPSSORTED>, <MENU_GROUP_EVERYTHINGSORTED>, or
+#   <MENU_GROUP_UNSORTED> flags.  It will always go for the most comprehensive sort possible, so if a group only has one
+#   entry, it will be flagged as <MENU_GROUP_EVERYTHINGSORTED>.
+#
+#   <DetectIndexGroups()> should be called beforehand, as the <MENU_GROUP_ISINDEXGROUP> flag affects how the order is
+#   detected.
+#
+#   The sort detection stops if it reaches a <MENU_ENDOFORIGINAL> entry, so new entries can be added to the end while still
+#   allowing the original sort to be detected.
+#
+#   Parameters:
+#
+#       forceAll - If set, the order will be detected for all groups regardless of whether <MENU_GROUP_UPDATEORDER> is set.
+#
+sub DetectOrder #(forceAll)
+    {
+    my ($self, $forceAll) = @_;
+    my @groupStack = ( $menu );
+
+    while (scalar @groupStack)
+        {
+        my $groupEntry = pop @groupStack;
+        my $index = 0;
+
+
+        # First detect the sort.
+
+        if ($forceAll || ($groupEntry->Flags() & ::MENU_GROUP_UPDATEORDER()) )
+            {
+            my $order = ::MENU_GROUP_EVERYTHINGSORTED();
+
+            my $lastFile;
+            my $lastFileOrGroup;
+
+            while ($index < scalar @{$groupEntry->GroupContent()} &&
+                     $groupEntry->GroupContent()->[$index]->Type() != ::MENU_ENDOFORIGINAL() &&
+                     $order != ::MENU_GROUP_UNSORTED())
+                {
+                my $entry = $groupEntry->GroupContent()->[$index];
+
+
+                # Ignore the last entry if it's an index group.  We don't want it to affect the sort.
+
+                if ($index + 1 == scalar @{$groupEntry->GroupContent()} &&
+                    $entry->Type() == ::MENU_GROUP() && ($entry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) )
+                    {
+                    # Ignore.
+
+                    # This is an awkward code construct, basically working towards an else instead of using an if, but the code just gets
+                    # too hard to read otherwise.  The compiled code should work out to roughly the same thing anyway.
+                    }
+
+
+                # Ignore the first entry if it's the general index in an index group.  We don't want it to affect the sort.
+
+                elsif ($index == 0 && ($groupEntry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) &&
+                        $entry->Type() == ::MENU_INDEX() && $entry->Target() eq ::TOPIC_GENERAL() )
+                    {
+                    # Ignore.
+                    }
+
+
+                # Degenerate the sort.
+
+                else
+                    {
+
+                    if ($order == ::MENU_GROUP_EVERYTHINGSORTED() && $index > 0 &&
+                        ::StringCompare($entry->Title(), $groupEntry->GroupContent()->[$index - 1]->Title()) < 0)
+                        {  $order = ::MENU_GROUP_FILESANDGROUPSSORTED();  };
+
+                    if ($order == ::MENU_GROUP_FILESANDGROUPSSORTED() &&
+                        ($entry->Type() == ::MENU_FILE() || $entry->Type() == ::MENU_GROUP()) &&
+                        defined $lastFileOrGroup && ::StringCompare($entry->Title(), $lastFileOrGroup->Title()) < 0)
+                        {  $order = ::MENU_GROUP_FILESSORTED();  };
+
+                    if ($order == ::MENU_GROUP_FILESSORTED() &&
+                        $entry->Type() == ::MENU_FILE() && defined $lastFile &&
+                        ::StringCompare($entry->Title(), $lastFile->Title()) < 0)
+                        {  $order = ::MENU_GROUP_UNSORTED();  };
+
+                    };
+
+
+                # Set the lastX parameters for comparison and add sub-groups to the stack.
+
+                if ($entry->Type() == ::MENU_FILE())
+                    {
+                    $lastFile = $entry;
+                    $lastFileOrGroup = $entry;
+                    }
+                elsif ($entry->Type() == ::MENU_GROUP())
+                    {
+                    $lastFileOrGroup = $entry;
+                    push @groupStack, $entry;
+                    };
+
+                $index++;
+                };
+
+            $groupEntry->SetFlags($groupEntry->Flags() | $order);
+            };
+
+
+        # Find any subgroups in the remaining entries.
+
+        while ($index < scalar @{$groupEntry->GroupContent()})
+            {
+            my $entry = $groupEntry->GroupContent()->[$index];
+
+            if ($entry->Type() == ::MENU_GROUP())
+                {  push @groupStack, $entry;  };
+
+            $index++;
+            };
+        };
+    };
+
+
+#
+#   Function: GenerateAutoFileTitles
+#
+#   Creates titles for the unlocked file entries in all groups that have the <MENU_GROUP_UPDATETITLES> flag set.  It clears the
+#   flag afterwards so it can be used efficiently for multiple sweeps.
+#
+#   Parameters:
+#
+#       forceAll - If set, forces all the unlocked file titles to update regardless of whether the group has the
+#                     <MENU_GROUP_UPDATETITLES> flag set.
+#
+sub GenerateAutoFileTitles #(forceAll)
+    {
+    my ($self, $forceAll) = @_;
+
+    my @groupStack = ( $menu );
+
+    while (scalar @groupStack)
+        {
+        my $groupEntry = pop @groupStack;
+
+        if ($forceAll || ($groupEntry->Flags() & ::MENU_GROUP_UPDATETITLES()) )
+            {
+            # Find common prefixes and paths to strip from the default menu titles.
+
+            my @sharedDirectories = $self->SharedDirectoriesOf($groupEntry);
+            my $noSharedDirectories = (scalar @sharedDirectories == 0);
+
+            my @sharedPrefixes;
+            my $noSharedPrefixes;
+
+            foreach my $entry (@{$groupEntry->GroupContent()})
+                {
+                if ($entry->Type() == ::MENU_FILE())
+                    {
+                    # Find the common prefixes among all file entries that are unlocked and don't use the file name as their default title.
+
+                    my $defaultTitle = NaturalDocs::Project->DefaultMenuTitleOf($entry->Target());
+
+                    if (!$noSharedPrefixes && ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0 &&
+                        $defaultTitle ne $entry->Target())
+                        {
+                        # If the filename is part of the title, separate it off so no part of it gets included as a common prefix.  This would
+                        # happen if there's a group with only one file in it (Project.h => h) or only files that differ by extension
+                        # (Project.h, Project.cpp => h, cpp) and people labeled them manually (// File: Project.h).
+                        my $filename = (NaturalDocs::File->SplitPath($entry->Target()))[2];
+                        my $filenamePart;
+
+                        if ( length $defaultTitle >= length $filename &&
+                             lc(substr($defaultTitle, 0 - length($filename))) eq lc($filename) )
+                            {
+                            $filenamePart = substr($defaultTitle, 0 - length($filename));
+                            $defaultTitle = substr($defaultTitle, 0, 0 - length($filename));
+                            };
+
+
+                        my @entryPrefixes = split(/(\.|::|->)/, $defaultTitle);
+
+                        # Remove potential leading undef/empty string.
+                        if (!length $entryPrefixes[0])
+                            {  shift @entryPrefixes;  };
+
+                        # Remove last entry.  Something has to exist for the title.  If we already separated off the filename, that will be
+                        # it instead.
+                        if (!$filenamePart)
+                            {  pop @entryPrefixes;  };
+
+                        if (!scalar @entryPrefixes)
+                            {  $noSharedPrefixes = 1;  }
+                        elsif (!scalar @sharedPrefixes)
+                            {  @sharedPrefixes = @entryPrefixes;  }
+                        elsif ($entryPrefixes[0] ne $sharedPrefixes[0])
+                            {  $noSharedPrefixes = 1;  }
+
+                        # If both arrays have entries, and the first is shared...
+                        else
+                            {
+                            my $index = 1;
+
+                            while ($index < scalar @sharedPrefixes && $entryPrefixes[$index] eq $sharedPrefixes[$index])
+                                {  $index++;  };
+
+                            if ($index < scalar @sharedPrefixes)
+                                {  splice(@sharedPrefixes, $index);  };
+                            };
+                        };
+
+                    };  # if entry is MENU_FILE
+                };  # foreach entry in group content.
+
+
+            if (!scalar @sharedPrefixes)
+                {  $noSharedPrefixes = 1;  };
+
+
+            # Update all the menu titles of unlocked file entries.
+
+            foreach my $entry (@{$groupEntry->GroupContent()})
+                {
+                if ($entry->Type() == ::MENU_FILE() && ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0)
+                    {
+                    my $title = NaturalDocs::Project->DefaultMenuTitleOf($entry->Target());
+
+                    if ($title eq $entry->Target())
+                        {
+                        my ($volume, $directoryString, $file) = NaturalDocs::File->SplitPath($entry->Target());
+                        my @directories = NaturalDocs::File->SplitDirectories($directoryString);
+
+                        if (!$noSharedDirectories)
+                            {  splice(@directories, 0, scalar @sharedDirectories);  };
+
+                        # directory\...\directory\file.ext
+
+                        if (scalar @directories > 2)
+                            {  @directories = ( $directories[0], '...', $directories[-1] );  };
+
+                        $directoryString = NaturalDocs::File->JoinDirectories(@directories);
+                        $title = NaturalDocs::File->JoinPaths($directoryString, $file);
+                        }
+
+                    else
+                        {
+                        my $filename = (NaturalDocs::File->SplitPath($entry->Target()))[2];
+                        my $filenamePart;
+
+                        if ( length $title >= length $filename &&
+                             lc(substr($title, 0 - length($filename))) eq lc($filename) )
+                            {
+                            $filenamePart = substr($title, 0 - length($filename));
+                            $title = substr($title, 0, 0 - length($filename));
+                            };
+
+                        my @segments = split(/(::|\.|->)/, $title);
+                        if (!length $segments[0])
+                            {  shift @segments;  };
+
+                        if ($filenamePart)
+                            {  push @segments, $filenamePart;  };
+
+                        if (!$noSharedPrefixes)
+                            {  splice(@segments, 0, scalar @sharedPrefixes);  };
+
+                        # package...package::target
+
+                        if (scalar @segments > 5)
+                            {  splice(@segments, 1, scalar @segments - 4, '...');  };
+
+                        $title = join('', @segments);
+                        };
+
+                    $entry->SetTitle($title);
+                    };  # If entry is an unlocked file
+                };  # Foreach entry
+
+            $groupEntry->SetFlags( $groupEntry->Flags() & ~::MENU_GROUP_UPDATETITLES() );
+
+            };  # If updating group titles
+
+        # Now find any subgroups.
+        foreach my $entry (@{$groupEntry->GroupContent()})
+            {
+            if ($entry->Type() == ::MENU_GROUP())
+                {  push @groupStack, $entry;  };
+            };
+        };
+
+    };
+
+
+#
+#   Function: ResortGroups
+#
+#   Resorts all groups that have <MENU_GROUP_UPDATEORDER> set.  Assumes <DetectOrder()> and <GenerateAutoFileTitles()>
+#   have already been called.  Will clear the flag and any <MENU_ENDOFORIGINAL> entries on reordered groups.
+#
+#   Parameters:
+#
+#       forceAll - If set, resorts all groups regardless of whether <MENU_GROUP_UPDATEORDER> is set.
+#
+sub ResortGroups #(forceAll)
+    {
+    my ($self, $forceAll) = @_;
+    my @groupStack = ( $menu );
+
+    while (scalar @groupStack)
+        {
+        my $groupEntry = pop @groupStack;
+
+        if ($forceAll || ($groupEntry->Flags() & ::MENU_GROUP_UPDATEORDER()) )
+            {
+            my $newEntriesIndex;
+
+
+            # Strip the ENDOFORIGINAL.
+
+            if ($groupEntry->Flags() & ::MENU_GROUP_HASENDOFORIGINAL())
+                {
+                $newEntriesIndex = 0;
+
+                while ($newEntriesIndex < scalar @{$groupEntry->GroupContent()} &&
+                         $groupEntry->GroupContent()->[$newEntriesIndex]->Type() != ::MENU_ENDOFORIGINAL() )
+                    {  $newEntriesIndex++;  };
+
+                $groupEntry->DeleteFromGroup($newEntriesIndex);
+
+                $groupEntry->SetFlags( $groupEntry->Flags() & ~::MENU_GROUP_HASENDOFORIGINAL() );
+                }
+            else
+                {  $newEntriesIndex = -1;  };
+
+
+            # Strip the exceptions.
+
+            my $trailingIndexGroup;
+            my $leadingGeneralIndex;
+
+            if ( ($groupEntry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) &&
+                 $groupEntry->GroupContent()->[0]->Type() == ::MENU_INDEX() &&
+                 $groupEntry->GroupContent()->[0]->Target() eq ::TOPIC_GENERAL() )
+                {
+                $leadingGeneralIndex = shift @{$groupEntry->GroupContent()};
+                if ($newEntriesIndex != -1)
+                    {  $newEntriesIndex--;  };
+                }
+
+            elsif (scalar @{$groupEntry->GroupContent()} && $newEntriesIndex != 0)
+                {
+                my $lastIndex;
+
+                if ($newEntriesIndex != -1)
+                    {  $lastIndex = $newEntriesIndex - 1;  }
+                else
+                    {  $lastIndex = scalar @{$groupEntry->GroupContent()} - 1;  };
+
+                if ($groupEntry->GroupContent()->[$lastIndex]->Type() == ::MENU_GROUP() &&
+                    ( $groupEntry->GroupContent()->[$lastIndex]->Flags() & ::MENU_GROUP_ISINDEXGROUP() ) )
+                    {
+                    $trailingIndexGroup = $groupEntry->GroupContent()->[$lastIndex];
+                    $groupEntry->DeleteFromGroup($lastIndex);
+
+                    if ($newEntriesIndex != -1)
+                        {  $newEntriesIndex++;  };
+                    };
+                };
+
+
+            # If there weren't already exceptions, strip them from the new entries.
+
+            if ( (!defined $trailingIndexGroup || !defined $leadingGeneralIndex) && $newEntriesIndex != -1)
+                {
+                my $index = $newEntriesIndex;
+
+                while ($index < scalar @{$groupEntry->GroupContent()})
+                    {
+                    my $entry = $groupEntry->GroupContent()->[$index];
+
+                    if (!defined $trailingIndexGroup &&
+                        $entry->Type() == ::MENU_GROUP() && ($entry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) )
+                        {
+                        $trailingIndexGroup = $entry;
+                        $groupEntry->DeleteFromGroup($index);
+                        }
+                    elsif (!defined $leadingGeneralIndex && ($groupEntry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) &&
+                            $entry->Type() == ::MENU_INDEX() && !defined $entry->Target())
+                        {
+                        $leadingGeneralIndex = $entry;
+                        $groupEntry->DeleteFromGroup($index);
+                        }
+                    else
+                        {  $index++;  };
+                    };
+                };
+
+
+            # If there's no order, we still want to sort the new additions.
+
+            if ($groupEntry->Flags() & ::MENU_GROUP_UNSORTED())
+                {
+                if ($newEntriesIndex != -1)
+                    {
+                    my @newEntries =
+                        @{$groupEntry->GroupContent()}[$newEntriesIndex..scalar @{$groupEntry->GroupContent()} - 1];
+
+                    @newEntries = sort { $self->CompareEntries($a, $b) } @newEntries;
+
+                    foreach my $newEntry (@newEntries)
+                        {
+                        $groupEntry->GroupContent()->[$newEntriesIndex] = $newEntry;
+                        $newEntriesIndex++;
+                        };
+                    };
+                }
+
+            elsif ($groupEntry->Flags() & ::MENU_GROUP_EVERYTHINGSORTED())
+                {
+                @{$groupEntry->GroupContent()} = sort { $self->CompareEntries($a, $b) } @{$groupEntry->GroupContent()};
+                }
+
+            elsif ( ($groupEntry->Flags() & ::MENU_GROUP_FILESSORTED()) ||
+                     ($groupEntry->Flags() & ::MENU_GROUP_FILESANDGROUPSSORTED()) )
+                {
+                my $groupContent = $groupEntry->GroupContent();
+                my @newEntries;
+
+                if ($newEntriesIndex != -1)
+                    {  @newEntries = splice( @$groupContent, $newEntriesIndex );  };
+
+
+                # First resort the existing entries.
+
+                # A couple of support functions.  They're defined here instead of spun off into their own functions because they're only
+                # used here and to make them general we would need to add support for the other sort options.
+
+                sub IsIncludedInSort #(groupEntry, entry)
+                    {
+                    my ($self, $groupEntry, $entry) = @_;
+
+                    return ($entry->Type() == ::MENU_FILE() ||
+                                ( $entry->Type() == ::MENU_GROUP() &&
+                                    ($groupEntry->Flags() & ::MENU_GROUP_FILESANDGROUPSSORTED()) ) );
+                    };
+
+                sub IsSorted #(groupEntry)
+                    {
+                    my ($self, $groupEntry) = @_;
+                    my $lastApplicable;
+
+                    foreach my $entry (@{$groupEntry->GroupContent()})
+                        {
+                        # If the entry is applicable to the sort order...
+                        if ($self->IsIncludedInSort($groupEntry, $entry))
+                            {
+                            if (defined $lastApplicable)
+                                {
+                                if ($self->CompareEntries($entry, $lastApplicable) < 0)
+                                    {  return undef;  };
+                                };
+
+                            $lastApplicable = $entry;
+                            };
+                        };
+
+                    return 1;
+                    };
+
+
+                # There's a good chance it's still sorted.  They should only become unsorted if an auto-title changes.
+                if (!$self->IsSorted($groupEntry))
+                    {
+                    # Crap.  Okay, method one is to sort each group of continuous sortable elements.  There's a possibility that doing
+                    # this will cause the whole to become sorted again.  We try this first, even though it isn't guaranteed to succeed,
+                    # because it will restore the sort without moving any unsortable entries.
+
+                    # Copy it because we'll need the original if this fails.
+                    my @originalGroupContent = @$groupContent;
+
+                    my $index = 0;
+                    my $startSortable = 0;
+
+                    while (1)
+                        {
+                        # If index is on an unsortable entry or the end of the array...
+                        if ($index == scalar @$groupContent || !$self->IsIncludedInSort($groupEntry, $groupContent->[$index]))
+                            {
+                            # If we have at least two sortable entries...
+                            if ($index - $startSortable >= 2)
+                                {
+                                # Sort them.
+                                my @sortableEntries = @{$groupContent}[$startSortable .. $index - 1];
+                                @sortableEntries = sort { $self->CompareEntries($a, $b) } @sortableEntries;
+                                foreach my $sortableEntry (@sortableEntries)
+                                    {
+                                    $groupContent->[$startSortable] = $sortableEntry;
+                                    $startSortable++;
+                                    };
+                                };
+
+                            if ($index == scalar @$groupContent)
+                                {  last;  };
+
+                            $startSortable = $index + 1;
+                            };
+
+                        $index++;
+                        };
+
+                    if (!$self->IsSorted($groupEntry))
+                        {
+                        # Crap crap.  Okay, now we do a full sort but with potential damage to the original structure.  Each unsortable
+                        # element is locked to the next sortable element.  We sort the sortable elements, bringing all the unsortable
+                        # pieces with them.
+
+                        my @pieces = ( [ ] );
+                        my $currentPiece = $pieces[0];
+
+                        foreach my $entry (@originalGroupContent)
+                            {
+                            push @$currentPiece, $entry;
+
+                            # If the entry is sortable...
+                            if ($self->IsIncludedInSort($groupEntry, $entry))
+                                {
+                                $currentPiece = [ ];
+                                push @pieces, $currentPiece;
+                                };
+                            };
+
+                        my $lastUnsortablePiece;
+
+                        # If the last entry was sortable, we'll have an empty piece at the end.  Drop it.
+                        if (scalar @{$pieces[-1]} == 0)
+                            {  pop @pieces;  }
+
+                        # If the last entry wasn't sortable, the last piece won't end with a sortable element.  Save it, but remove it
+                        # from the list.
+                        else
+                            {  $lastUnsortablePiece = pop @pieces;  };
+
+                        # Sort the list.
+                        @pieces = sort { $self->CompareEntries( $a->[-1], $b->[-1] ) } @pieces;
+
+                        # Copy it back to the original.
+                        if (defined $lastUnsortablePiece)
+                            {  push @pieces, $lastUnsortablePiece;  };
+
+                        my $index = 0;
+
+                        foreach my $piece (@pieces)
+                            {
+                            foreach my $entry (@{$piece})
+                                {
+                                $groupEntry->GroupContent()->[$index] = $entry;
+                                $index++;
+                                };
+                            };
+                        };
+                    };
+
+
+                # Okay, the orginal entries are sorted now.  Sort the new entries and apply.
+
+                if (scalar @newEntries)
+                    {
+                    @newEntries = sort { $self->CompareEntries($a, $b) } @newEntries;
+                    my @originalEntries = @$groupContent;
+                    @$groupContent = ( );
+
+                    while (1)
+                        {
+                        while (scalar @originalEntries && !$self->IsIncludedInSort($groupEntry, $originalEntries[0]))
+                            {  push @$groupContent, (shift @originalEntries);  };
+
+                        if (!scalar @originalEntries || !scalar @newEntries)
+                            {  last;  };
+
+                        while (scalar @newEntries && $self->CompareEntries($newEntries[0], $originalEntries[0]) < 0)
+                            {  push @$groupContent, (shift @newEntries);  };
+
+                        push @$groupContent, (shift @originalEntries);
+
+                        if (!scalar @originalEntries || !scalar @newEntries)
+                            {  last;  };
+                        };
+
+                    if (scalar @originalEntries)
+                        {  push @$groupContent, @originalEntries;  }
+                    elsif (scalar @newEntries)
+                        {  push @$groupContent, @newEntries;  };
+                    };
+                };
+
+
+            # Now re-add the exceptions.
+
+            if (defined $leadingGeneralIndex)
+                {
+                unshift @{$groupEntry->GroupContent()}, $leadingGeneralIndex;
+                };
+
+            if (defined $trailingIndexGroup)
+                {
+                $groupEntry->PushToGroup($trailingIndexGroup);
+                };
+
+            };
+
+        foreach my $entry (@{$groupEntry->GroupContent()})
+            {
+            if ($entry->Type() == ::MENU_GROUP())
+                {  push @groupStack, $entry;  };
+            };
+        };
+    };
+
+
+#
+#   Function: CompareEntries
+#
+#   A comparison function for use in sorting.  Compares the two entries by their titles with <StringCompare()>, but in the case
+#   of a tie, puts <MENU_FILE> entries above <MENU_GROUP> entries.
+#
+sub CompareEntries #(a, b)
+    {
+    my ($self, $a, $b) = @_;
+
+    my $result = ::StringCompare($a->Title(), $b->Title());
+
+    if ($result == 0)
+        {
+        if ($a->Type() == ::MENU_FILE() && $b->Type() == ::MENU_GROUP())
+            {  $result = -1;  }
+        elsif ($a->Type() == ::MENU_GROUP() && $b->Type() == ::MENU_FILE())
+            {  $result = 1;  };
+        };
+
+    return $result;
+    };
+
+
+#
+#   Function: SharedDirectoriesOf
+#
+#   Returns an array of all the directories shared by the files in the group.  If none, returns an empty array.
+#
+sub SharedDirectoriesOf #(group)
+    {
+    my ($self, $groupEntry) = @_;
+    my @sharedDirectories;
+
+    foreach my $entry (@{$groupEntry->GroupContent()})
+        {
+        if ($entry->Type() == ::MENU_FILE())
+            {
+            my @entryDirectories = NaturalDocs::File->SplitDirectories( (NaturalDocs::File->SplitPath($entry->Target()))[1] );
+
+            if (!scalar @sharedDirectories)
+                {  @sharedDirectories = @entryDirectories;  }
+            else
+                {  ::ShortenToMatchStrings(\@sharedDirectories, \@entryDirectories);  };
+
+            if (!scalar @sharedDirectories)
+                {  last;  };
+            };
+        };
+
+    return @sharedDirectories;
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Menu/Entry.pm b/docs/tool/Modules/NaturalDocs/Menu/Entry.pm
new file mode 100644
index 00000000..58989cca
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Menu/Entry.pm
@@ -0,0 +1,201 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Menu::Entry
+#
+###############################################################################
+#
+#   A class representing an entry in the menu.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Menu::Entry;
+
+
+###############################################################################
+# Group: Implementation
+
+#
+#   Constants: Members
+#
+#   The object is implemented as a blessed arrayref with the indexes below.
+#
+#       TYPE      - The <MenuEntryType>
+#       TITLE     - The title of the entry.
+#       TARGET  - The target of the entry.  If the type is <MENU_FILE>, it will be the source <FileName>.  If the type is
+#                       <MENU_LINK>, it will be the URL.  If the type is <MENU_GROUP>, it will be an arrayref of
+#                       <NaturalDocs::Menu::Entry> objects representing the group's content.  If the type is <MENU_INDEX>, it will be
+#                       a <TopicType>.
+#       FLAGS    - Any <Menu Entry Flags> that apply.
+#
+use constant TYPE => 0;
+use constant TITLE => 1;
+use constant TARGET => 2;
+use constant FLAGS => 3;
+# DEPENDENCY: New() depends on the order of these constants.
+
+
+###############################################################################
+# Group: Functions
+
+#
+#   Function: New
+#
+#   Creates and returns a new object.
+#
+#   Parameters:
+#
+#       type     - The <MenuEntryType>.
+#       title      - The title of the entry.
+#       target   - The target of the entry, if applicable.  If the type is <MENU_FILE>, use the source <FileName>.  If the type is
+#                     <MENU_LINK>, use the URL.  If the type is <MENU_INDEX>, use the <TopicType>.  Otherwise set it to undef.
+#       flags     - Any <Menu Entry Flags> that apply.
+#
+sub New #(type, title, target, flags)
+    {
+    # DEPENDENCY: This gode depends on the order of the constants.
+
+    my $package = shift;
+
+    my $object = [ @_ ];
+    bless $object, $package;
+
+    if ($object->[TYPE] == ::MENU_GROUP())
+        {  $object->[TARGET] = [ ];  };
+    if (!defined $object->[FLAGS])
+        {  $object->[FLAGS] = 0;  };
+
+    return $object;
+    };
+
+
+#   Function: Type
+#   Returns the <MenuEntryType>.
+sub Type
+    {  return $_[0]->[TYPE];  };
+
+#   Function: Title
+#   Returns the title of the entry.
+sub Title
+    {  return $_[0]->[TITLE];  };
+
+# Function: SetTitle
+# Replaces the entry's title.
+sub SetTitle #(title)
+    {  $_[0]->[TITLE] = $_[1];  };
+
+#
+#   Function: Target
+#
+#   Returns the target of the entry, if applicable.  If the type is <MENU_FILE>, it returns the source <FileName>.  If the type is
+#   <MENU_LINK>, it returns the URL.  If the type is <MENU_INDEX>, it returns the <TopicType>.  Otherwise it returns undef.
+#
+sub Target
+    {
+    my $self = shift;
+
+    # Group entries are the only time when target won't be undef when it should be.
+    if ($self->Type() == ::MENU_GROUP())
+        {  return undef;  }
+    else
+        {  return $self->[TARGET];  };
+    };
+
+# Function: SetTarget
+# Replaces the entry's target.
+sub SetTarget #(target)
+    {  $_[0]->[TARGET] = $_[1];  };
+
+#   Function: Flags
+#   Returns the <Menu Entry Flags>.
+sub Flags
+    {  return $_[0]->[FLAGS];  };
+
+# Function: SetFlags
+# Replaces the <Menu Entry Flags>.
+sub SetFlags #(flags)
+    {  $_[0]->[FLAGS] = $_[1];  };
+
+
+
+###############################################################################
+# Group: Group Functions
+#
+#   All of these functions assume the type is <MENU_GROUP>.  Do *not* call any of these without checking <Type()> first.
+
+
+#
+#   Function: GroupContent
+#
+#   Returns an arrayref of <NaturalDocs::Menu::Entry> objects representing the contents of the
+#   group, or undef otherwise.  This arrayref will always exist for <MENU_GROUP>'s and can be changed.
+#
+sub GroupContent
+    {
+    return $_[0]->[TARGET];
+    };
+
+
+#
+#   Function: GroupIsEmpty
+#
+#   If the type is <MENU_GROUP>, returns whether the group is empty.
+#
+sub GroupIsEmpty
+    {
+    my $self = shift;
+    return (scalar @{$self->GroupContent()} > 0);
+    };
+
+
+#
+#   Function: PushToGroup
+#
+#   Pushes the entry to the end of the group content.
+#
+sub PushToGroup #(entry)
+    {
+    my ($self, $entry) = @_;
+    push @{$self->GroupContent()}, $entry;
+    };
+
+
+#
+#   Function: DeleteFromGroup
+#
+#   Deletes an entry from the group content by index.
+#
+sub DeleteFromGroup #(index)
+    {
+    my ($self, $index) = @_;
+
+    my $groupContent = $self->GroupContent();
+
+    splice( @$groupContent, $index, 1 );
+    };
+
+
+#
+#   Function: MarkEndOfOriginal
+#
+#   If the group doesn't already have one, adds a <MENU_ENDOFORIGINAL> entry to the end and sets the
+#   <MENU_GROUP_HASENDOFORIGINAL> flag.
+#
+sub MarkEndOfOriginal
+    {
+    my $self = shift;
+
+    if (($self->Flags() & ::MENU_GROUP_HASENDOFORIGINAL()) == 0)
+        {
+        $self->PushToGroup( NaturalDocs::Menu::Entry->New(::MENU_ENDOFORIGINAL(), undef, undef, undef) );
+        $self->SetFlags( $self->Flags() | ::MENU_GROUP_HASENDOFORIGINAL() );
+        };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/NDMarkup.pm b/docs/tool/Modules/NaturalDocs/NDMarkup.pm
new file mode 100644
index 00000000..d394d2c7
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/NDMarkup.pm
@@ -0,0 +1,76 @@
+###############################################################################
+#
+#   Package: NaturalDocs::NDMarkup
+#
+###############################################################################
+#
+#   A package of support functions for dealing with <NDMarkup>.
+#
+#   Usage and Dependencies:
+#
+#       The package doesn't depend on any Natural Docs packages and is ready to use right away.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+
+use strict;
+use integer;
+
+package NaturalDocs::NDMarkup;
+
+#
+#   Function: ConvertAmpChars
+#
+#   Substitutes certain characters with their <NDMarkup> amp chars.
+#
+#   Parameters:
+#
+#       text - The block of text to convert.
+#
+#   Returns:
+#
+#       The converted text block.
+#
+sub ConvertAmpChars #(text)
+    {
+    my ($self, $text) = @_;
+
+    $text =~ s/&/&amp;/g;
+    $text =~ s/</&lt;/g;
+    $text =~ s/>/&gt;/g;
+    $text =~ s/\"/&quot;/g;
+
+    return $text;
+    };
+
+
+#
+#   Function: RestoreAmpChars
+#
+#   Replaces <NDMarkup> amp chars with their original symbols.
+#
+#   Parameters:
+#
+#       text - The text to restore.
+#
+#   Returns:
+#
+#       The restored text.
+#
+sub RestoreAmpChars #(text)
+    {
+    my ($self, $text) = @_;
+
+    $text =~ s/&quot;/\"/g;
+    $text =~ s/&gt;/>/g;
+    $text =~ s/&lt;/</g;
+    $text =~ s/&amp;/&/g;
+
+    return $text;
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Parser.pm b/docs/tool/Modules/NaturalDocs/Parser.pm
new file mode 100644
index 00000000..e88cd289
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Parser.pm
@@ -0,0 +1,1331 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Parser
+#
+###############################################################################
+#
+#   A package that coordinates source file parsing between the <NaturalDocs::Languages::Base>-derived objects and its own
+#   sub-packages such as <NaturalDocs::Parser::Native>.  Also handles sending symbols to <NaturalDocs::SymbolTable> and
+#   other generic topic processing.
+#
+#   Usage and Dependencies:
+#
+#       - Prior to use, <NaturalDocs::Settings>, <NaturalDocs::Languages>, <NaturalDocs::Project>, <NaturalDocs::SymbolTable>,
+#         and <NaturalDocs::ClassHierarchy> must be initialized.  <NaturalDocs::SymbolTable> and <NaturalDocs::ClassHierarchy>
+#         do not have to be fully resolved.
+#
+#       - Aside from that, the package is ready to use right away.  It does not have its own initialization function.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use NaturalDocs::Parser::ParsedTopic;
+use NaturalDocs::Parser::Native;
+use NaturalDocs::Parser::JavaDoc;
+
+use strict;
+use integer;
+
+package NaturalDocs::Parser;
+
+
+
+###############################################################################
+# Group: Variables
+
+
+#
+#   var: sourceFile
+#
+#   The source <FileName> currently being parsed.
+#
+my $sourceFile;
+
+#
+#   var: language
+#
+#   The language object for the file, derived from <NaturalDocs::Languages::Base>.
+#
+my $language;
+
+#
+#   Array: parsedFile
+#
+#   An array of <NaturalDocs::Parser::ParsedTopic> objects.
+#
+my @parsedFile;
+
+
+#
+#   bool: parsingForInformation
+#   Whether <ParseForInformation()> was called.  If false, then <ParseForBuild()> was called.
+#
+my $parsingForInformation;
+
+
+
+###############################################################################
+# Group: Functions
+
+#
+#   Function: ParseForInformation
+#
+#   Parses the input file for information.  Will update the information about the file in <NaturalDocs::SymbolTable> and
+#   <NaturalDocs::Project>.
+#
+#   Parameters:
+#
+#       file - The <FileName> to parse.
+#
+sub ParseForInformation #(file)
+    {
+    my ($self, $file) = @_;
+    $sourceFile = $file;
+
+    $parsingForInformation = 1;
+
+    # Watch this parse so we detect any changes.
+    NaturalDocs::SymbolTable->WatchFileForChanges($sourceFile);
+    NaturalDocs::ClassHierarchy->WatchFileForChanges($sourceFile);
+    NaturalDocs::SourceDB->WatchFileForChanges($sourceFile);
+
+    my $defaultMenuTitle = $self->Parse();
+
+    foreach my $topic (@parsedFile)
+        {
+        # Add a symbol for the topic.
+
+        my $type = $topic->Type();
+        if ($type eq ::TOPIC_ENUMERATION())
+            {  $type = ::TOPIC_TYPE();  };
+
+        NaturalDocs::SymbolTable->AddSymbol($topic->Symbol(), $sourceFile, $type,
+                                                                   $topic->Prototype(), $topic->Summary());
+
+
+        # You can't put the function call directly in a while with a regex.  It has to sit in a variable to work.
+        my $body = $topic->Body();
+
+
+        # If it's a list or enum topic, add a symbol for each description list entry.
+
+        if ($topic->IsList() || $topic->Type() eq ::TOPIC_ENUMERATION())
+            {
+            # We'll hijack the enum constants to apply to non-enum behavior too.
+            my $behavior;
+
+            if ($topic->Type() eq ::TOPIC_ENUMERATION())
+                {
+                $type = ::TOPIC_CONSTANT();
+                $behavior = $language->EnumValues();
+                }
+            elsif (NaturalDocs::Topics->TypeInfo($topic->Type())->Scope() == ::SCOPE_ALWAYS_GLOBAL())
+                {
+                $behavior = ::ENUM_GLOBAL();
+                }
+            else
+                {
+                $behavior = ::ENUM_UNDER_PARENT();
+                };
+
+            while ($body =~ /<ds>([^<]+)<\/ds><dd>(.*?)<\/dd>/g)
+                {
+                my ($listTextSymbol, $listSummary) = ($1, $2);
+
+                $listTextSymbol = NaturalDocs::NDMarkup->RestoreAmpChars($listTextSymbol);
+                my $listSymbol = NaturalDocs::SymbolString->FromText($listTextSymbol);
+
+                if ($behavior == ::ENUM_UNDER_PARENT())
+                    {  $listSymbol = NaturalDocs::SymbolString->Join($topic->Package(), $listSymbol);  }
+                elsif ($behavior == ::ENUM_UNDER_TYPE())
+                    {  $listSymbol = NaturalDocs::SymbolString->Join($topic->Symbol(), $listSymbol);  };
+
+                NaturalDocs::SymbolTable->AddSymbol($listSymbol, $sourceFile, $type, undef,
+                                                                           $self->GetSummaryFromDescriptionList($listSummary));
+                };
+            };
+
+
+        # Add references in the topic.
+
+        while ($body =~ /<link target=\"([^\"]*)\" name=\"[^\"]*\" original=\"[^\"]*\">/g)
+            {
+            my $linkText = NaturalDocs::NDMarkup->RestoreAmpChars($1);
+            my $linkSymbol = NaturalDocs::SymbolString->FromText($linkText);
+
+            NaturalDocs::SymbolTable->AddReference(::REFERENCE_TEXT(), $linkSymbol,
+                                                                           $topic->Package(), $topic->Using(), $sourceFile);
+            };
+
+
+        # Add images in the topic.
+
+        while ($body =~ /<img mode=\"[^\"]*\" target=\"([^\"]+)\" original=\"[^\"]*\">/g)
+            {
+            my $target = NaturalDocs::NDMarkup->RestoreAmpChars($1);
+            NaturalDocs::ImageReferenceTable->AddReference($sourceFile, $target);
+            };
+        };
+
+    # Handle any changes to the file.
+    NaturalDocs::ClassHierarchy->AnalyzeChanges();
+    NaturalDocs::SymbolTable->AnalyzeChanges();
+    NaturalDocs::SourceDB->AnalyzeWatchedFileChanges();
+
+    # Update project on the file's characteristics.
+    my $hasContent = (scalar @parsedFile > 0);
+
+    NaturalDocs::Project->SetHasContent($sourceFile, $hasContent);
+    if ($hasContent)
+        {  NaturalDocs::Project->SetDefaultMenuTitle($sourceFile, $defaultMenuTitle);  };
+
+    # We don't need to keep this around.
+    @parsedFile = ( );
+    };
+
+
+#
+#   Function: ParseForBuild
+#
+#   Parses the input file for building, returning it as a <NaturalDocs::Parser::ParsedTopic> arrayref.
+#
+#   Note that all new and changed files should be parsed for symbols via <ParseForInformation()> before calling this function on
+#   *any* file.  The reason is that <NaturalDocs::SymbolTable> needs to know about all the symbol definitions and references to
+#   resolve them properly.
+#
+#   Parameters:
+#
+#       file - The <FileName> to parse for building.
+#
+#   Returns:
+#
+#       An arrayref of the source file as <NaturalDocs::Parser::ParsedTopic> objects.
+#
+sub ParseForBuild #(file)
+    {
+    my ($self, $file) = @_;
+    $sourceFile = $file;
+
+    $parsingForInformation = undef;
+
+    $self->Parse();
+
+    return \@parsedFile;
+    };
+
+
+
+
+###############################################################################
+# Group: Interface Functions
+
+
+#
+#   Function: OnComment
+#
+#   The function called by <NaturalDocs::Languages::Base>-derived objects when their parsers encounter a comment
+#   suitable for documentation.
+#
+#   Parameters:
+#
+#       commentLines - An arrayref of the comment's lines.  The language's comment symbols should be converted to spaces,
+#                               and there should be no line break characters at the end of each line.  *The original memory will be
+#                               changed.*
+#       lineNumber - The line number of the first of the comment lines.
+#       isJavaDoc - Whether the comment is in JavaDoc format.
+#
+#   Returns:
+#
+#       The number of topics created by this comment, or zero if none.
+#
+sub OnComment #(string[] commentLines, int lineNumber, bool isJavaDoc)
+    {
+    my ($self, $commentLines, $lineNumber, $isJavaDoc) = @_;
+
+    $self->CleanComment($commentLines);
+
+    # We check if it's definitely Natural Docs content first.  This overrides all else, since it's possible that a comment could start
+    # with a topic line yet have something that looks like a JavaDoc tag.  Natural Docs wins in this case.
+    if (NaturalDocs::Parser::Native->IsMine($commentLines, $isJavaDoc))
+        {  return NaturalDocs::Parser::Native->ParseComment($commentLines, $isJavaDoc, $lineNumber, \@parsedFile);  }
+
+    elsif (NaturalDocs::Parser::JavaDoc->IsMine($commentLines, $isJavaDoc))
+        {  return NaturalDocs::Parser::JavaDoc->ParseComment($commentLines, $isJavaDoc, $lineNumber, \@parsedFile);  }
+
+    # If the content is ambiguous and it's a JavaDoc-styled comment, treat it as Natural Docs content.
+    elsif ($isJavaDoc)
+        {  return NaturalDocs::Parser::Native->ParseComment($commentLines, $isJavaDoc, $lineNumber, \@parsedFile);  }
+    };
+
+
+#
+#   Function: OnClass
+#
+#   A function called by <NaturalDocs::Languages::Base>-derived objects when their parsers encounter a class declaration.
+#
+#   Parameters:
+#
+#       class - The <SymbolString> of the class encountered.
+#
+sub OnClass #(class)
+    {
+    my ($self, $class) = @_;
+
+    if ($parsingForInformation)
+        {  NaturalDocs::ClassHierarchy->AddClass($sourceFile, $class);  };
+    };
+
+
+#
+#   Function: OnClassParent
+#
+#   A function called by <NaturalDocs::Languages::Base>-derived objects when their parsers encounter a declaration of
+#   inheritance.
+#
+#   Parameters:
+#
+#       class - The <SymbolString> of the class we're in.
+#       parent - The <SymbolString> of the class it inherits.
+#       scope - The package <SymbolString> that the reference appeared in.
+#       using - An arrayref of package <SymbolStrings> that the reference has access to via "using" statements.
+#       resolvingFlags - Any <Resolving Flags> to be used when resolving the reference.  <RESOLVE_NOPLURAL> is added
+#                              automatically since that would never apply to source code.
+#
+sub OnClassParent #(class, parent, scope, using, resolvingFlags)
+    {
+    my ($self, $class, $parent, $scope, $using, $resolvingFlags) = @_;
+
+    if ($parsingForInformation)
+        {
+        NaturalDocs::ClassHierarchy->AddParentReference($sourceFile, $class, $parent, $scope, $using,
+                                                                                   $resolvingFlags | ::RESOLVE_NOPLURAL());
+        };
+    };
+
+
+
+###############################################################################
+# Group: Support Functions
+
+
+#   Function: Parse
+#
+#   Opens the source file and parses process.  Most of the actual parsing is done in <NaturalDocs::Languages::Base->ParseFile()>
+#   and <OnComment()>, though.
+#
+#   *Do not call externally.*  Rather, call <ParseForInformation()> or <ParseForBuild()>.
+#
+#   Returns:
+#
+#       The default menu title of the file.  Will be the <FileName> if nothing better is found.
+#
+sub Parse
+    {
+    my ($self) = @_;
+
+    NaturalDocs::Error->OnStartParsing($sourceFile);
+
+    $language = NaturalDocs::Languages->LanguageOf($sourceFile);
+    NaturalDocs::Parser::Native->Start();
+    @parsedFile = ( );
+
+    my ($autoTopics, $scopeRecord) = $language->ParseFile($sourceFile, \@parsedFile);
+
+
+    $self->AddToClassHierarchy();
+
+    $self->BreakLists();
+
+    if (defined $autoTopics)
+        {
+        if (defined $scopeRecord)
+            {  $self->RepairPackages($autoTopics, $scopeRecord);  };
+
+        $self->MergeAutoTopics($language, $autoTopics);
+        };
+
+    $self->RemoveRemainingHeaderlessTopics();
+
+
+    # We don't need to do this if there aren't any auto-topics because the only package changes would be implied by the comments.
+    if (defined $autoTopics)
+        {  $self->AddPackageDelineators();  };
+
+    if (!NaturalDocs::Settings->NoAutoGroup())
+        {  $self->MakeAutoGroups($autoTopics);  };
+
+
+    # Set the menu title.
+
+    my $defaultMenuTitle = $sourceFile;
+
+    if (scalar @parsedFile)
+        {
+        my $addFileTitle;
+
+        if (NaturalDocs::Settings->OnlyFileTitles())
+            {
+            # We still want to use the title from the topics if the first one is a file.
+            if ($parsedFile[0]->Type() eq ::TOPIC_FILE())
+                {  $addFileTitle = 0;  }
+            else
+                {  $addFileTitle = 1;  };
+            }
+        elsif (scalar @parsedFile == 1 || NaturalDocs::Topics->TypeInfo( $parsedFile[0]->Type() )->PageTitleIfFirst())
+            {  $addFileTitle = 0;  }
+        else
+            {  $addFileTitle = 1;  };
+
+        if (!$addFileTitle)
+            {
+            $defaultMenuTitle = $parsedFile[0]->Title();
+            }
+        else
+            {
+            # If the title ended up being the file name, add a leading section for it.
+
+            unshift @parsedFile,
+                       NaturalDocs::Parser::ParsedTopic->New(::TOPIC_FILE(), (NaturalDocs::File->SplitPath($sourceFile))[2],
+                                                                                  undef, undef, undef, undef, undef, 1, undef);
+            };
+        };
+
+    NaturalDocs::Error->OnEndParsing($sourceFile);
+
+    return $defaultMenuTitle;
+    };
+
+
+#
+#   Function: CleanComment
+#
+#   Removes any extraneous formatting and whitespace from the comment.  Eliminates comment boxes, horizontal lines, trailing
+#   whitespace from lines, and expands all tab characters.  It keeps leading whitespace, though, since it may be needed for
+#   example code, and blank lines, since the original line numbers are needed.
+#
+#   Parameters:
+#
+#       commentLines  - An arrayref of the comment lines to clean.  *The original memory will be changed.*  Lines should have the
+#                                language's comment symbols replaced by spaces and not have a trailing line break.
+#
+sub CleanComment #(commentLines)
+    {
+    my ($self, $commentLines) = @_;
+
+    use constant DONT_KNOW => 0;
+    use constant IS_UNIFORM => 1;
+    use constant IS_UNIFORM_IF_AT_END => 2;
+    use constant IS_NOT_UNIFORM => 3;
+
+    my $leftSide = DONT_KNOW;
+    my $rightSide = DONT_KNOW;
+    my $leftSideChar;
+    my $rightSideChar;
+
+    my $index = 0;
+    my $tabLength = NaturalDocs::Settings->TabLength();
+
+    while ($index < scalar @$commentLines)
+        {
+        # Strip trailing whitespace from the original.
+
+        $commentLines->[$index] =~ s/[ \t]+$//;
+
+
+        # Expand tabs in the original.  This method is almost six times faster than Text::Tabs' method.
+
+        my $tabIndex = index($commentLines->[$index], "\t");
+
+        while ($tabIndex != -1)
+            {
+            substr( $commentLines->[$index], $tabIndex, 1, ' ' x ($tabLength - ($tabIndex % $tabLength)) );
+            $tabIndex = index($commentLines->[$index], "\t", $tabIndex);
+            };
+
+
+        # Make a working copy and strip leading whitespace as well.  This has to be done after tabs are expanded because
+        # stripping indentation could change how far tabs are expanded.
+
+        my $line = $commentLines->[$index];
+        $line =~ s/^ +//;
+
+        # If the line is blank...
+        if (!length $line)
+            {
+            # If we have a potential vertical line, this only acceptable if it's at the end of the comment.
+            if ($leftSide == IS_UNIFORM)
+                {  $leftSide = IS_UNIFORM_IF_AT_END;  };
+            if ($rightSide == IS_UNIFORM)
+                {  $rightSide = IS_UNIFORM_IF_AT_END;  };
+            }
+
+        # If there's at least four symbols in a row, it's a horizontal line.  The second regex supports differing edge characters.  It
+        # doesn't matter if any of this matches the left and right side symbols.  The length < 256 is a sanity check, because that
+        # regexp has caused the perl regexp engine to choke on an insane line someone sent me from an automatically generated
+        # file.  It had over 10k characters on the first line, and most of them were 0x00.
+        elsif ($line =~ /^([^a-zA-Z0-9 ])\1{3,}$/ ||
+                (length $line < 256 && $line =~ /^([^a-zA-Z0-9 ])\1*([^a-zA-Z0-9 ])\2{3,}([^a-zA-Z0-9 ])\3*$/) )
+            {
+            # Ignore it.  This has no effect on the vertical line detection.  We want to keep it in the output though in case it was
+            # in a code section.
+            }
+
+        # If the line is not blank or a horizontal line...
+        else
+            {
+            # More content means any previous blank lines are no longer tolerated in vertical line detection.  They are only
+            # acceptable at the end of the comment.
+
+            if ($leftSide == IS_UNIFORM_IF_AT_END)
+                {  $leftSide = IS_NOT_UNIFORM;  };
+            if ($rightSide == IS_UNIFORM_IF_AT_END)
+                {  $rightSide = IS_NOT_UNIFORM;  };
+
+
+            # Detect vertical lines.  Lines are only lines if they are followed by whitespace or a connected horizontal line.
+            # Otherwise we may accidentally detect lines from short comments that just happen to have every first or last
+            # character the same.
+
+            if ($leftSide != IS_NOT_UNIFORM)
+                {
+                if ($line =~ /^([^a-zA-Z0-9])\1*(?: |$)/)
+                    {
+                    if ($leftSide == DONT_KNOW)
+                        {
+                        $leftSide = IS_UNIFORM;
+                        $leftSideChar = $1;
+                        }
+                    else # ($leftSide == IS_UNIFORM)  Other choices already ruled out.
+                        {
+                        if ($leftSideChar ne $1)
+                            {  $leftSide = IS_NOT_UNIFORM;  };
+                        };
+                    }
+                # We'll tolerate the lack of symbols on the left on the first line, because it may be a
+                # /* Function: Whatever
+                #  * Description.
+                #  */
+                # comment which would have the leading /* blanked out.
+                elsif ($index != 0)
+                    {
+                    $leftSide = IS_NOT_UNIFORM;
+                    };
+                };
+
+            if ($rightSide != IS_NOT_UNIFORM)
+                {
+                if ($line =~ / ([^a-zA-Z0-9])\1*$/)
+                    {
+                    if ($rightSide == DONT_KNOW)
+                        {
+                        $rightSide = IS_UNIFORM;
+                        $rightSideChar = $1;
+                        }
+                    else # ($rightSide == IS_UNIFORM)  Other choices already ruled out.
+                        {
+                        if ($rightSideChar ne $1)
+                            {  $rightSide = IS_NOT_UNIFORM;  };
+                        };
+                    }
+                else
+                    {
+                    $rightSide = IS_NOT_UNIFORM;
+                    };
+                };
+
+            # We'll remove vertical lines later if they're uniform throughout the entire comment.
+            };
+
+        $index++;
+        };
+
+
+    if ($leftSide == IS_UNIFORM_IF_AT_END)
+        {  $leftSide = IS_UNIFORM;  };
+    if ($rightSide == IS_UNIFORM_IF_AT_END)
+        {  $rightSide = IS_UNIFORM;  };
+
+
+    $index = 0;
+    my $inCodeSection = 0;
+
+    while ($index < scalar @$commentLines)
+        {
+        # Clear horizontal lines only if we're not in a code section.
+        if ($commentLines->[$index] =~ /^ *([^a-zA-Z0-9 ])\1{3,}$/ ||
+            ( length $commentLines->[$index] < 256 &&
+              $commentLines->[$index] =~ /^ *([^a-zA-Z0-9 ])\1*([^a-zA-Z0-9 ])\2{3,}([^a-zA-Z0-9 ])\3*$/ ) )
+        	{
+        	if (!$inCodeSection)
+        		{  $commentLines->[$index] = '';  }
+        	}
+
+        else
+        	{
+	        # Clear vertical lines.
+
+	        if ($leftSide == IS_UNIFORM)
+	            {
+	            # This works because every line should either start this way, be blank, or be the first line that doesn't start with a
+	            # symbol.
+	            $commentLines->[$index] =~ s/^ *([^a-zA-Z0-9 ])\1*//;
+	            };
+
+	        if ($rightSide == IS_UNIFORM)
+	            {
+	            $commentLines->[$index] =~ s/ *([^a-zA-Z0-9 ])\1*$//;
+	            };
+
+
+	        # Clear horizontal lines again if there were vertical lines.  This catches lines that were separated from the verticals by
+	        # whitespace.
+
+	        if (($leftSide == IS_UNIFORM || $rightSide == IS_UNIFORM) && !$inCodeSection)
+	            {
+	            $commentLines->[$index] =~ s/^ *([^a-zA-Z0-9 ])\1{3,}$//;
+	            $commentLines->[$index] =~ s/^ *([^a-zA-Z0-9 ])\1*([^a-zA-Z0-9 ])\2{3,}([^a-zA-Z0-9 ])\3*$//;
+	            };
+
+
+	        # Check for the start and end of code sections.  Note that this doesn't affect vertical line removal.
+
+	        if (!$inCodeSection &&
+	        	$commentLines->[$index] =~ /^ *\( *(?:(?:start|begin)? +)?(?:table|code|example|diagram) *\)$/i )
+	        	{
+	        	$inCodeSection = 1;
+	        	}
+	        elsif ($inCodeSection &&
+	        	    $commentLines->[$index] =~ /^ *\( *(?:end|finish|done)(?: +(?:table|code|example|diagram))? *\)$/i)
+	        	 {
+	        	 $inCodeSection = 0;
+	        	 }
+	        }
+
+
+        $index++;
+        };
+
+    };
+
+
+
+###############################################################################
+# Group: Processing Functions
+
+
+#
+#   Function: RepairPackages
+#
+#   Recalculates the packages for all comment topics using the auto-topics and the scope record.  Call this *before* calling
+#   <MergeAutoTopics()>.
+#
+#   Parameters:
+#
+#       autoTopics - A reference to the list of automatically generated <NaturalDocs::Parser::ParsedTopics>.
+#       scopeRecord - A reference to an array of <NaturalDocs::Languages::Advanced::ScopeChanges>.
+#
+sub RepairPackages #(autoTopics, scopeRecord)
+    {
+    my ($self, $autoTopics, $scopeRecord) = @_;
+
+    my $topicIndex = 0;
+    my $autoTopicIndex = 0;
+    my $scopeIndex = 0;
+
+    my $topic = $parsedFile[0];
+    my $autoTopic = $autoTopics->[0];
+    my $scopeChange = $scopeRecord->[0];
+
+    my $currentPackage;
+    my $inFakePackage;
+
+    while (defined $topic)
+        {
+        # First update the scope via the record if its defined and has the lowest line number.
+        if (defined $scopeChange &&
+            $scopeChange->LineNumber() <= $topic->LineNumber() &&
+            (!defined $autoTopic || $scopeChange->LineNumber() <= $autoTopic->LineNumber()) )
+            {
+            $currentPackage = $scopeChange->Scope();
+            $scopeIndex++;
+            $scopeChange = $scopeRecord->[$scopeIndex];  # Will be undef when past end.
+            $inFakePackage = undef;
+            }
+
+        # Next try to end a fake scope with an auto topic if its defined and has the lowest line number.
+        elsif (defined $autoTopic &&
+                $autoTopic->LineNumber() <= $topic->LineNumber())
+            {
+            if ($inFakePackage)
+                {
+                $currentPackage = $autoTopic->Package();
+                $inFakePackage = undef;
+                };
+
+            $autoTopicIndex++;
+            $autoTopic = $autoTopics->[$autoTopicIndex];  # Will be undef when past end.
+            }
+
+
+        # Finally try to handle the topic, since it has the lowest line number.  Check for Type() because headerless topics won't have
+        # one.
+        else
+            {
+            my $scope;
+            if ($topic->Type())
+                {  $scope = NaturalDocs::Topics->TypeInfo($topic->Type())->Scope();  }
+            else
+                {  $scope = ::SCOPE_NORMAL();  };
+
+            if ($scope == ::SCOPE_START() || $scope == ::SCOPE_END())
+                {
+                # They should already have the correct class and scope.
+                $currentPackage = $topic->Package();
+                $inFakePackage = 1;
+                }
+           else
+                {
+                # Fix the package of everything else.
+
+                # Note that the first function or variable topic to appear in a fake package will assume that package even if it turns out
+                # to be incorrect in the actual code, since the topic will come before the auto-topic.  This will be corrected in
+                # MergeAutoTopics().
+
+                $topic->SetPackage($currentPackage);
+                };
+
+            $topicIndex++;
+            $topic = $parsedFile[$topicIndex];  # Will be undef when past end.
+            };
+        };
+
+    };
+
+
+#
+#   Function: MergeAutoTopics
+#
+#   Merges the automatically generated topics into the file.  If an auto-topic matches an existing topic, it will have it's prototype
+#   and package transferred.  If it doesn't, the auto-topic will be inserted into the list unless
+#   <NaturalDocs::Settings->DocumentedOnly()> is set.  If an existing topic doesn't have a title, it's assumed to be a headerless
+#   comment and will be merged with the next auto-topic or discarded.
+#
+#   Parameters:
+#
+#       language - The <NaturalDocs::Languages::Base>-derived class for the file.
+#       autoTopics - A reference to the list of automatically generated topics.
+#
+sub MergeAutoTopics #(language, autoTopics)
+    {
+    my ($self, $language, $autoTopics) = @_;
+
+    my $topicIndex = 0;
+    my $autoTopicIndex = 0;
+
+    # Keys are topic types, values are existence hashrefs of titles.
+    my %topicsInLists;
+
+    while ($topicIndex < scalar @parsedFile && $autoTopicIndex < scalar @$autoTopics)
+        {
+        my $topic = $parsedFile[$topicIndex];
+        my $autoTopic = $autoTopics->[$autoTopicIndex];
+
+        my $cleanTitle = $topic->Title();
+        $cleanTitle =~ s/[\t ]*\([^\(]*$//;
+
+        # Add the auto-topic if it's higher in the file than the current topic.
+        if ($autoTopic->LineNumber() < $topic->LineNumber())
+            {
+            if (exists $topicsInLists{$autoTopic->Type()} &&
+                exists $topicsInLists{$autoTopic->Type()}->{$autoTopic->Title()})
+                {
+                # Remove it from the list so a second one with the same name will be added.
+                delete $topicsInLists{$autoTopic->Type()}->{$autoTopic->Title()};
+                }
+            elsif (!NaturalDocs::Settings->DocumentedOnly())
+                {
+                splice(@parsedFile, $topicIndex, 0, $autoTopic);
+                $topicIndex++;
+                };
+
+            $autoTopicIndex++;
+            }
+
+        # Remove a headerless topic if there's another topic between it and the next auto-topic.
+        elsif (!$topic->Title() && $topicIndex + 1 < scalar @parsedFile &&
+                $parsedFile[$topicIndex+1]->LineNumber() < $autoTopic->LineNumber())
+            {
+            splice(@parsedFile, $topicIndex, 1);
+            }
+
+        # Transfer information if we have a match or a headerless topic.
+        elsif ( !$topic->Title() || ($topic->Type() == $autoTopic->Type() && index($autoTopic->Title(), $cleanTitle) != -1) )
+            {
+            $topic->SetType($autoTopic->Type());
+            $topic->SetPrototype($autoTopic->Prototype());
+            $topic->SetUsing($autoTopic->Using());
+
+            if (!$topic->Title())
+                {  $topic->SetTitle($autoTopic->Title());  };
+
+            if (NaturalDocs::Topics->TypeInfo($topic->Type())->Scope() != ::SCOPE_START())
+                {  $topic->SetPackage($autoTopic->Package());  }
+            elsif ($autoTopic->Package() ne $topic->Package())
+                {
+                my @autoPackageIdentifiers = NaturalDocs::SymbolString->IdentifiersOf($autoTopic->Package());
+                my @packageIdentifiers = NaturalDocs::SymbolString->IdentifiersOf($topic->Package());
+
+                while (scalar @autoPackageIdentifiers && $autoPackageIdentifiers[-1] eq $packageIdentifiers[-1])
+                    {
+                    pop @autoPackageIdentifiers;
+                    pop @packageIdentifiers;
+                    };
+
+                if (scalar @autoPackageIdentifiers)
+                    {  $topic->SetPackage( NaturalDocs::SymbolString->Join(@autoPackageIdentifiers) );  };
+                };
+
+            $topicIndex++;
+            $autoTopicIndex++;
+            }
+
+        # Extract topics in lists.
+        elsif ($topic->IsList())
+            {
+            if (!exists $topicsInLists{$topic->Type()})
+                {  $topicsInLists{$topic->Type()} = { };  };
+
+            my $body = $topic->Body();
+
+            while ($body =~ /<ds>([^<]+)<\/ds>/g)
+                {  $topicsInLists{$topic->Type()}->{NaturalDocs::NDMarkup->RestoreAmpChars($1)} = 1;  };
+
+            $topicIndex++;
+            }
+
+        # Otherwise there's no match.  Skip the topic.  The auto-topic will be added later.
+        else
+            {
+            $topicIndex++;
+            }
+        };
+
+    # Add any auto-topics remaining.
+    if (!NaturalDocs::Settings->DocumentedOnly())
+    	{
+	    while ($autoTopicIndex < scalar @$autoTopics)
+	        {
+	        my $autoTopic = $autoTopics->[$autoTopicIndex];
+
+	        if (exists $topicsInLists{$autoTopic->Type()} &&
+	            exists $topicsInLists{$autoTopic->Type()}->{$autoTopic->Title()})
+	            {
+	            # Remove it from the list so a second one with the same name will be added.
+	            delete $topicsInLists{$autoTopic->Type()}->{$autoTopic->Title()};
+	            }
+	        else
+	            {
+	            push(@parsedFile, $autoTopic);
+	            };
+
+	        $autoTopicIndex++;
+	        };
+        };
+   };
+
+
+#
+#   Function: RemoveRemainingHeaderlessTopics
+#
+#   After <MergeAutoTopics()> is done, this function removes any remaining headerless topics from the file.  If they don't merge
+#   into anything, they're not valid topics.
+#
+sub RemoveRemainingHeaderlessTopics
+    {
+    my ($self) = @_;
+
+    my $index = 0;
+    while ($index < scalar @parsedFile)
+        {
+        if ($parsedFile[$index]->Title())
+            {  $index++;  }
+        else
+            {  splice(@parsedFile, $index, 1);  };
+        };
+    };
+
+
+#
+#   Function: MakeAutoGroups
+#
+#   Creates group topics for files that do not have them.
+#
+sub MakeAutoGroups
+    {
+    my ($self) = @_;
+
+    # No groups only one topic.
+    if (scalar @parsedFile < 2)
+        {  return;  };
+
+    my $index = 0;
+    my $startStretch = 0;
+
+    # Skip the first entry if its the page title.
+    if (NaturalDocs::Topics->TypeInfo( $parsedFile[0]->Type() )->PageTitleIfFirst())
+        {
+        $index = 1;
+        $startStretch = 1;
+        };
+
+    # Make auto-groups for each stretch between scope-altering topics.
+    while ($index < scalar @parsedFile)
+        {
+        my $scope = NaturalDocs::Topics->TypeInfo($parsedFile[$index]->Type())->Scope();
+
+        if ($scope == ::SCOPE_START() || $scope == ::SCOPE_END())
+            {
+            if ($index > $startStretch)
+                {  $index += $self->MakeAutoGroupsFor($startStretch, $index);  };
+
+            $startStretch = $index + 1;
+            };
+
+        $index++;
+        };
+
+    if ($index > $startStretch)
+        {  $self->MakeAutoGroupsFor($startStretch, $index);  };
+    };
+
+
+#
+#   Function: MakeAutoGroupsFor
+#
+#   Creates group topics for sections of files that do not have them.  A support function for <MakeAutoGroups()>.
+#
+#   Parameters:
+#
+#       startIndex - The index to start at.
+#       endIndex - The index to end at.  Not inclusive.
+#
+#   Returns:
+#
+#       The number of group topics added.
+#
+sub MakeAutoGroupsFor #(startIndex, endIndex)
+    {
+    my ($self, $startIndex, $endIndex) = @_;
+
+    # No groups if any are defined already.
+    for (my $i = $startIndex; $i < $endIndex; $i++)
+        {
+        if ($parsedFile[$i]->Type() eq ::TOPIC_GROUP())
+            {  return 0;  };
+        };
+
+
+    use constant COUNT => 0;
+    use constant TYPE => 1;
+    use constant SECOND_TYPE => 2;
+    use constant SIZE => 3;
+
+    # This is an array of ( count, type, secondType ) triples.  Count and Type will always be filled in; count is the number of
+    # consecutive topics of type.  On the second pass, if small groups are combined secondType will be filled in.  There will not be
+    # more than two types per group.
+    my @groups;
+    my $groupIndex = 0;
+
+
+    # First pass: Determine all the groups.
+
+    my $i = $startIndex;
+    my $currentType;
+
+    while ($i < $endIndex)
+        {
+        if (!defined $currentType || ($parsedFile[$i]->Type() ne $currentType && $parsedFile[$i]->Type() ne ::TOPIC_GENERIC()) )
+            {
+            if (defined $currentType)
+                {  $groupIndex += SIZE;  };
+
+            $currentType = $parsedFile[$i]->Type();
+
+            $groups[$groupIndex + COUNT] = 1;
+            $groups[$groupIndex + TYPE] = $currentType;
+            }
+        else
+            {  $groups[$groupIndex + COUNT]++;  };
+
+        $i++;
+        };
+
+
+    # Second pass: Combine groups based on "noise".  Noise means types go from A to B to A at least once, and there are at least
+    # two groups in a row with three or less, and at least one of those groups is two or less.  So 3, 3, 3 doesn't count as noise, but
+    # 3, 2, 3 does.
+
+    $groupIndex = 0;
+
+    # While there are at least three groups left...
+    while ($groupIndex < scalar @groups - (2 * SIZE))
+        {
+        # If the group two places in front of this one has the same type...
+        if ($groups[$groupIndex + (2 * SIZE) + TYPE] eq $groups[$groupIndex + TYPE])
+            {
+            # It means we went from A to B to A, which partially qualifies as noise.
+
+            my $firstType = $groups[$groupIndex + TYPE];
+            my $secondType = $groups[$groupIndex + SIZE + TYPE];
+
+            if (NaturalDocs::Topics->TypeInfo($firstType)->CanGroupWith($secondType) ||
+                NaturalDocs::Topics->TypeInfo($secondType)->CanGroupWith($firstType))
+                {
+                my $hasNoise;
+
+                my $hasThrees;
+                my $hasTwosOrOnes;
+
+                my $endIndex = $groupIndex;
+
+                while ($endIndex < scalar @groups &&
+                         ($groups[$endIndex + TYPE] eq $firstType || $groups[$endIndex + TYPE] eq $secondType))
+                    {
+                    if ($groups[$endIndex + COUNT] > 3)
+                        {
+                        # They must be consecutive to count.
+                        $hasThrees = 0;
+                        $hasTwosOrOnes = 0;
+                        }
+                    elsif ($groups[$endIndex + COUNT] == 3)
+                        {
+                        $hasThrees = 1;
+
+                        if ($hasTwosOrOnes)
+                            {  $hasNoise = 1;  };
+                        }
+                    else # < 3
+                        {
+                        if ($hasThrees || $hasTwosOrOnes)
+                            {  $hasNoise = 1;  };
+
+                        $hasTwosOrOnes = 1;
+                        };
+
+                    $endIndex += SIZE;
+                    };
+
+                if (!$hasNoise)
+                    {
+                    $groupIndex = $endIndex - SIZE;
+                    }
+                else # hasNoise
+                    {
+                    $groups[$groupIndex + SECOND_TYPE] = $secondType;
+
+                    for (my $noiseIndex = $groupIndex + SIZE; $noiseIndex < $endIndex; $noiseIndex += SIZE)
+                        {
+                        $groups[$groupIndex + COUNT] += $groups[$noiseIndex + COUNT];
+                        };
+
+                    splice(@groups, $groupIndex + SIZE, $endIndex - $groupIndex - SIZE);
+
+                    $groupIndex += SIZE;
+                    };
+                }
+
+            else # They can't group together
+                {
+                $groupIndex += SIZE;
+                };
+            }
+
+        else
+            {  $groupIndex += SIZE;  };
+        };
+
+
+    # Finally, create group topics for the parsed file.
+
+    $groupIndex = 0;
+    $i = $startIndex;
+
+    while ($groupIndex < scalar @groups)
+        {
+        if ($groups[$groupIndex + TYPE] ne ::TOPIC_GENERIC())
+            {
+            my $topic = $parsedFile[$i];
+            my $title = NaturalDocs::Topics->NameOfType($groups[$groupIndex + TYPE], 1);
+
+            if (defined $groups[$groupIndex + SECOND_TYPE])
+                {  $title .= ' and ' . NaturalDocs::Topics->NameOfType($groups[$groupIndex + SECOND_TYPE], 1);  };
+
+            splice(@parsedFile, $i, 0, NaturalDocs::Parser::ParsedTopic->New(::TOPIC_GROUP(),
+                                                                                                            $title,
+                                                                                                            $topic->Package(), $topic->Using(),
+                                                                                                            undef, undef, undef,
+                                                                                                            $topic->LineNumber()) );
+            $i++;
+            };
+
+        $i += $groups[$groupIndex + COUNT];
+        $groupIndex += SIZE;
+        };
+
+    return (scalar @groups / SIZE);
+    };
+
+
+#
+#   Function: AddToClassHierarchy
+#
+#   Adds any class topics to the class hierarchy, since they may not have been called with <OnClass()> if they didn't match up to
+#   an auto-topic.
+#
+sub AddToClassHierarchy
+    {
+    my ($self) = @_;
+
+    foreach my $topic (@parsedFile)
+        {
+        if ($topic->Type() && NaturalDocs::Topics->TypeInfo( $topic->Type() )->ClassHierarchy())
+            {
+            if ($topic->IsList())
+                {
+                my $body = $topic->Body();
+
+                while ($body =~ /<ds>([^<]+)<\/ds>/g)
+                    {
+                    $self->OnClass( NaturalDocs::SymbolString->FromText( NaturalDocs::NDMarkup->RestoreAmpChars($1) ) );
+                    };
+                }
+            else
+                {
+                $self->OnClass($topic->Package());
+                };
+            };
+        };
+    };
+
+
+#
+#   Function: AddPackageDelineators
+#
+#   Adds section and class topics to make sure the package is correctly represented in the documentation.  Should be called last in
+#   this process.
+#
+sub AddPackageDelineators
+    {
+    my ($self) = @_;
+
+    my $index = 0;
+    my $currentPackage;
+
+    # Values are the arrayref [ title, type ];
+    my %usedPackages;
+
+    while ($index < scalar @parsedFile)
+        {
+        my $topic = $parsedFile[$index];
+
+        if ($topic->Package() ne $currentPackage)
+            {
+            $currentPackage = $topic->Package();
+            my $scopeType = NaturalDocs::Topics->TypeInfo($topic->Type())->Scope();
+
+            if ($scopeType == ::SCOPE_START())
+                {
+                $usedPackages{$currentPackage} = [ $topic->Title(), $topic->Type() ];
+                }
+            elsif ($scopeType == ::SCOPE_END())
+                {
+                my $newTopic;
+
+                if (!defined $currentPackage)
+                    {
+                    $newTopic = NaturalDocs::Parser::ParsedTopic->New(::TOPIC_SECTION(), 'Global',
+                                                                                                   undef, undef,
+                                                                                                   undef, undef, undef,
+                                                                                                   $topic->LineNumber(), undef);
+                    }
+                else
+                    {
+                    my ($title, $body, $summary, $type);
+                    my @packageIdentifiers = NaturalDocs::SymbolString->IdentifiersOf($currentPackage);
+
+                    if (exists $usedPackages{$currentPackage})
+                        {
+                        $title = $usedPackages{$currentPackage}->[0];
+                        $type = $usedPackages{$currentPackage}->[1];
+                        $body = '<p>(continued)</p>';
+                        $summary = '(continued)';
+                        }
+                    else
+                        {
+                        $title = join($language->PackageSeparator(), @packageIdentifiers);
+                        $type = ::TOPIC_CLASS();
+
+                        # Body and summary stay undef.
+
+                        $usedPackages{$currentPackage} = $title;
+                        };
+
+                    my @titleIdentifiers = NaturalDocs::SymbolString->IdentifiersOf( NaturalDocs::SymbolString->FromText($title) );
+                    for (my $i = 0; $i < scalar @titleIdentifiers; $i++)
+                        {  pop @packageIdentifiers;  };
+
+                    $newTopic = NaturalDocs::Parser::ParsedTopic->New($type, $title,
+                                                                                                   NaturalDocs::SymbolString->Join(@packageIdentifiers), undef,
+                                                                                                   undef, $summary, $body,
+                                                                                                   $topic->LineNumber(), undef);
+                    }
+
+                splice(@parsedFile, $index, 0, $newTopic);
+                $index++;
+                }
+            };
+
+        $index++;
+        };
+    };
+
+
+#
+#   Function: BreakLists
+#
+#   Breaks list topics into individual topics.
+#
+sub BreakLists
+    {
+    my $self = shift;
+
+    my $index = 0;
+
+    while ($index < scalar @parsedFile)
+        {
+        my $topic = $parsedFile[$index];
+
+        if ($topic->IsList() && NaturalDocs::Topics->TypeInfo( $topic->Type() )->BreakLists())
+            {
+            my $body = $topic->Body();
+
+            my @newTopics;
+            my $newBody;
+
+            my $bodyIndex = 0;
+
+            for (;;)
+                {
+                my $startList = index($body, '<dl>', $bodyIndex);
+
+                if ($startList == -1)
+                    {  last;  };
+
+                $newBody .= substr($body, $bodyIndex, $startList - $bodyIndex);
+
+                my $endList = index($body, '</dl>', $startList);
+                my $listBody = substr($body, $startList, $endList - $startList);
+
+                while ($listBody =~ /<ds>([^<]+)<\/ds><dd>(.*?)<\/dd>/g)
+                    {
+                    my ($symbol, $description) = ($1, $2);
+
+                    push @newTopics, NaturalDocs::Parser::ParsedTopic->New( $topic->Type(), $symbol, $topic->Package(),
+                                                                                                            $topic->Using(), undef,
+                                                                                                            $self->GetSummaryFromDescriptionList($description),
+                                                                                                            '<p>' . $description .  '</p>', $topic->LineNumber(),
+                                                                                                            undef );
+                    };
+
+                $bodyIndex = $endList + 5;
+                };
+
+            $newBody .= substr($body, $bodyIndex);
+
+            # Remove trailing headings.
+            $newBody =~ s/(?:<h>[^<]+<\/h>)+$//;
+
+            # Remove empty headings.
+            $newBody =~ s/(?:<h>[^<]+<\/h>)+(<h>[^<]+<\/h>)/$1/g;
+
+            if ($newBody)
+                {
+                unshift @newTopics, NaturalDocs::Parser::ParsedTopic->New( ::TOPIC_GROUP(), $topic->Title(), $topic->Package(),
+                                                                                                          $topic->Using(), undef,
+                                                                                                          $self->GetSummaryFromBody($newBody), $newBody,
+                                                                                                          $topic->LineNumber(), undef );
+                };
+
+            splice(@parsedFile, $index, 1, @newTopics);
+
+            $index += scalar @newTopics;
+            }
+
+        else # not a list
+            {  $index++;  };
+        };
+    };
+
+
+#
+#   Function: GetSummaryFromBody
+#
+#   Returns the summary text from the topic body.
+#
+#   Parameters:
+#
+#       body - The complete topic body, in <NDMarkup>.
+#
+#   Returns:
+#
+#       The topic summary, or undef if none.
+#
+sub GetSummaryFromBody #(body)
+    {
+    my ($self, $body) = @_;
+
+    my $summary;
+
+    # Extract the first sentence from the leading paragraph, if any.  We'll tolerate a single header beforehand, but nothing else.
+
+    if ($body =~ /^(?:<h>[^<]*<\/h>)?<p>(.*?)(<\/p>|[\.\!\?](?:[\)\}\'\ ]|&quot;|&gt;))/x)
+        {
+        $summary = $1;
+
+        if ($2 ne '</p>')
+            {  $summary .= $2;  };
+        };
+
+    return $summary;
+    };
+
+
+#
+#   Function: GetSummaryFromDescriptionList
+#
+#   Returns the summary text from a description list entry.
+#
+#   Parameters:
+#
+#       description - The description in <NDMarkup>.  Should be the content between the <dd></dd> tags only.
+#
+#   Returns:
+#
+#       The description summary, or undef if none.
+#
+sub GetSummaryFromDescriptionList #(description)
+    {
+    my ($self, $description) = @_;
+
+    my $summary;
+
+    if ($description =~ /^(.*?)($|[\.\!\?](?:[\)\}\'\ ]|&quot;|&gt;))/)
+        {  $summary = $1 . $2;  };
+
+    return $summary;
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Parser/JavaDoc.pm b/docs/tool/Modules/NaturalDocs/Parser/JavaDoc.pm
new file mode 100644
index 00000000..860b0c5f
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Parser/JavaDoc.pm
@@ -0,0 +1,464 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Parser::JavaDoc
+#
+###############################################################################
+#
+#   A package for translating JavaDoc topics into Natural Docs.
+#
+#   Supported tags:
+#
+#       - @param
+#       - @author
+#       - @deprecated
+#       - @code, @literal (doesn't change font)
+#       - @exception, @throws (doesn't link to class)
+#       - @link, @linkplain (doesn't change font)
+#       - @return, @returns
+#       - @see
+#       - @since
+#       - @value (shown as link instead of replacement)
+#       - @version
+#
+#   Stripped tags:
+#
+#       - @inheritDoc
+#       - @serial, @serialField, @serialData
+#       - All other block level tags.
+#
+#   Unsupported tags:
+#
+#       These will appear literally in the output because I cannot handle them easily.
+#
+#       - @docRoot
+#       - Any other tags not mentioned
+#
+#   Supported HTML:
+#
+#       - p
+#       - b, i, u
+#       - pre
+#       - a href
+#       - ol, ul, li (ol gets converted to ul)
+#       - gt, lt, amp, quot, nbsp entities
+#
+#   Stripped HTML:
+#
+#       - code
+#       - HTML comments
+#
+#   Unsupported HTML:
+#
+#       These will appear literally in the output because I cannot handle them easily.
+#
+#       - Any tags with additional properties other than a href.  (ex. <p class=Something>)
+#       - Any other tags not mentioned
+#
+#   Reference:
+#
+#       http://java.sun.com/j2se/1.5.0/docs/tooldocs/windows/javadoc.html
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Parser::JavaDoc;
+
+
+#
+#   hash: blockTags
+#   An existence hash of the all-lowercase JavaDoc block tags, not including the @.
+#
+my %blockTags = ( 'param' => 1, 'author' => 1, 'deprecated' => 1, 'exception' => 1, 'return' => 1, 'see' => 1,
+                             'serial' => 1, 'serialfield' => 1, 'serialdata' => 1, 'since' => 1, 'throws' => 1, 'version' => 1,
+                             'returns' => 1 );
+
+#
+#   hash: inlineTags
+#   An existence hash of the all-lowercase JavaDoc inline tags, not including the @.
+#
+my %inlineTags = ( 'inheritdoc' => 1, 'docroot' => 1, 'code' => 1, 'literal' => 1, 'link' => 1, 'linkplain' => 1, 'value' => 1 );
+
+
+##
+#   Examines the comment and returns whether it is *definitely* JavaDoc content, i.e. is owned by this package.
+#
+#   Parameters:
+#
+#       commentLines - An arrayref of the comment lines.  Must have been run through <NaturalDocs::Parser->CleanComment()>.
+#       isJavaDoc - Whether the comment is JavaDoc styled.  This doesn't necessarily mean it has JavaDoc content.
+#
+#   Returns:
+#
+#       Whether the comment is *definitely* JavaDoc content.
+#
+sub IsMine #(string[] commentLines, bool isJavaDoc)
+    {
+    my ($self, $commentLines, $isJavaDoc) = @_;
+
+    if (!$isJavaDoc)
+        {  return undef;  };
+
+    for (my $line = 0; $line < scalar @$commentLines; $line++)
+        {
+        if ($commentLines->[$line] =~ /^ *@([a-z]+) /i && exists $blockTags{$1} ||
+            $commentLines->[$line] =~ /\{@([a-z]+) /i && exists $inlineTags{$1})
+            {
+            return 1;
+            };
+        };
+
+    return 0;
+    };
+
+
+##
+#   Parses the JavaDoc-syntax comment and adds it to the parsed topic list.
+#
+#   Parameters:
+#
+#       commentLines - An arrayref of the comment lines.  Must have been run through <NaturalDocs::Parser->CleanComment()>.
+#                               *The original memory will be changed.*
+#       isJavaDoc - Whether the comment is JavaDoc styled.  This doesn't necessarily mean it has JavaDoc content.
+#       lineNumber - The line number of the first of the comment lines.
+#       parsedTopics - A reference to the array where any new <NaturalDocs::Parser::ParsedTopics> should be placed.
+#
+#   Returns:
+#
+#       The number of parsed topics added to the array, which in this case will always be one.
+#
+sub ParseComment #(string[] commentLines, bool isJavaDoc, int lineNumber, ParsedTopics[]* parsedTopics)
+    {
+    my ($self, $commentLines, $isJavaDoc, $lineNumber, $parsedTopics) = @_;
+
+
+    # Stage one: Before block level tags.
+
+    my $i = 0;
+    my $output;
+    my $unformattedText;
+    my $inCode;
+    my $sharedCodeIndent;
+
+    while ($i < scalar @$commentLines &&
+              !($commentLines->[$i] =~ /^ *@([a-z]+) /i && exists $blockTags{$1}) )
+        {
+        my $line = $self->ConvertAmpChars($commentLines->[$i]);
+        my @tokens = split(/(&lt;\/?pre&gt;)/, $line);
+
+        foreach my $token (@tokens)
+            {
+            if ($token =~ /^&lt;pre&gt;$/i)
+                {
+                if (!$inCode && $unformattedText)
+                    {
+                    $output .= '<p>' . $self->FormatText($unformattedText, 1) . '</p>';
+                    };
+
+                $inCode = 1;
+                $unformattedText = undef;
+                }
+            elsif ($token =~ /^&lt;\/pre&gt;$/i)
+                {
+                if ($inCode && $unformattedText)
+                    {
+                    $unformattedText =~ s/^ {$sharedCodeIndent}//mg;
+                    $unformattedText =~ s/\n{3,}/\n\n/g;
+                    $unformattedText =~ s/\n+$//;
+                    $output .= '<code>' . $unformattedText . '</code>';
+
+                    $sharedCodeIndent = undef;
+                    };
+
+                $inCode = 0;
+                $unformattedText = undef;
+                }
+            elsif (length($token))
+                {
+                if (!$inCode)
+                    {
+                    $token =~ s/^ +//;
+                    if ($unformattedText)
+                        {  $unformattedText .= ' ';  };
+                    }
+                else
+                    {
+                    $token =~ /^( *)/;
+                    my $indent = length($1);
+
+                    if (!defined $sharedCodeIndent || $indent < $sharedCodeIndent)
+                        {  $sharedCodeIndent = $indent;  };
+                    };
+
+                $unformattedText .= $token;
+                };
+            };
+
+        if ($inCode && $unformattedText)
+            {  $unformattedText .= "\n";  };
+
+        $i++;
+        };
+
+    if ($unformattedText)
+        {
+        if ($inCode)
+            {
+            $unformattedText =~ s/^ {$sharedCodeIndent}//mg;
+            $unformattedText =~ s/\n{3,}/\n\n/g;
+            $unformattedText =~ s/\n+$//;
+            $output .= '<code>' . $unformattedText . '</code>';
+            }
+        else
+            {  $output .= '<p>' . $self->FormatText($unformattedText, 1) . '</p>';  };
+
+        $unformattedText = undef;
+        };
+
+
+    # Stage two: Block level tags.
+
+    my ($keyword, $value, $unformattedTextPtr, $unformattedTextCloser);
+    my ($params, $authors, $deprecation, $throws, $returns, $seeAlso, $since, $version);
+
+
+    while ($i < scalar @$commentLines)
+        {
+        my $line = $self->ConvertAmpChars($commentLines->[$i]);
+        $line =~ s/^ +//;
+
+        if ($line =~ /^@([a-z]+) ?(.*)$/i)
+            {
+            ($keyword, $value) = (lc($1), $2);
+
+            # Process the previous one, if any.
+            if ($unformattedText)
+                {
+                $$unformattedTextPtr .= $self->FormatText($unformattedText) . $unformattedTextCloser;
+                $unformattedText = undef;
+                };
+
+            if ($keyword eq 'param')
+                {
+                $value =~ /^([a-z0-9_]+) *(.*)$/i;
+
+                $params .= '<de>' . $1 . '</de><dd>';
+                $unformattedText = $2;
+
+                $unformattedTextPtr = \$params;
+                $unformattedTextCloser = '</dd>';
+                }
+            elsif ($keyword eq 'exception' || $keyword eq 'throws')
+                {
+                $value =~ /^([a-z0-9_]+) *(.*)$/i;
+
+                $throws .= '<de>' . $1 . '</de><dd>';
+                $unformattedText = $2;
+
+                $unformattedTextPtr = \$throws;
+                $unformattedTextCloser = '</dd>';
+                }
+            elsif ($keyword eq 'return' || $keyword eq 'returns')
+                {
+                if ($returns)
+                    {  $returns .= ' ';  };
+
+                $unformattedText = $value;
+                $unformattedTextPtr = \$returns;
+                $unformattedTextCloser = undef;
+                }
+            elsif ($keyword eq 'author')
+                {
+                if ($authors)
+                    {  $authors .= ', ';  };
+
+                $unformattedText = $value;
+                $unformattedTextPtr = \$authors;
+                $unformattedTextCloser = undef;
+                }
+            elsif ($keyword eq 'deprecated')
+                {
+                if ($deprecation)
+                    {  $deprecation .= ' ';  };
+
+                $unformattedText = $value;
+                $unformattedTextPtr = \$deprecation;
+                $unformattedTextCloser = undef;
+                }
+            elsif ($keyword eq 'since')
+                {
+                if ($since)
+                    {  $since .= ', ';  };
+
+                $unformattedText = $value;
+                $unformattedTextPtr = \$since;
+                $unformattedTextCloser = undef;
+                }
+            elsif ($keyword eq 'version')
+                {
+                if ($version)
+                    {  $version .= ', ';  };
+
+                $unformattedText = $value;
+                $unformattedTextPtr = \$version;
+                $unformattedTextCloser = undef;
+                }
+            elsif ($keyword eq 'see')
+                {
+                if ($seeAlso)
+                    {  $seeAlso .= ', ';  };
+
+                $unformattedText = undef;
+
+                if ($value =~ /^&(?:quot|lt);/i)
+                    {  $seeAlso .= $self->FormatText($value);  }
+                else
+                    {  $seeAlso .= $self->ConvertLink($value);  };
+                };
+
+            # Everything else will be skipped.
+            }
+        elsif ($unformattedText)
+            {
+            $unformattedText .= ' ' . $line;
+            };
+
+        $i++;
+        };
+
+    if ($unformattedText)
+        {
+        $$unformattedTextPtr .= $self->FormatText($unformattedText) . $unformattedTextCloser;
+        $unformattedText = undef;
+        };
+
+    if ($params)
+        {  $output .= '<h>Parameters</h><dl>' . $params . '</dl>';  };
+    if ($returns)
+        {  $output .= '<h>Returns</h><p>' . $returns . '</p>';  };
+    if ($throws)
+        {  $output .= '<h>Throws</h><dl>' . $throws . '</dl>';  };
+    if ($since)
+        {  $output .= '<h>Since</h><p>' . $since . '</p>';  };
+    if ($version)
+        {  $output .= '<h>Version</h><p>' . $version . '</p>';  };
+    if ($deprecation)
+        {  $output .= '<h>Deprecated</h><p>' . $deprecation . '</p>';  };
+    if ($authors)
+        {  $output .= '<h>Author</h><p>' . $authors . '</p>';  };
+    if ($seeAlso)
+        {  $output .= '<h>See Also</h><p>' . $seeAlso . '</p>';  };
+
+
+    # Stage three: Build the parsed topic.
+
+    my $summary = NaturalDocs::Parser->GetSummaryFromBody($output);
+
+    push @$parsedTopics, NaturalDocs::Parser::ParsedTopic->New(undef, undef, undef, undef, undef, $summary,
+                                                                                                $output, $lineNumber, undef);
+    return 1;
+    };
+
+
+##
+#   Translates any inline tags or HTML codes to <NDMarkup> and returns it.
+#
+sub FormatText #(string text, bool inParagraph)
+    {
+    my ($self, $text, $inParagraph) = @_;
+
+    # JavaDoc Literal
+
+    $text =~ s/\{\@(?:code|literal) ([^\}]*)\}/$self->ConvertAmpChars($1)/gie;
+
+
+    # HTML
+
+    $text =~ s/&lt;b&gt;(.*?)&lt;\/b&gt;/<b>$1<\/b>/gi;
+    $text =~ s/&lt;i&gt;(.*?)&lt;\/i&gt;/<i>$1<\/i>/gi;
+    $text =~ s/&lt;u&gt;(.*?)&lt;\/u&gt;/<u>$1<\/u>/gi;
+
+    $text =~ s/&lt;code&gt;(.*?)&lt;\/code&gt;/$1/gi;
+
+    $text =~ s/&lt;ul.*?&gt;(.*?)&lt;\/ul&gt;/<ul>$1<\/ul>/gi;
+    $text =~ s/&lt;ol.*?&gt;(.*?)&lt;\/ol&gt;/<ul>$1<\/ul>/gi;
+    $text =~ s/&lt;li.*?&gt;(.*?)&lt;\/li&gt;/<li>$1<\/li>/gi;
+
+    $text =~ s/&lt;!--.*?--&gt;//gi;
+
+    $text =~ s/&lt;\/p&gt;//gi;
+    $text =~ s/^&lt;p&gt;//i;
+    if ($inParagraph)
+        {  $text =~ s/&lt;p&gt;/<\/p><p>/gi;  }
+    else
+        {  $text =~ s/&lt;p&gt;//gi;  };
+
+    $text =~ s/&lt;a href=&quot;mailto:(.*?)&quot;.*?&gt;(.*?)&lt;\/a&gt;/$self->MakeEMailLink($1, $2)/gie;
+    $text =~ s/&lt;a href=&quot;(.*?)&quot;.*?&gt;(.*?)&lt;\/a&gt;/$self->MakeURLLink($1, $2)/gie;
+
+    $text =~ s/&amp;nbsp;/ /gi;
+    $text =~ s/&amp;amp;/&amp;/gi;
+    $text =~ s/&amp;gt;/&gt;/gi;
+    $text =~ s/&amp;lt;/&lt;/gi;
+    $text =~ s/&amp;quot;/&quot;/gi;
+
+
+
+    # JavaDoc
+
+    $text =~ s/\{\@inheritdoc\}//gi;
+    $text =~ s/\{\@(?:linkplain|link|value) ([^\}]*)\}/$self->ConvertLink($1)/gie;
+
+    return $text;
+    };
+
+
+sub ConvertAmpChars #(text)
+    {
+    my ($self, $text) = @_;
+
+    $text =~ s/&/&amp;/g;
+    $text =~ s/</&lt;/g;
+    $text =~ s/>/&gt;/g;
+    $text =~ s/"/&quot;/g;
+
+    return $text;
+    };
+
+sub ConvertLink #(text)
+    {
+    my ($self, $text) = @_;
+
+    $text =~ /^ *([a-z0-9\_\.\:\#]+(?:\([^\)]*\))?) *(.*)$/i;
+    my ($target, $label) = ($1, $2);
+
+    # Convert the anchor to part of the link, but remove it altogether if it's the beginning of the link.
+    $target =~ s/^\#//;
+    $target =~ s/\#/\./;
+
+    $label =~ s/ +$//;
+
+    if (!length $label)
+        {  return '<link target="' . $target . '" name="' . $target . '" original="' . $target . '">';  }
+    else
+        {  return '<link target="' . $target . '" name="' . $label . '" original="' . $label . ' (' . $target . ')">';  };
+    };
+
+sub MakeURLLink #(target, text)
+    {
+    my ($self, $target, $text) = @_;
+    return '<url target="' . $target . '" name="' . $text . '">';
+    };
+
+sub MakeEMailLink #(target, text)
+    {
+    my ($self, $target, $text) = @_;
+    return '<email target="' . $target . '" name="' . $text . '">';
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Parser/Native.pm b/docs/tool/Modules/NaturalDocs/Parser/Native.pm
new file mode 100644
index 00000000..61ba97e5
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Parser/Native.pm
@@ -0,0 +1,1060 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Parser::Native
+#
+###############################################################################
+#
+#   A package that converts comments from Natural Docs' native format into <NaturalDocs::Parser::ParsedTopic> objects.
+#   Unlike most second-level packages, these are packages and not object classes.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+
+use strict;
+use integer;
+
+package NaturalDocs::Parser::Native;
+
+
+###############################################################################
+# Group: Variables
+
+
+# Return values of TagType().  Not documented here.
+use constant POSSIBLE_OPENING_TAG => 1;
+use constant POSSIBLE_CLOSING_TAG => 2;
+use constant NOT_A_TAG => 3;
+
+
+#
+#   var: package
+#
+#   A <SymbolString> representing the package normal topics will be a part of at the current point in the file.  This is a package variable
+#   because it needs to be reserved between function calls.
+#
+my $package;
+
+#
+#   hash: functionListIgnoredHeadings
+#
+#   An existence hash of all the headings that prevent the parser from creating function list symbols.  Whenever one of
+#   these headings are used in a function list topic, symbols are not created from definition lists until the next heading.  The keys
+#   are in all lowercase.
+#
+my %functionListIgnoredHeadings = ( 'parameters' => 1,
+                                                       'parameter' => 1,
+                                                       'params' => 1,
+                                                       'param' => 1,
+                                                       'arguments' => 1,
+                                                       'argument' => 1,
+                                                       'args' => 1,
+                                                       'arg' => 1 );
+
+
+###############################################################################
+# Group: Interface Functions
+
+
+#
+#   Function: Start
+#
+#   This will be called whenever a file is about to be parsed.  It allows the package to reset its internal state.
+#
+sub Start
+    {
+    my ($self) = @_;
+    $package = undef;
+    };
+
+
+#
+#   Function: IsMine
+#
+#   Examines the comment and returns whether it is *definitely* Natural Docs content, i.e. it is owned by this package.  Note
+#   that a comment can fail this function and still be interpreted as a Natural Docs content, for example a JavaDoc-styled comment
+#   that doesn't have header lines but no JavaDoc tags either.
+#
+#   Parameters:
+#
+#       commentLines - An arrayref of the comment lines.  Must have been run through <NaturalDocs::Parser->CleanComment()>.
+#       isJavaDoc - Whether the comment was JavaDoc-styled.
+#
+#   Returns:
+#
+#       Whether the comment is *definitely* Natural Docs content.
+#
+sub IsMine #(string[] commentLines, bool isJavaDoc)
+    {
+    my ($self, $commentLines, $isJavaDoc) = @_;
+
+    # Skip to the first line with content.
+    my $line = 0;
+
+    while ($line < scalar @$commentLines && !length $commentLines->[$line])
+        {  $line++;  };
+
+    return $self->ParseHeaderLine($commentLines->[$line]);
+    };
+
+
+
+#
+#   Function: ParseComment
+#
+#   This will be called whenever a comment capable of containing Natural Docs content is found.
+#
+#   Parameters:
+#
+#       commentLines - An arrayref of the comment lines.  Must have been run through <NaturalDocs::Parser->CleanComment()>.
+#                               *The original memory will be changed.*
+#       isJavaDoc - Whether the comment is JavaDoc styled.
+#       lineNumber - The line number of the first of the comment lines.
+#       parsedTopics - A reference to the array where any new <NaturalDocs::Parser::ParsedTopics> should be placed.
+#
+#   Returns:
+#
+#       The number of parsed topics added to the array, or zero if none.
+#
+sub ParseComment #(commentLines, isJavaDoc, lineNumber, parsedTopics)
+    {
+    my ($self, $commentLines, $isJavaDoc, $lineNumber, $parsedTopics) = @_;
+
+    my $topicCount = 0;
+    my $prevLineBlank = 1;
+    my $inCodeSection = 0;
+
+    my ($type, $scope, $isPlural, $title, $symbol);
+    #my $package;  # package variable.
+    my ($newKeyword, $newTitle);
+
+    my $index = 0;
+
+    my $bodyStart = 0;
+    my $bodyEnd = 0;  # Not inclusive.
+
+    while ($index < scalar @$commentLines)
+        {
+        # Everything but leading whitespace was removed beforehand.
+
+        # If we're in a code section...
+        if ($inCodeSection)
+            {
+            if ($commentLines->[$index] =~ /^ *\( *(?:end|finish|done)(?: +(?:table|code|example|diagram))? *\)$/i)
+                {  $inCodeSection = undef;  };
+
+            $prevLineBlank = 0;
+            $bodyEnd++;
+            }
+
+        # If the line is empty...
+        elsif (!length($commentLines->[$index]))
+            {
+            $prevLineBlank = 1;
+
+            if ($topicCount)
+                {  $bodyEnd++;  };
+            }
+
+        # If the line has a recognized header and the previous line is blank...
+        elsif ($prevLineBlank && (($newKeyword, $newTitle) = $self->ParseHeaderLine($commentLines->[$index])) )
+            {
+            # Process the previous one, if any.
+
+            if ($topicCount)
+                {
+                if ($scope == ::SCOPE_START() || $scope == ::SCOPE_END())
+                    {  $package = undef;  };
+
+                my $body = $self->FormatBody($commentLines, $bodyStart, $bodyEnd, $type, $isPlural);
+                my $newTopic = $self->MakeParsedTopic($type, $title, $package, $body, $lineNumber + $bodyStart - 1, $isPlural);
+                push @$parsedTopics, $newTopic;
+
+                $package = $newTopic->Package();
+                };
+
+            $title = $newTitle;
+
+            my $typeInfo;
+            ($type, $typeInfo, $isPlural) = NaturalDocs::Topics->KeywordInfo($newKeyword);
+            $scope = $typeInfo->Scope();
+
+            $bodyStart = $index + 1;
+            $bodyEnd = $index + 1;
+
+            $topicCount++;
+
+            $prevLineBlank = 0;
+            }
+
+        # If we're on a non-empty, non-header line of a JavaDoc-styled comment and we haven't started a topic yet...
+        elsif ($isJavaDoc && !$topicCount)
+            {
+            $type = undef;
+            $scope = ::SCOPE_NORMAL();  # The scope repair and topic merging processes will handle if this is a class topic.
+            $isPlural = undef;
+            $title = undef;
+            $symbol = undef;
+
+            $bodyStart = $index;
+            $bodyEnd = $index + 1;
+
+            $topicCount++;
+
+            $prevLineBlank = undef;
+            }
+
+        # If we're on a normal content line within a topic
+        elsif ($topicCount)
+            {
+            $prevLineBlank = 0;
+            $bodyEnd++;
+
+            if ($commentLines->[$index] =~ /^ *\( *(?:(?:start|begin)? +)?(?:table|code|example|diagram) *\)$/i)
+                {  $inCodeSection = 1;  };
+            };
+
+
+        $index++;
+        };
+
+
+    # Last one, if any.  This is the only one that gets the prototypes.
+    if ($bodyStart)
+        {
+        if ($scope == ::SCOPE_START() || $scope == ::SCOPE_END())
+            {  $package = undef;  };
+
+        my $body = $self->FormatBody($commentLines, $bodyStart, $bodyEnd, $type, $isPlural);
+        my $newTopic = $self->MakeParsedTopic($type, $title, $package, $body, $lineNumber + $bodyStart - 1, $isPlural);
+        push @$parsedTopics, $newTopic;
+        $topicCount++;
+
+        $package = $newTopic->Package();
+        };
+
+    return $topicCount;
+    };
+
+
+#
+#   Function: ParseHeaderLine
+#
+#   If the passed line is a topic header, returns the array ( keyword, title ).  Otherwise returns an empty array.
+#
+sub ParseHeaderLine #(line)
+    {
+    my ($self, $line) = @_;
+
+    if ($line =~ /^ *([a-z0-9 ]*[a-z0-9]): +(.*)$/i)
+        {
+        my ($keyword, $title) = ($1, $2);
+
+        # We need to do it this way because if you do "if (ND:T->KeywordInfo($keyword)" and the last element of the array it
+        # returns is false, the statement is false.  That is really retarded, but there it is.
+        my ($type, undef, undef) = NaturalDocs::Topics->KeywordInfo($keyword);
+
+        if ($type)
+            {  return ($keyword, $title);  }
+        else
+            {  return ( );  };
+        }
+    else
+        {  return ( );  };
+    };
+
+
+
+###############################################################################
+# Group: Support Functions
+
+
+#
+#   Function: MakeParsedTopic
+#
+#   Creates a <NaturalDocs::Parser::ParsedTopic> object for the passed parameters.  Scope is gotten from
+#   the package variable <package> instead of from the parameters.  The summary is generated from the body.
+#
+#   Parameters:
+#
+#       type         - The <TopicType>.  May be undef for headerless topics.
+#       title          - The title of the topic.  May be undef for headerless topics.
+#       package    - The package <SymbolString> the topic appears in.
+#       body        - The topic's body in <NDMarkup>.
+#       lineNumber - The topic's line number.
+#       isList         - Whether the topic is a list.
+#
+#   Returns:
+#
+#       The <NaturalDocs::Parser::ParsedTopic> object.
+#
+sub MakeParsedTopic #(type, title, package, body, lineNumber, isList)
+    {
+    my ($self, $type, $title, $package, $body, $lineNumber, $isList) = @_;
+
+    my $summary;
+
+    if (defined $body)
+        {  $summary = NaturalDocs::Parser->GetSummaryFromBody($body);  };
+
+    return NaturalDocs::Parser::ParsedTopic->New($type, $title, $package, undef, undef, $summary,
+                                                                         $body, $lineNumber, $isList);
+    };
+
+
+#
+#    Function: FormatBody
+#
+#    Converts the section body to <NDMarkup>.
+#
+#    Parameters:
+#
+#       commentLines - The arrayref of comment lines.
+#       startingIndex  - The starting index of the body to format.
+#       endingIndex   - The ending index of the body to format, *not* inclusive.
+#       type               - The type of the section.  May be undef for headerless comments.
+#       isList              - Whether it's a list topic.
+#
+#    Returns:
+#
+#        The body formatted in <NDMarkup>.
+#
+sub FormatBody #(commentLines, startingIndex, endingIndex, type, isList)
+    {
+    my ($self, $commentLines, $startingIndex, $endingIndex, $type, $isList) = @_;
+
+    use constant TAG_NONE => 1;
+    use constant TAG_PARAGRAPH => 2;
+    use constant TAG_BULLETLIST => 3;
+    use constant TAG_DESCRIPTIONLIST => 4;
+    use constant TAG_HEADING => 5;
+    use constant TAG_PREFIXCODE => 6;
+    use constant TAG_TAGCODE => 7;
+
+    my %tagEnders = ( TAG_NONE() => '',
+                                 TAG_PARAGRAPH() => '</p>',
+                                 TAG_BULLETLIST() => '</li></ul>',
+                                 TAG_DESCRIPTIONLIST() => '</dd></dl>',
+                                 TAG_HEADING() => '</h>',
+                                 TAG_PREFIXCODE() => '</code>',
+                                 TAG_TAGCODE() => '</code>' );
+
+    my $topLevelTag = TAG_NONE;
+
+    my $output;
+    my $textBlock;
+    my $prevLineBlank = 1;
+
+    my $codeBlock;
+    my $removedCodeSpaces;
+
+    my $ignoreListSymbols;
+
+    my $index = $startingIndex;
+
+    while ($index < $endingIndex)
+        {
+        # If we're in a tagged code section...
+        if ($topLevelTag == TAG_TAGCODE)
+            {
+            if ($commentLines->[$index] =~ /^ *\( *(?:end|finish|done)(?: +(?:table|code|example|diagram))? *\)$/i)
+                {
+                $codeBlock =~ s/\n+$//;
+                $output .= NaturalDocs::NDMarkup->ConvertAmpChars($codeBlock) . '</code>';
+                $codeBlock = undef;
+                $topLevelTag = TAG_NONE;
+                $prevLineBlank = undef;
+                }
+            else
+                {
+                $self->AddToCodeBlock($commentLines->[$index], \$codeBlock, \$removedCodeSpaces);
+                };
+            }
+
+        # If the line starts with a code designator...
+        elsif ($commentLines->[$index] =~ /^ *[>:|](.*)$/)
+            {
+            my $code = $1;
+
+            if ($topLevelTag == TAG_PREFIXCODE)
+                {
+                $self->AddToCodeBlock($code, \$codeBlock, \$removedCodeSpaces);
+                }
+            else # $topLevelTag != TAG_PREFIXCODE
+                {
+                if (defined $textBlock)
+                    {
+                    $output .= $self->RichFormatTextBlock($textBlock) . $tagEnders{$topLevelTag};
+                    $textBlock = undef;
+                    };
+
+                $topLevelTag = TAG_PREFIXCODE;
+                $output .= '<code>';
+                $self->AddToCodeBlock($code, \$codeBlock, \$removedCodeSpaces);
+                };
+            }
+
+        # If we're not in either code style...
+        else
+            {
+            # Strip any leading whitespace.
+            $commentLines->[$index] =~ s/^ +//;
+
+            # If we were in a prefixed code section...
+            if ($topLevelTag == TAG_PREFIXCODE)
+                {
+                $codeBlock =~ s/\n+$//;
+                $output .= NaturalDocs::NDMarkup->ConvertAmpChars($codeBlock) . '</code>';
+                $codeBlock = undef;
+                $topLevelTag = TAG_NONE;
+                $prevLineBlank = undef;
+                };
+
+
+            # If the line is blank...
+            if (!length($commentLines->[$index]))
+                {
+                # End a paragraph.  Everything else ignores it for now.
+                if ($topLevelTag == TAG_PARAGRAPH)
+                    {
+                    $output .= $self->RichFormatTextBlock($textBlock) . '</p>';
+                    $textBlock = undef;
+                    $topLevelTag = TAG_NONE;
+                    };
+
+                $prevLineBlank = 1;
+                }
+
+            # If the line starts with a bullet...
+            elsif ($commentLines->[$index] =~ /^[-\*o+] +([^ ].*)$/ &&
+                    substr($1, 0, 2) ne '- ')  # Make sure "o - Something" is a definition, not a bullet.
+                {
+                my $bulletedText = $1;
+
+                if (defined $textBlock)
+                    {  $output .= $self->RichFormatTextBlock($textBlock);  };
+
+                if ($topLevelTag == TAG_BULLETLIST)
+                    {
+                    $output .= '</li><li>';
+                    }
+                else #($topLevelTag != TAG_BULLETLIST)
+                    {
+                    $output .= $tagEnders{$topLevelTag} . '<ul><li>';
+                    $topLevelTag = TAG_BULLETLIST;
+                    };
+
+                $textBlock = $bulletedText;
+
+                $prevLineBlank = undef;
+                }
+
+            # If the line looks like a description list entry...
+            elsif ($commentLines->[$index] =~ /^(.+?) +- +([^ ].*)$/ && $topLevelTag != TAG_PARAGRAPH)
+                {
+                my $entry = $1;
+                my $description = $2;
+
+                if (defined $textBlock)
+                    {  $output .= $self->RichFormatTextBlock($textBlock);  };
+
+                if ($topLevelTag == TAG_DESCRIPTIONLIST)
+                    {
+                    $output .= '</dd>';
+                    }
+                else #($topLevelTag != TAG_DESCRIPTIONLIST)
+                    {
+                    $output .= $tagEnders{$topLevelTag} . '<dl>';
+                    $topLevelTag = TAG_DESCRIPTIONLIST;
+                    };
+
+                if (($isList && !$ignoreListSymbols) || $type eq ::TOPIC_ENUMERATION())
+                    {
+                    $output .= '<ds>' . NaturalDocs::NDMarkup->ConvertAmpChars($entry) . '</ds><dd>';
+                    }
+                else
+                    {
+                    $output .= '<de>' . NaturalDocs::NDMarkup->ConvertAmpChars($entry) . '</de><dd>';
+                    };
+
+                $textBlock = $description;
+
+                $prevLineBlank = undef;
+                }
+
+            # If the line could be a header...
+            elsif ($prevLineBlank && $commentLines->[$index] =~ /^(.*)([^ ]):$/)
+                {
+                my $headerText = $1 . $2;
+
+                if (defined $textBlock)
+                    {
+                    $output .= $self->RichFormatTextBlock($textBlock);
+                    $textBlock = undef;
+                    }
+
+                $output .= $tagEnders{$topLevelTag};
+                $topLevelTag = TAG_NONE;
+
+                $output .= '<h>' . $self->RichFormatTextBlock($headerText) . '</h>';
+
+                if ($type eq ::TOPIC_FUNCTION() && $isList)
+                    {
+                    $ignoreListSymbols = exists $functionListIgnoredHeadings{lc($headerText)};
+                    };
+
+                $prevLineBlank = undef;
+                }
+
+            # If the line looks like a code tag...
+            elsif ($commentLines->[$index] =~ /^\( *(?:(?:start|begin)? +)?(?:table|code|example|diagram) *\)$/i)
+                {
+                if (defined $textBlock)
+                    {
+                    $output .= $self->RichFormatTextBlock($textBlock);
+                    $textBlock = undef;
+                    };
+
+                $output .= $tagEnders{$topLevelTag} . '<code>';
+                $topLevelTag = TAG_TAGCODE;
+                }
+
+            # If the line looks like an inline image...
+            elsif ($commentLines->[$index] =~ /^(\( *see +)([^\)]+?)( *\))$/i)
+                {
+                if (defined $textBlock)
+                    {
+                    $output .= $self->RichFormatTextBlock($textBlock);
+                    $textBlock = undef;
+                    };
+
+                $output .= $tagEnders{$topLevelTag};
+                $topLevelTag = TAG_NONE;
+
+                $output .= '<img mode="inline" target="' . NaturalDocs::NDMarkup->ConvertAmpChars($2) . '" '
+                                . 'original="' . NaturalDocs::NDMarkup->ConvertAmpChars($1 . $2 . $3) . '">';
+
+                $prevLineBlank = undef;
+                }
+
+            # If the line isn't any of those, we consider it normal text.
+            else
+                {
+                # A blank line followed by normal text ends lists.  We don't handle this when we detect if the line's blank because
+                # we don't want blank lines between list items to break the list.
+                if ($prevLineBlank && ($topLevelTag == TAG_BULLETLIST || $topLevelTag == TAG_DESCRIPTIONLIST))
+                    {
+                    $output .= $self->RichFormatTextBlock($textBlock) . $tagEnders{$topLevelTag} . '<p>';
+
+                    $topLevelTag = TAG_PARAGRAPH;
+                    $textBlock = undef;
+                    }
+
+                elsif ($topLevelTag == TAG_NONE)
+                    {
+                    $output .= '<p>';
+                    $topLevelTag = TAG_PARAGRAPH;
+                    # textBlock will already be undef.
+                    };
+
+                if (defined $textBlock)
+                    {  $textBlock .= ' ';  };
+
+                $textBlock .= $commentLines->[$index];
+
+                $prevLineBlank = undef;
+                };
+            };
+
+        $index++;
+        };
+
+    # Clean up anything left dangling.
+    if (defined $textBlock)
+        {
+        $output .= $self->RichFormatTextBlock($textBlock) . $tagEnders{$topLevelTag};
+        }
+    elsif (defined $codeBlock)
+        {
+        $codeBlock =~ s/\n+$//;
+        $output .= NaturalDocs::NDMarkup->ConvertAmpChars($codeBlock) . '</code>';
+        };
+
+    return $output;
+    };
+
+
+#
+#   Function: AddToCodeBlock
+#
+#   Adds a line of text to a code block, handling all the indentation processing required.
+#
+#   Parameters:
+#
+#       line - The line of text to add.
+#       codeBlockRef - A reference to the code block to add it to.
+#       removedSpacesRef - A reference to a variable to hold the number of spaces removed.  It needs to be stored between calls.
+#                                      It will reset itself automatically when the code block codeBlockRef points to is undef.
+#
+sub AddToCodeBlock #(line, codeBlockRef, removedSpacesRef)
+    {
+    my ($self, $line, $codeBlockRef, $removedSpacesRef) = @_;
+
+    $line =~ /^( *)(.*)$/;
+    my ($spaces, $code) = ($1, $2);
+
+    if (!defined $$codeBlockRef)
+        {
+        if (length($code))
+            {
+            $$codeBlockRef = $code . "\n";
+            $$removedSpacesRef = length($spaces);
+            };
+        # else ignore leading line breaks.
+        }
+
+    elsif (length $code)
+        {
+        # Make sure we have the minimum amount of spaces to the left possible.
+        if (length($spaces) != $$removedSpacesRef)
+            {
+            my $spaceDifference = abs( length($spaces) - $$removedSpacesRef );
+            my $spacesToAdd = ' ' x $spaceDifference;
+
+            if (length($spaces) > $$removedSpacesRef)
+                {
+                $$codeBlockRef .= $spacesToAdd;
+                }
+            else
+                {
+                $$codeBlockRef =~ s/^(.)/$spacesToAdd . $1/gme;
+                $$removedSpacesRef = length($spaces);
+                };
+            };
+
+        $$codeBlockRef .= $code . "\n";
+        }
+
+    else # (!length $code)
+        {
+        $$codeBlockRef .= "\n";
+        };
+    };
+
+
+#
+#   Function: RichFormatTextBlock
+#
+#   Applies rich <NDMarkup> formatting to a chunk of text.  This includes both amp chars, formatting tags, and link tags.
+#
+#   Parameters:
+#
+#       text - The block of text to format.
+#
+#   Returns:
+#
+#       The formatted text block.
+#
+sub RichFormatTextBlock #(text)
+    {
+    my ($self, $text) = @_;
+    my $output;
+
+
+    # First find bare urls, e-mail addresses, and images.  We have to do this before the split because they may contain underscores
+    # or asterisks.  We have to mark the tags with \x1E and \x1F so they don't get confused with angle brackets from the comment.
+    # We can't convert the amp chars beforehand because we need lookbehinds in the regexps below and they need to be
+    # constant length.  Sucks, huh?
+
+    $text =~ s{
+                       # The previous character can't be an alphanumeric or an opening angle bracket.
+                       (?<!  [a-z0-9<]  )
+
+                       # Optional mailto:.  Ignored in output.
+                       (?:mailto\:)?
+
+                       # Begin capture
+                       (
+
+                       # The user portion.  Alphanumeric and - _.  Dots can appear between, but not at the edges or more than
+                       # one in a row.
+                       (?:  [a-z0-9\-_]+  \.  )*   [a-z0-9\-_]+
+
+                       @
+
+                       # The domain.  Alphanumeric and -.  Dots same as above, however, there must be at least two sections
+                       # and the last one must be two to four alphanumeric characters (.com, .uk, .info, .203 for IP addresses)
+                       (?:  [a-z0-9\-]+  \.  )+  [a-z]{2,4}
+
+                       # End capture.
+                       )
+
+                       # The next character can't be an alphanumeric, which should prevent .abcde from matching the two to
+                       # four character requirement, or a closing angle bracket.
+                       (?!  [a-z0-9>]  )
+
+                       }
+
+                       {"\x1E" . 'email target="' . NaturalDocs::NDMarkup->ConvertAmpChars($1) . '" '
+                       . 'name="' . NaturalDocs::NDMarkup->ConvertAmpChars($1) . '"' . "\x1F"}igxe;
+
+    $text =~ s{
+                       # The previous character can't be an alphanumeric or an opening angle bracket.
+                       (?<!  [a-z0-9<]  )
+
+                       # Begin capture.
+                       (
+
+                       # URL must start with one of the acceptable protocols.
+                       (?:http|https|ftp|news|file)\:
+
+                       # The acceptable URL characters as far as I know.
+                       [a-z0-9\-\=\~\@\#\%\&\_\+\/\;\:\?\*\.\,]*
+
+                       # The URL characters minus period and comma.  If it ends on them, they're probably intended as
+                       # punctuation.
+                       [a-z0-9\-\=\~\@\#\%\&\_\+\/\;\:\?\*]
+
+                       # End capture.
+                       )
+
+                       # The next character must not be an acceptable character or a closing angle bracket.  This will prevent the URL
+                       # from ending early just to get a match.
+                       (?!  [a-z0-9\-\=\~\@\#\%\&\_\+\/\;\:\?\*\>]  )
+
+                       }
+
+                       {"\x1E" . 'url target="' . NaturalDocs::NDMarkup->ConvertAmpChars($1) . '" '
+                       . 'name="' . NaturalDocs::NDMarkup->ConvertAmpChars($1) . '"' . "\x1F"}igxe;
+
+
+    # Find image links.  Inline images should already be pulled out by now.
+
+    $text =~ s{(\( *see +)([^\)]+?)( *\))}
+                      {"\x1E" . 'img mode="link" target="' . NaturalDocs::NDMarkup->ConvertAmpChars($2) . '" '
+                        . 'original="' . NaturalDocs::NDMarkup->ConvertAmpChars($1 . $2 . $3) . '"' . "\x1F"}gie;
+
+
+
+    # Split the text from the potential tags.
+
+    my @tempTextBlocks = split(/([\*_<>\x1E\x1F])/, $text);
+
+    # Since the symbols are considered dividers, empty strings could appear between two in a row or at the beginning/end of the
+    # array.  This could seriously screw up TagType(), so we need to get rid of them.
+    my @textBlocks;
+
+    while (scalar @tempTextBlocks)
+        {
+        my $tempTextBlock = shift @tempTextBlocks;
+
+        if (length $tempTextBlock)
+            {  push @textBlocks, $tempTextBlock;  };
+        };
+
+
+    my $bold;
+    my $underline;
+    my $underlineHasWhitespace;
+
+    my $index = 0;
+
+    while ($index < scalar @textBlocks)
+        {
+        if ($textBlocks[$index] eq "\x1E")
+            {
+            $output .= '<';
+            $index++;
+
+            while ($textBlocks[$index] ne "\x1F")
+                {
+                $output .= $textBlocks[$index];
+                $index++;
+                };
+
+            $output .= '>';
+            }
+
+        elsif ($textBlocks[$index] eq '<' && $self->TagType(\@textBlocks, $index) == POSSIBLE_OPENING_TAG)
+            {
+            my $endingIndex = $self->ClosingTag(\@textBlocks, $index, undef);
+
+            if ($endingIndex != -1)
+                {
+                my $linkText;
+                $index++;
+
+                while ($index < $endingIndex)
+                    {
+                    $linkText .= $textBlocks[$index];
+                    $index++;
+                    };
+                # Index will be incremented again at the end of the loop.
+
+                $linkText = NaturalDocs::NDMarkup->ConvertAmpChars($linkText);
+
+                if ($linkText =~ /^(?:mailto\:)?((?:[a-z0-9\-_]+\.)*[a-z0-9\-_]+@(?:[a-z0-9\-]+\.)+[a-z]{2,4})$/i)
+                    {  $output .= '<email target="' . $1 . '" name="' . $1 . '">';  }
+                elsif ($linkText =~ /^(?:http|https|ftp|news|file)\:/i)
+                    {  $output .= '<url target="' . $linkText . '" name="' . $linkText . '">';  }
+                else
+                    {  $output .= '<link target="' . $linkText . '" name="' . $linkText . '" original="&lt;' . $linkText . '&gt;">';  };
+                }
+
+            else # it's not a link.
+                {
+                $output .= '&lt;';
+                };
+            }
+
+        elsif ($textBlocks[$index] eq '*')
+            {
+            my $tagType = $self->TagType(\@textBlocks, $index);
+
+            if ($tagType == POSSIBLE_OPENING_TAG && $self->ClosingTag(\@textBlocks, $index, undef) != -1)
+                {
+                # ClosingTag() makes sure tags aren't opened multiple times in a row.
+                $bold = 1;
+                $output .= '<b>';
+                }
+            elsif ($bold && $tagType == POSSIBLE_CLOSING_TAG)
+                {
+                $bold = undef;
+                $output .= '</b>';
+                }
+            else
+                {
+                $output .= '*';
+                };
+            }
+
+        elsif ($textBlocks[$index] eq '_')
+            {
+            my $tagType = $self->TagType(\@textBlocks, $index);
+
+             if ($tagType == POSSIBLE_OPENING_TAG && $self->ClosingTag(\@textBlocks, $index, \$underlineHasWhitespace) != -1)
+                {
+                # ClosingTag() makes sure tags aren't opened multiple times in a row.
+                $underline = 1;
+                #underlineHasWhitespace is set by ClosingTag().
+                $output .= '<u>';
+                }
+            elsif ($underline && $tagType == POSSIBLE_CLOSING_TAG)
+                {
+                $underline = undef;
+                #underlineHasWhitespace will be reset by the next opening underline.
+                $output .= '</u>';
+                }
+            elsif ($underline && !$underlineHasWhitespace)
+                {
+                # If there's no whitespace between underline tags, all underscores are replaced by spaces so
+                # _some_underlined_text_ becomes <u>some underlined text</u>.  The standard _some underlined text_
+                # will work too.
+                $output .= ' ';
+                }
+            else
+                {
+                $output .= '_';
+                };
+            }
+
+        else # plain text or a > that isn't part of a link
+            {
+            $output .= NaturalDocs::NDMarkup->ConvertAmpChars($textBlocks[$index]);
+           };
+
+        $index++;
+        };
+
+    return $output;
+    };
+
+
+#
+#   Function: TagType
+#
+#   Returns whether the tag is a possible opening or closing tag, or neither.  "Possible" because it doesn't check if an opening tag is
+#   closed or a closing tag is opened, just whether the surrounding characters allow it to be a candidate for a tag.  For example, in
+#   "A _B" the underscore is a possible opening underline tag, but in "A_B" it is not.  Support function for <RichFormatTextBlock()>.
+#
+#   Parameters:
+#
+#       textBlocks  - A reference to an array of text blocks.
+#       index         - The index of the tag.
+#
+#   Returns:
+#
+#       POSSIBLE_OPENING_TAG, POSSIBLE_CLOSING_TAG, or NOT_A_TAG.
+#
+sub TagType #(textBlocks, index)
+    {
+    my ($self, $textBlocks, $index) = @_;
+
+
+    # Possible opening tags
+
+    if ( ( $textBlocks->[$index] =~ /^[\*_<]$/ ) &&
+
+        # Before it must be whitespace, the beginning of the text, or ({["'-/*_.
+        ( $index == 0 || $textBlocks->[$index-1] =~ /[\ \t\n\(\{\[\"\'\-\/\*\_]$/ ) &&
+
+        # Notes for 2.0: Include Spanish upside down ! and ? as well as opening quotes (66) and apostrophes (6).  Look into
+        # Unicode character classes as well.
+
+        # After it must be non-whitespace.
+        ( $index + 1 < scalar @$textBlocks && $textBlocks->[$index+1] !~ /^[\ \t\n]/) &&
+
+        # Make sure we don't accept <<, <=, <-, or *= as opening tags.
+        ( $textBlocks->[$index] ne '<' || $textBlocks->[$index+1] !~ /^[<=-]/ ) &&
+        ( $textBlocks->[$index] ne '*' || $textBlocks->[$index+1] !~ /^[\=\*]/ ) &&
+
+        # Make sure we don't accept * or _ before it unless it's <.
+        ( $textBlocks->[$index] eq '<' || $index == 0 || $textBlocks->[$index-1] !~ /[\*\_]$/) )
+        {
+        return POSSIBLE_OPENING_TAG;
+        }
+
+
+    # Possible closing tags
+
+    elsif ( ( $textBlocks->[$index] =~ /^[\*_>]$/) &&
+
+            # After it must be whitespace, the end of the text, or )}].,!?"';:-/*_.
+            ( $index + 1 == scalar @$textBlocks || $textBlocks->[$index+1] =~ /^[ \t\n\)\]\}\.\,\!\?\"\'\;\:\-\/\*\_]/ ||
+              # Links also get plurals, like <link>s, <linx>es, <link>'s, and <links>'.
+              ( $textBlocks->[$index] eq '>' && $textBlocks->[$index+1] =~ /^(?:es|s|\')/ ) ) &&
+
+            # Notes for 2.0: Include closing quotes (99) and apostrophes (9).  Look into Unicode character classes as well.
+
+            # Before it must be non-whitespace.
+            ( $index != 0 && $textBlocks->[$index-1] !~ /[ \t\n]$/ ) &&
+
+            # Make sure we don't accept >>, ->, or => as closing tags.  >= is already taken care of.
+            ( $textBlocks->[$index] ne '>' || $textBlocks->[$index-1] !~ /[>=-]$/ ) &&
+
+            # Make sure we don't accept * or _ after it unless it's >.
+            ( $textBlocks->[$index] eq '>' || $textBlocks->[$index+1] !~ /[\*\_]$/) )
+        {
+        return POSSIBLE_CLOSING_TAG;
+        }
+
+    else
+        {
+        return NOT_A_TAG;
+        };
+
+    };
+
+
+#
+#   Function: ClosingTag
+#
+#   Returns whether a tag is closed or not, where it's closed if it is, and optionally whether there is any whitespace between the
+#   tags.  Support function for <RichFormatTextBlock()>.
+#
+#   The results of this function are in full context, meaning that if it says a tag is closed, it can be interpreted as that tag in the
+#   final output.  It takes into account any spoiling factors, like there being two opening tags in a row.
+#
+#   Parameters:
+#
+#       textBlocks             - A reference to an array of text blocks.
+#       index                    - The index of the opening tag.
+#       hasWhitespaceRef  - A reference to the variable that will hold whether there is whitespace between the tags or not.  If
+#                                     undef, the function will not check.  If the tag is not closed, the variable will not be changed.
+#
+#   Returns:
+#
+#       If the tag is closed, it returns the index of the closing tag and puts whether there was whitespace between the tags in
+#       hasWhitespaceRef if it was specified.  If the tag is not closed, it returns -1 and doesn't touch the variable pointed to by
+#       hasWhitespaceRef.
+#
+sub ClosingTag #(textBlocks, index, hasWhitespace)
+    {
+    my ($self, $textBlocks, $index, $hasWhitespaceRef) = @_;
+
+    my $hasWhitespace;
+    my $closingTag;
+
+    if ($textBlocks->[$index] eq '*' || $textBlocks->[$index] eq '_')
+        {  $closingTag = $textBlocks->[$index];  }
+    elsif ($textBlocks->[$index] eq '<')
+        {  $closingTag = '>';  }
+    else
+        {  return -1;  };
+
+    my $beginningIndex = $index;
+    $index++;
+
+    while ($index < scalar @$textBlocks)
+        {
+        if ($textBlocks->[$index] eq '<' && $self->TagType($textBlocks, $index) == POSSIBLE_OPENING_TAG)
+            {
+            # If we hit a < and we're checking whether a link is closed, it's not.  The first < becomes literal and the second one
+            # becomes the new link opening.
+            if ($closingTag eq '>')
+                {
+                return -1;
+                }
+
+            # If we're not searching for the end of a link, we have to skip the link because formatting tags cannot appear within
+            # them.  That's of course provided it's closed.
+            else
+                {
+                my $linkHasWhitespace;
+
+                my $endIndex = $self->ClosingTag($textBlocks, $index,
+                                                                    ($hasWhitespaceRef && !$hasWhitespace ? \$linkHasWhitespace : undef) );
+
+                if ($endIndex != -1)
+                    {
+                    if ($linkHasWhitespace)
+                        {  $hasWhitespace = 1;  };
+
+                    # index will be incremented again at the end of the loop, which will bring us past the link's >.
+                    $index = $endIndex;
+                    };
+                };
+            }
+
+        elsif ($textBlocks->[$index] eq $closingTag)
+            {
+            my $tagType = $self->TagType($textBlocks, $index);
+
+            if ($tagType == POSSIBLE_CLOSING_TAG)
+                {
+                # There needs to be something between the tags for them to count.
+                if ($index == $beginningIndex + 1)
+                    {  return -1;  }
+                else
+                    {
+                    # Success!
+
+                    if ($hasWhitespaceRef)
+                        {  $$hasWhitespaceRef = $hasWhitespace;  };
+
+                    return $index;
+                    };
+                }
+
+            # If there are two opening tags of the same type, the first becomes literal and the next becomes part of a tag.
+            elsif ($tagType == POSSIBLE_OPENING_TAG)
+                {  return -1;  }
+            }
+
+        elsif ($hasWhitespaceRef && !$hasWhitespace)
+            {
+            if ($textBlocks->[$index] =~ /[ \t\n]/)
+                {  $hasWhitespace = 1;  };
+            };
+
+        $index++;
+        };
+
+    # Hit the end of the text blocks if we're here.
+    return -1;
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Parser/ParsedTopic.pm b/docs/tool/Modules/NaturalDocs/Parser/ParsedTopic.pm
new file mode 100644
index 00000000..a08d65ad
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Parser/ParsedTopic.pm
@@ -0,0 +1,253 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Parser::ParsedTopic
+#
+###############################################################################
+#
+#   A class for parsed topics of source files.  Also encompasses some of the <TopicType>-specific behavior.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Parser::ParsedTopic;
+
+
+###############################################################################
+# Group: Implementation
+
+#
+#   Constants: Members
+#
+#   The object is a blessed arrayref with the following indexes.
+#
+#       TYPE           - The <TopicType>.
+#       TITLE          - The title of the topic.
+#       PACKAGE    - The package <SymbolString> the topic appears in, or undef if none.
+#       USING         - An arrayref of additional package <SymbolStrings> available to the topic via "using" statements, or undef if
+#                           none.
+#       PROTOTYPE - The prototype, if it exists and is applicable.
+#       SUMMARY    - The summary, if it exists.
+#       BODY          - The body of the topic, formatted in <NDMarkup>.  Some topics may not have bodies, and if not, this
+#                           will be undef.
+#       LINE_NUMBER  - The line number the topic appears at in the file.
+#       IS_LIST - Whether the topic is a list.
+#
+use NaturalDocs::DefineMembers 'TYPE', 'TITLE', 'PACKAGE', 'USING', 'PROTOTYPE', 'SUMMARY', 'BODY',
+                                                 'LINE_NUMBER', 'IS_LIST';
+# DEPENDENCY: New() depends on the order of these constants, and that this class is not inheriting any members.
+
+
+#
+#   Architecture: Title, Package, and Symbol Behavior
+#
+#   Title, package, and symbol behavior is a little awkward so it deserves some explanation.  Basically you set them according to
+#   certain rules, but you get computed values that try to hide all the different scoping situations.
+#
+#   Normal Topics:
+#
+#       Set them to the title and package as they appear.  "Function" and "PkgA.PkgB" will return "Function" for the title,
+#       "PkgA.PkgB" for the package, and "PkgA.PkgB.Function" for the symbol.
+#
+#       In the rare case that a title has a separator symbol it's treated as inadvertant, so "A vs. B" in "PkgA.PkgB" still returns just
+#       "PkgA.PkgB" for the package even though if you got it from the symbol it can be seen as "PkgA.PkgB.A vs".
+#
+#   Scope Topics:
+#
+#       Set the title normally and leave the package undef.  So "PkgA.PkgB" and undef will return "PkgA.PkgB" for the title as well
+#       as for the package and symbol.
+#
+#       The only time you should set the package is when you have full language support and they only documented the class with
+#       a partial title.  So if you documented "PkgA.PkgB" with just "PkgB", you want to set the package to "PkgA".  This
+#       will return "PkgB" as the title for presentation and will return "PkgA.PkgB" for the package and symbol, which is correct.
+#
+#   Always Global Topics:
+#
+#       Set the title and package normally, do not set the package to undef.  So "Global" and "PkgA.PkgB" will return "Global" as
+#       the title, "PkgA.PkgB" as the package, and "Global" as the symbol.
+#
+#   Um, yeah...:
+#
+#       So does this suck?  Yes, yes it does.  But the suckiness is centralized here instead of having to be handled everywhere these
+#       issues come into play.  Just realize there are a certain set of rules to follow when you *set* these variables, and the results
+#       you see when you *get* them are computed rather than literal.
+#
+
+
+###############################################################################
+# Group: Functions
+
+#
+#   Function: New
+#
+#   Creates a new object.
+#
+#   Parameters:
+#
+#       type          - The <TopicType>.
+#       title           - The title of the topic.
+#       package    - The package <SymbolString> the topic appears in, or undef if none.
+#       using         - An arrayref of additional package <SymbolStrings> available to the topic via "using" statements, or undef if
+#                          none.
+#       prototype   - The prototype, if it exists and is applicable.  Otherwise set to undef.
+#       summary   - The summary of the topic, if any.
+#       body          - The body of the topic, formatted in <NDMarkup>.  May be undef, as some topics may not have bodies.
+#       lineNumber - The line number the topic appears at in the file.
+#       isList          - Whether the topic is a list topic or not.
+#
+#   Returns:
+#
+#       The new object.
+#
+sub New #(type, title, package, using, prototype, summary, body, lineNumber, isList)
+    {
+    # DEPENDENCY: This depends on the order of the parameter list being the same as the constants, and that there are no
+    # members inherited from a base class.
+
+    my $package = shift;
+
+    my $object = [ @_ ];
+    bless $object, $package;
+
+    if (defined $object->[USING])
+        {  $object->[USING] = [ @{$object->[USING]} ];  };
+
+    return $object;
+    };
+
+
+# Function: Type
+# Returns the <TopicType>.
+sub Type
+    {  return $_[0]->[TYPE];  };
+
+# Function: SetType
+# Replaces the <TopicType>.
+sub SetType #(type)
+    {  $_[0]->[TYPE] = $_[1];  };
+
+# Function: IsList
+# Returns whether the topic is a list.
+sub IsList
+    {  return $_[0]->[IS_LIST];  };
+
+# Function: SetIsList
+# Sets whether the topic is a list.
+sub SetIsList
+    {  $_[0]->[IS_LIST] = $_[1];  };
+
+# Function: Title
+# Returns the title of the topic.
+sub Title
+    {  return $_[0]->[TITLE];  };
+
+# Function: SetTitle
+# Replaces the topic title.
+sub SetTitle #(title)
+    {  $_[0]->[TITLE] = $_[1];  };
+
+#
+#   Function: Symbol
+#
+#   Returns the <SymbolString> defined by the topic.  It is fully resolved and does _not_ need to be joined with <Package()>.
+#
+#   Type-Specific Behavior:
+#
+#       - If the <TopicType> is always global, the symbol will be generated from the title only.
+#       - Everything else's symbols will be generated from the title and the package passed to <New()>.
+#
+sub Symbol
+    {
+    my ($self) = @_;
+
+    my $titleSymbol = NaturalDocs::SymbolString->FromText($self->[TITLE]);
+
+    if (NaturalDocs::Topics->TypeInfo($self->Type())->Scope() == ::SCOPE_ALWAYS_GLOBAL())
+        {  return $titleSymbol;  }
+    else
+        {
+        return NaturalDocs::SymbolString->Join( $self->[PACKAGE], $titleSymbol );
+        };
+    };
+
+
+#
+#   Function: Package
+#
+#   Returns the package <SymbolString> that the topic appears in.
+#
+#   Type-Specific Behavior:
+#
+#       - If the <TopicType> has scope, the package will be generated from both the title and the package passed to <New()>, not
+#         just the package.
+#       - If the <TopicType> is always global, the package will be the one passed to <New()>, even though it isn't part of it's
+#         <Symbol()>.
+#       - Everything else's package will be what was passed to <New()>, even if the title has separator symbols in it.
+#
+sub Package
+    {
+    my ($self) = @_;
+
+    # Headerless topics may not have a type yet.
+    if ($self->Type() && NaturalDocs::Topics->TypeInfo($self->Type())->Scope() == ::SCOPE_START())
+        {  return $self->Symbol();  }
+    else
+        {  return $self->[PACKAGE];  };
+    };
+
+
+# Function: SetPackage
+# Replaces the package the topic appears in.  This will behave the same way as the package parameter in <New()>.  Later calls
+# to <Package()> will still be generated according to its type-specific behavior.
+sub SetPackage #(package)
+    {  $_[0]->[PACKAGE] = $_[1];  };
+
+# Function: Using
+# Returns an arrayref of additional scope <SymbolStrings> available to the topic via "using" statements, or undef if none.
+sub Using
+    {  return $_[0]->[USING];  };
+
+# Function: SetUsing
+# Replaces the using arrayref of sope <SymbolStrings>.
+sub SetUsing #(using)
+    {  $_[0]->[USING] = $_[1];  };
+
+# Function: Prototype
+# Returns the prototype if one is defined.  Will be undef otherwise.
+sub Prototype
+    {  return $_[0]->[PROTOTYPE];  };
+
+# Function: SetPrototype
+# Replaces the function or variable prototype.
+sub SetPrototype #(prototype)
+    {  $_[0]->[PROTOTYPE] = $_[1];  };
+
+# Function: Summary
+# Returns the topic summary, if it exists, formatted in <NDMarkup>.
+sub Summary
+    {  return $_[0]->[SUMMARY];  };
+
+# Function: Body
+# Returns the topic's body, formatted in <NDMarkup>.  May be undef.
+sub Body
+    {  return $_[0]->[BODY];  };
+
+# Function: SetBody
+# Replaces the topic's body, formatted in <NDMarkup>.  May be undef.
+sub SetBody #(body)
+    {
+    my ($self, $body) = @_;
+    $self->[BODY] = $body;
+    };
+
+# Function: LineNumber
+# Returns the line the topic appears at in the file.
+sub LineNumber
+    {  return $_[0]->[LINE_NUMBER];  };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Project.pm b/docs/tool/Modules/NaturalDocs/Project.pm
new file mode 100644
index 00000000..e4d94bad
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Project.pm
@@ -0,0 +1,1402 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Project
+#
+###############################################################################
+#
+#   A package that manages information about the files in the source tree, as well as the list of files that have to be parsed
+#   and built.
+#
+#   Usage and Dependencies:
+#
+#       - All the <Config and Data File Functions> are available immediately, except for the status functions.
+#
+#       - <ReparseEverything()> and <RebuildEverything()> are available immediately, because they may need to be called
+#         after <LoadConfigFileInfo()> but before <LoadSourceFileInfo()>.
+#
+#       - Prior to <LoadConfigFileInfo()>, <NaturalDocs::Settings> must be initialized.
+#
+#       - After <LoadConfigFileInfo()>, the status <Config and Data File Functions> are available as well.
+#
+#       - Prior to <LoadSourceFileInfo()>, <NaturalDocs::Settings> and <NaturalDocs::Languages> must be initialized.
+#
+#       - After <LoadSourceFileInfo()>, the rest of the <Source File Functions> are available.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use NaturalDocs::Project::SourceFile;
+use NaturalDocs::Project::ImageFile;
+
+use strict;
+use integer;
+
+package NaturalDocs::Project;
+
+
+###############################################################################
+# Group: File Handles
+
+#
+#   handle: FH_FILEINFO
+#
+#   The file handle for the file information file, <FileInfo.nd>.
+#
+
+#
+#   handle: FH_CONFIGFILEINFO
+#
+#   The file handle for the config file information file, <ConfigFileInfo.nd>.
+#
+
+#
+#   handle: FH_IMAGEFILE
+#
+#   The file handle for determining the dimensions of image files.
+#
+
+
+
+###############################################################################
+# Group: Source File Variables
+
+
+#
+#   hash: supportedFiles
+#
+#   A hash of all the supported files in the input directory.  The keys are the <FileNames>, and the values are
+#   <NaturalDocs::Project::SourceFile> objects.
+#
+my %supportedFiles;
+
+#
+#   hash: filesToParse
+#
+#   An existence hash of all the <FileNames> that need to be parsed.
+#
+my %filesToParse;
+
+#
+#   hash: filesToBuild
+#
+#   An existence hash of all the <FileNames> that need to be built.
+#
+my %filesToBuild;
+
+#
+#   hash: filesToPurge
+#
+#   An existence hash of the <FileNames> that had Natural Docs content last time, but now either don't exist or no longer have
+#   content.
+#
+my %filesToPurge;
+
+#
+#   hash: unbuiltFilesWithContent
+#
+#   An existence hash of all the <FileNames> that have Natural Docs content but are not part of <filesToBuild>.
+#
+my %unbuiltFilesWithContent;
+
+
+# bool: reparseEverything
+# Whether all the source files need to be reparsed.
+my $reparseEverything;
+
+# bool: rebuildEverything
+# Whether all the source files need to be rebuilt.
+my $rebuildEverything;
+
+# hash: mostUsedLanguage
+# The name of the most used language.  Doesn't include text files.
+my $mostUsedLanguage;
+
+
+
+###############################################################################
+# Group: Configuration File Variables
+
+
+#
+#   hash: mainConfigFile
+#
+#   A hash mapping all the main configuration file names without paths to their <FileStatus>.  Prior to <LoadConfigFileInfo()>,
+#   it serves as an existence hashref of the file names.
+#
+my %mainConfigFiles = ( 'Topics.txt' => 1, 'Languages.txt' => 1 );
+
+#
+#   hash: userConfigFiles
+#
+#   A hash mapping all the user configuration file names without paths to their <FileStatus>.  Prior to <LoadConfigFileInfo()>,
+#   it serves as an existence hashref of the file names.
+#
+my %userConfigFiles = ( 'Topics.txt' => 1, 'Languages.txt' => 1, 'Menu.txt' => 1 );
+
+
+
+
+###############################################################################
+# Group: Image File Variables
+
+
+#
+#   hash: imageFileExtensions
+#
+#   An existence hash of all the file extensions for images.  Extensions are in all lowercase.
+#
+my %imageFileExtensions = ( 'jpg' => 1, 'jpeg' => 1, 'gif' => 1, 'png' => 1, 'bmp' => 1 );
+
+
+#
+#   hash: imageFiles
+#
+#   A hash of all the image files in the project.  The keys are the <FileNames> and the values are
+#   <NaturalDocs::Project::ImageFiles>.
+#
+my %imageFiles;
+
+
+#
+#   hash: imageFilesToUpdate
+#
+#   An existence hash of all the image <FileNames> that need to be updated, either because they changed or they're new to the
+#   project.
+#
+my %imageFilesToUpdate;
+
+
+#
+#   hash: imageFilesToPurge
+#
+#   An existence hash of all the image <FileNames> that need to be purged, either because the files no longer exist or because
+#   they are no longer used.
+#
+my %imageFilesToPurge;
+
+
+#
+#   hash: insensitiveImageFiles
+#
+#   A hash that maps all lowercase image <FileNames> to their proper case as it would appear in <imageFiles>.  Used for
+#   case insensitivity, obviously.
+#
+#   You can't just use all lowercase in <imageFiles> because both Linux and HTTP are case sensitive, so the original case must
+#   be preserved.  We also want to allow separate entries for files that differ based only on case, so it goes to <imageFiles> first
+#   where they can be distinguished and here only if there's no match.  Ties are broken by whichever is lower with cmp, because
+#   it has to resolve consistently on all runs of the program.
+#
+my %insensitiveImageFiles;
+
+
+
+###############################################################################
+# Group: Files
+
+
+#
+#   File: FileInfo.nd
+#
+#   An index of the state of the files as of the last parse.  Used to determine if files were added, deleted, or changed.
+#
+#   Format:
+#
+#       The format is a text file.
+#
+#       > [VersionInt: app version]
+#
+#       The beginning of the file is the <VersionInt> it was generated with.
+#
+#       > [most used language name]
+#
+#       Next is the name of the most used language in the source tree.  Does not include text files.
+#
+#       Each following line is
+#
+#       > [file name] tab [last modification time] tab [has ND content (0 or 1)] tab [default menu title] \n
+#
+#   Revisions:
+#
+#       1.3:
+#
+#           - The line following the <VersionInt>, which was previously the last modification time of <Menu.txt>, was changed to
+#             the name of the most used language.
+#
+#       1.16:
+#
+#           - File names are now absolute.  Prior to 1.16, they were relative to the input directory since only one was allowed.
+#
+#       1.14:
+#
+#           - The file was renamed from NaturalDocs.files to FileInfo.nd and moved into the Data subdirectory.
+#
+#       0.95:
+#
+#           - The file version was changed to match the program version.  Prior to 0.95, the version line was 1.  Test for "1" instead
+#             of "1.0" to distinguish.
+#
+
+
+#
+#   File: ConfigFileInfo.nd
+#
+#   An index of the state of the config files as of the last parse.
+#
+#   Format:
+#
+#       > [BINARY_FORMAT]
+#       > [VersionInt: app version]
+#
+#       First is the standard <BINARY_FORMAT> <VersionInt> header.
+#
+#       > [UInt32: last modification time of menu]
+#       > [UInt32: last modification of main topics file]
+#       > [UInt32: last modification of user topics file]
+#       > [UInt32: last modification of main languages file]
+#       > [UInt32: last modification of user languages file]
+#
+#       Next are the last modification times of various configuration files as UInt32s in the standard Unix format.
+#
+#
+#   Revisions:
+#
+#       1.3:
+#
+#           - The file was added to Natural Docs.  Previously the last modification of <Menu.txt> was stored in <FileInfo.nd>, and
+#             <Topics.txt> and <Languages.txt> didn't exist.
+#
+
+
+#
+#   File: ImageFileInfo.nd
+#
+#   An index of the state of the image files as of the last parse.
+#
+#   Format:
+#
+#       > [Standard Binary Header]
+#
+#       First is the standard binary file header as defined by <NaturalDocs::BinaryFile>.
+#
+#       > [AString16: file name or undef]
+#       > [UInt32: last modification time]
+#       > [UInt8: was used]
+#
+#       This section is repeated until the file name is null.  The last modification times are UInt32s in the standard Unix format.
+#
+#
+#   Revisions:
+#
+#       1.4:
+#
+#           - The file was added to Natural Docs.
+#
+
+
+
+###############################################################################
+# Group: File Functions
+
+#
+#   Function: LoadSourceFileInfo
+#
+#   Loads the project file from disk and compares it against the files in the input directory.  Project is loaded from
+#   <FileInfo.nd>.  New and changed files will be added to <FilesToParse()>, and if they have content,
+#   <FilesToBuild()>.
+#
+#   Will call <NaturalDocs::Languages->OnMostUsedLanguageKnown()> if <MostUsedLanguage()> changes.
+#
+#   Returns:
+#
+#       Returns whether the project was changed in any way.
+#
+sub LoadSourceFileInfo
+    {
+    my ($self) = @_;
+
+    $self->GetAllSupportedFiles();
+    NaturalDocs::Languages->OnMostUsedLanguageKnown();
+
+    my $fileIsOkay;
+    my $version;
+    my $hasChanged;
+
+    if (open(FH_FILEINFO, '<' . $self->DataFile('FileInfo.nd')))
+        {
+        # Check if the file is in the right format.
+        $version = NaturalDocs::Version->FromTextFile(\*FH_FILEINFO);
+
+        # The project file need to be rebuilt for 1.16.  The source files need to be reparsed and the output files rebuilt for 1.4.
+        # We'll tolerate the difference between 1.16 and 1.3 in the loader.
+
+        if (NaturalDocs::Version->CheckFileFormat( $version, NaturalDocs::Version->FromString('1.16') ))
+            {
+            $fileIsOkay = 1;
+
+            if (!NaturalDocs::Version->CheckFileFormat( $version, NaturalDocs::Version->FromString('1.4') ))
+                {
+                $reparseEverything = 1;
+                $rebuildEverything = 1;
+                $hasChanged = 1;
+                };
+            }
+        else
+            {
+            close(FH_FILEINFO);
+            $hasChanged = 1;
+            };
+        };
+
+
+    if ($fileIsOkay)
+        {
+        my %indexedFiles;
+
+
+        my $line = <FH_FILEINFO>;
+        ::XChomp(\$line);
+
+        # Prior to 1.3 it was the last modification time of Menu.txt, which we ignore and treat as though the most used language
+        # changed.  Prior to 1.32 the settings didn't transfer over correctly to Menu.txt so we need to behave that way again.
+        if ($version < NaturalDocs::Version->FromString('1.32') || lc($mostUsedLanguage) ne lc($line))
+            {
+            $reparseEverything = 1;
+            NaturalDocs::SymbolTable->RebuildAllIndexes();
+            };
+
+
+        # Parse the rest of the file.
+
+        while ($line = <FH_FILEINFO>)
+            {
+            ::XChomp(\$line);
+            my ($file, $modification, $hasContent, $menuTitle) = split(/\t/, $line, 4);
+
+            # If the file no longer exists...
+            if (!exists $supportedFiles{$file})
+                {
+                if ($hasContent)
+                    {  $filesToPurge{$file} = 1;  };
+
+                $hasChanged = 1;
+                }
+
+            # If the file still exists...
+            else
+                {
+                $indexedFiles{$file} = 1;
+
+                # If the file changed...
+                if ($supportedFiles{$file}->LastModified() != $modification)
+                    {
+                    $supportedFiles{$file}->SetStatus(::FILE_CHANGED());
+                    $filesToParse{$file} = 1;
+
+                    # If the file loses its content, this will be removed by SetHasContent().
+                    if ($hasContent)
+                        {  $filesToBuild{$file} = 1;  };
+
+                    $hasChanged = 1;
+                    }
+
+                # If the file has not changed...
+                else
+                    {
+                    my $status;
+
+                    if ($rebuildEverything && $hasContent)
+                        {
+                        $status = ::FILE_CHANGED();
+
+                        # If the file loses its content, this will be removed by SetHasContent().
+                        $filesToBuild{$file} = 1;
+                        $hasChanged = 1;
+                        }
+                    else
+                        {
+                        $status = ::FILE_SAME();
+
+                        if ($hasContent)
+                            {  $unbuiltFilesWithContent{$file} = 1;  };
+                        };
+
+                    if ($reparseEverything)
+                        {
+                        $status = ::FILE_CHANGED();
+
+                        $filesToParse{$file} = 1;
+                        $hasChanged = 1;
+                        };
+
+                    $supportedFiles{$file}->SetStatus($status);
+                    };
+
+                $supportedFiles{$file}->SetHasContent($hasContent);
+                $supportedFiles{$file}->SetDefaultMenuTitle($menuTitle);
+                };
+            };
+
+        close(FH_FILEINFO);
+
+
+        # Check for added files.
+
+        if (scalar keys %supportedFiles > scalar keys %indexedFiles)
+            {
+            foreach my $file (keys %supportedFiles)
+                {
+                if (!exists $indexedFiles{$file})
+                    {
+                    $supportedFiles{$file}->SetStatus(::FILE_NEW());
+                    $supportedFiles{$file}->SetDefaultMenuTitle($file);
+                    $supportedFiles{$file}->SetHasContent(undef);
+                    $filesToParse{$file} = 1;
+                    # It will be added to filesToBuild if HasContent gets set to true when it's parsed.
+                    $hasChanged = 1;
+                    };
+                };
+            };
+        }
+
+    # If something's wrong with FileInfo.nd, everything is new.
+    else
+        {
+        foreach my $file (keys %supportedFiles)
+            {
+            $supportedFiles{$file}->SetStatus(::FILE_NEW());
+            $supportedFiles{$file}->SetDefaultMenuTitle($file);
+            $supportedFiles{$file}->SetHasContent(undef);
+            $filesToParse{$file} = 1;
+            # It will be added to filesToBuild if HasContent gets set to true when it's parsed.
+            };
+
+        $hasChanged = 1;
+        };
+
+
+    # There are other side effects, so we need to call this.
+    if ($rebuildEverything)
+        {  $self->RebuildEverything();  };
+
+
+    return $hasChanged;
+    };
+
+
+#
+#   Function: SaveSourceFileInfo
+#
+#   Saves the source file info to disk.  Everything is saved in <FileInfo.nd>.
+#
+sub SaveSourceFileInfo
+    {
+    my ($self) = @_;
+
+    open(FH_FILEINFO, '>' . $self->DataFile('FileInfo.nd'))
+        or die "Couldn't save project file " . $self->DataFile('FileInfo.nd') . "\n";
+
+    NaturalDocs::Version->ToTextFile(\*FH_FILEINFO, NaturalDocs::Settings->AppVersion());
+
+    print FH_FILEINFO $mostUsedLanguage . "\n";
+
+    while (my ($fileName, $file) = each %supportedFiles)
+        {
+        print FH_FILEINFO $fileName . "\t"
+                              . $file->LastModified() . "\t"
+                              . ($file->HasContent() || '0') . "\t"
+                              . $file->DefaultMenuTitle() . "\n";
+        };
+
+    close(FH_FILEINFO);
+    };
+
+
+#
+#   Function: LoadConfigFileInfo
+#
+#   Loads the config file info from disk.
+#
+sub LoadConfigFileInfo
+    {
+    my ($self) = @_;
+
+    my $fileIsOkay;
+    my $version;
+    my $fileName = NaturalDocs::Project->DataFile('ConfigFileInfo.nd');
+
+    if (open(FH_CONFIGFILEINFO, '<' . $fileName))
+        {
+        # See if it's binary.
+        binmode(FH_CONFIGFILEINFO);
+
+        my $firstChar;
+        read(FH_CONFIGFILEINFO, $firstChar, 1);
+
+        if ($firstChar == ::BINARY_FORMAT())
+            {
+            $version = NaturalDocs::Version->FromBinaryFile(\*FH_CONFIGFILEINFO);
+
+            # It hasn't changed since being introduced.
+
+            if (NaturalDocs::Version->CheckFileFormat($version))
+                {  $fileIsOkay = 1;  }
+            else
+                {  close(FH_CONFIGFILEINFO);  };
+            }
+
+        else # it's not in binary
+            {  close(FH_CONFIGFILEINFO);  };
+        };
+
+    my @configFiles = ( $self->UserConfigFile('Menu.txt'), \$userConfigFiles{'Menu.txt'},
+                                 $self->MainConfigFile('Topics.txt'), \$mainConfigFiles{'Topics.txt'},
+                                 $self->UserConfigFile('Topics.txt'), \$userConfigFiles{'Topics.txt'},
+                                 $self->MainConfigFile('Languages.txt'), \$mainConfigFiles{'Languages.txt'},
+                                 $self->UserConfigFile('Languages.txt'), \$userConfigFiles{'Languages.txt'} );
+
+    if ($fileIsOkay)
+        {
+        my $raw;
+
+        read(FH_CONFIGFILEINFO, $raw, 20);
+        my @configFileDates = unpack('NNNNN', $raw);
+
+        while (scalar @configFiles)
+            {
+            my $file = shift @configFiles;
+            my $fileStatus = shift @configFiles;
+            my $fileDate = shift @configFileDates;
+
+            if (-e $file)
+                {
+                if ($fileDate == (stat($file))[9])
+                    {  $$fileStatus = ::FILE_SAME();  }
+                else
+                    {  $$fileStatus = ::FILE_CHANGED();  };
+                }
+            else
+                {  $$fileStatus = ::FILE_DOESNTEXIST();  };
+            };
+
+        close(FH_CONFIGFILEINFO);
+        }
+    else # !$fileIsOkay
+        {
+        while (scalar @configFiles)
+            {
+            my $file = shift @configFiles;
+            my $fileStatus = shift @configFiles;
+
+            if (-e $file)
+                {  $$fileStatus = ::FILE_CHANGED();  }
+            else
+                {  $$fileStatus = ::FILE_DOESNTEXIST();  };
+            };
+        };
+
+    if ($userConfigFiles{'Menu.txt'} == ::FILE_SAME() && $rebuildEverything)
+        {  $userConfigFiles{'Menu.txt'} = ::FILE_CHANGED();  };
+    };
+
+
+#
+#   Function: SaveConfigFileInfo
+#
+#   Saves the config file info to disk.  You *must* save all other config files first, such as <Menu.txt> and <Topics.txt>.
+#
+sub SaveConfigFileInfo
+    {
+    my ($self) = @_;
+
+    open (FH_CONFIGFILEINFO, '>' . NaturalDocs::Project->DataFile('ConfigFileInfo.nd'))
+        or die "Couldn't save " . NaturalDocs::Project->DataFile('ConfigFileInfo.nd') . ".\n";
+
+    binmode(FH_CONFIGFILEINFO);
+
+    print FH_CONFIGFILEINFO '' . ::BINARY_FORMAT();
+
+    NaturalDocs::Version->ToBinaryFile(\*FH_CONFIGFILEINFO, NaturalDocs::Settings->AppVersion());
+
+    print FH_CONFIGFILEINFO pack('NNNNN', (stat($self->UserConfigFile('Menu.txt')))[9],
+                                                                (stat($self->MainConfigFile('Topics.txt')))[9],
+                                                                (stat($self->UserConfigFile('Topics.txt')))[9],
+                                                                (stat($self->MainConfigFile('Languages.txt')))[9],
+                                                                (stat($self->UserConfigFile('Languages.txt')))[9] );
+
+    close(FH_CONFIGFILEINFO);
+    };
+
+
+#
+#   Function: LoadImageFileInfo
+#
+#   Loads the image file info from disk.
+#
+sub LoadImageFileInfo
+    {
+    my ($self) = @_;
+
+    my $version = NaturalDocs::BinaryFile->OpenForReading( NaturalDocs::Project->DataFile('ImageFileInfo.nd') );
+    my $fileIsOkay;
+
+    if (defined $version)
+        {
+        # It hasn't changed since being introduced.
+
+        if (NaturalDocs::Version->CheckFileFormat($version))
+            {  $fileIsOkay = 1;  }
+        else
+            {  NaturalDocs::BinaryFile->Close();  };
+        };
+
+    if ($fileIsOkay)
+        {
+        # [AString16: file name or undef]
+
+        while (my $imageFile = NaturalDocs::BinaryFile->GetAString16())
+            {
+            # [UInt32: last modified]
+            # [UInt8: was used]
+
+            my $lastModified = NaturalDocs::BinaryFile->GetUInt32();
+            my $wasUsed = NaturalDocs::BinaryFile->GetUInt8();
+
+            my $imageFileObject = $imageFiles{$imageFile};
+
+            # If there's an image file in ImageFileInfo.nd that no longer exists...
+            if (!$imageFileObject)
+                {
+                $imageFileObject = NaturalDocs::Project::ImageFile->New($lastModified, ::FILE_DOESNTEXIST(), $wasUsed);
+                $imageFiles{$imageFile} = $imageFileObject;
+
+                if ($wasUsed)
+                    {  $imageFilesToPurge{$imageFile} = 1;  };
+                }
+            else
+                {
+                $imageFileObject->SetWasUsed($wasUsed);
+
+                # This will be removed if it gets any references.
+                if ($wasUsed)
+                    {  $imageFilesToPurge{$imageFile} = 1;  };
+
+                if ($imageFileObject->LastModified() == $lastModified && !$rebuildEverything)
+                    {  $imageFileObject->SetStatus(::FILE_SAME());  }
+                else
+                    {  $imageFileObject->SetStatus(::FILE_CHANGED());  };
+                };
+            };
+
+        NaturalDocs::BinaryFile->Close();
+        }
+
+    else # !$fileIsOkay
+        {
+        $self->RebuildEverything();
+        };
+    };
+
+
+#
+#   Function: SaveImageFileInfo
+#
+#   Saves the image file info to disk.
+#
+sub SaveImageFileInfo
+    {
+    my $self = shift;
+
+    NaturalDocs::BinaryFile->OpenForWriting( NaturalDocs::Project->DataFile('ImageFileInfo.nd') );
+
+    while (my ($imageFile, $imageFileInfo) = each %imageFiles)
+        {
+        if ($imageFileInfo->Status() != ::FILE_DOESNTEXIST())
+            {
+            # [AString16: file name or undef]
+            # [UInt32: last modification time]
+            # [UInt8: was used]
+
+            NaturalDocs::BinaryFile->WriteAString16($imageFile);
+            NaturalDocs::BinaryFile->WriteUInt32($imageFileInfo->LastModified());
+            NaturalDocs::BinaryFile->WriteUInt8( ($imageFileInfo->ReferenceCount() > 0 ? 1 : 0) );
+            };
+        };
+
+    NaturalDocs::BinaryFile->WriteAString16(undef);
+    NaturalDocs::BinaryFile->Close();
+    };
+
+
+#
+#   Function: MigrateOldFiles
+#
+#   If the project uses the old file names used prior to 1.14, it converts them to the new file names.
+#
+sub MigrateOldFiles
+    {
+    my ($self) = @_;
+
+    my $projectDirectory = NaturalDocs::Settings->ProjectDirectory();
+
+    # We use the menu file as a test to see if we're using the new format.
+    if (-e NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs_Menu.txt'))
+        {
+        # The Data subdirectory would have been created by NaturalDocs::Settings.
+
+        rename( NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs_Menu.txt'), $self->UserConfigFile('Menu.txt') );
+
+        if (-e NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.sym'))
+            {  rename( NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.sym'), $self->DataFile('SymbolTable.nd') );  };
+
+        if (-e NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.files'))
+            {  rename( NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.files'), $self->DataFile('FileInfo.nd') );  };
+
+        if (-e NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.m'))
+            {  rename( NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.m'), $self->DataFile('PreviousMenuState.nd') );  };
+        };
+    };
+
+
+
+###############################################################################
+# Group: Config and Data File Functions
+
+
+#
+#   Function: MainConfigFile
+#
+#   Returns the full path to the passed main configuration file.  Pass the file name only.
+#
+sub MainConfigFile #(string file)
+    {
+    my ($self, $file) = @_;
+    return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ConfigDirectory(), $file );
+    };
+
+#
+#   Function: MainConfigFileStatus
+#
+#   Returns the <FileStatus> of the passed main configuration file.  Pass the file name only.
+#
+sub MainConfigFileStatus #(string file)
+    {
+    my ($self, $file) = @_;
+    return $mainConfigFiles{$file};
+    };
+
+#
+#   Function: UserConfigFile
+#
+#   Returns the full path to the passed user configuration file.  Pass the file name only.
+#
+sub UserConfigFile #(string file)
+    {
+    my ($self, $file) = @_;
+    return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDirectory(), $file );
+    };
+
+#
+#   Function: UserConfigFileStatus
+#
+#   Returns the <FileStatus> of the passed user configuration file.  Pass the file name only.
+#
+sub UserConfigFileStatus #(string file)
+    {
+    my ($self, $file) = @_;
+    return $userConfigFiles{$file};
+    };
+
+#
+#   Function: DataFile
+#
+#   Returns the full path to the passed data file.  Pass the file name only.
+#
+sub DataFile #(string file)
+    {
+    my ($self, $file) = @_;
+    return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDataDirectory(), $file );
+    };
+
+
+
+
+###############################################################################
+# Group: Source File Functions
+
+
+# Function: FilesToParse
+# Returns an existence hashref of the <FileNames> to parse.  This is not a copy of the data, so don't change it.
+sub FilesToParse
+    {  return \%filesToParse;  };
+
+# Function: FilesToBuild
+# Returns an existence hashref of the <FileNames> to build.  This is not a copy of the data, so don't change it.
+sub FilesToBuild
+    {  return \%filesToBuild;  };
+
+# Function: FilesToPurge
+# Returns an existence hashref of the <FileNames> that had content last time, but now either don't anymore or were deleted.
+# This is not a copy of the data, so don't change it.
+sub FilesToPurge
+    {  return \%filesToPurge;  };
+
+#
+#   Function: RebuildFile
+#
+#   Adds the file to the list of files to build.  This function will automatically filter out files that don't have Natural Docs content and
+#   files that are part of <FilesToPurge()>.  If this gets called on a file and that file later gets Natural Docs content, it will be added.
+#
+#   Parameters:
+#
+#       file - The <FileName> to build or rebuild.
+#
+sub RebuildFile #(file)
+    {
+    my ($self, $file) = @_;
+
+    # We don't want to add it to the build list if it doesn't exist, doesn't have Natural Docs content, or it's going to be purged.
+    # If it wasn't parsed yet and will later be found to have ND content, it will be added by SetHasContent().
+    if (exists $supportedFiles{$file} && !exists $filesToPurge{$file} && $supportedFiles{$file}->HasContent())
+        {
+        $filesToBuild{$file} = 1;
+
+        if (exists $unbuiltFilesWithContent{$file})
+            {  delete $unbuiltFilesWithContent{$file};  };
+        };
+    };
+
+
+#
+#   Function: ReparseEverything
+#
+#   Adds all supported files to the list of files to parse.  This does not necessarily mean these files are going to be rebuilt.
+#
+sub ReparseEverything
+    {
+    my ($self) = @_;
+
+    if (!$reparseEverything)
+        {
+        foreach my $file (keys %supportedFiles)
+            {
+            $filesToParse{$file} = 1;
+            };
+
+        $reparseEverything = 1;
+        };
+    };
+
+
+#
+#   Function: RebuildEverything
+#
+#   Adds all supported files to the list of files to build.  This does not necessarily mean these files are going to be reparsed.
+#
+sub RebuildEverything
+    {
+    my ($self) = @_;
+
+    foreach my $file (keys %unbuiltFilesWithContent)
+        {
+        $filesToBuild{$file} = 1;
+        };
+
+    %unbuiltFilesWithContent = ( );
+    $rebuildEverything = 1;
+
+    NaturalDocs::SymbolTable->RebuildAllIndexes();
+
+    if ($userConfigFiles{'Menu.txt'} == ::FILE_SAME())
+        {  $userConfigFiles{'Menu.txt'} = ::FILE_CHANGED();  };
+
+    while (my ($imageFile, $imageObject) = each %imageFiles)
+        {
+        if ($imageObject->ReferenceCount())
+            {  $imageFilesToUpdate{$imageFile} = 1;  };
+        };
+    };
+
+
+# Function: UnbuiltFilesWithContent
+# Returns an existence hashref of the <FileNames> that have Natural Docs content but are not part of <FilesToBuild()>.  This is
+# not a copy of the data so don't change it.
+sub UnbuiltFilesWithContent
+    {  return \%unbuiltFilesWithContent;  };
+
+# Function: FilesWithContent
+# Returns and existence hashref of the <FileNames> that have Natural Docs content.
+sub FilesWithContent
+    {
+    # Don't keep this one internally, but there's an easy way to make it.
+    return { %filesToBuild, %unbuiltFilesWithContent };
+    };
+
+
+#
+#   Function: HasContent
+#
+#   Returns whether the <FileName> contains Natural Docs content.
+#
+sub HasContent #(file)
+    {
+    my ($self, $file) = @_;
+
+    if (exists $supportedFiles{$file})
+        {  return $supportedFiles{$file}->HasContent();  }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: SetHasContent
+#
+#   Sets whether the <FileName> has Natural Docs content or not.
+#
+sub SetHasContent #(file, hasContent)
+    {
+    my ($self, $file, $hasContent) = @_;
+
+    if (exists $supportedFiles{$file} && $supportedFiles{$file}->HasContent() != $hasContent)
+        {
+        # If the file now has content...
+        if ($hasContent)
+            {
+            $filesToBuild{$file} = 1;
+            }
+
+        # If the file's content has been removed...
+        else
+            {
+            delete $filesToBuild{$file};  # may not be there
+            $filesToPurge{$file} = 1;
+            };
+
+        $supportedFiles{$file}->SetHasContent($hasContent);
+        };
+    };
+
+
+#
+#   Function: StatusOf
+#
+#   Returns the <FileStatus> of the passed <FileName>.
+#
+sub StatusOf #(file)
+    {
+    my ($self, $file) = @_;
+
+    if (exists $supportedFiles{$file})
+        {  return $supportedFiles{$file}->Status();  }
+    else
+        {  return ::FILE_DOESNTEXIST();  };
+    };
+
+
+#
+#   Function: DefaultMenuTitleOf
+#
+#   Returns the default menu title of the <FileName>.  If one isn't specified, it returns the <FileName>.
+#
+sub DefaultMenuTitleOf #(file)
+    {
+    my ($self, $file) = @_;
+
+    if (exists $supportedFiles{$file})
+        {  return $supportedFiles{$file}->DefaultMenuTitle();  }
+    else
+        {  return $file;  };
+    };
+
+
+#
+#   Function: SetDefaultMenuTitle
+#
+#   Sets the <FileName's> default menu title.
+#
+sub SetDefaultMenuTitle #(file, menuTitle)
+    {
+    my ($self, $file, $menuTitle) = @_;
+
+    if (exists $supportedFiles{$file} && $supportedFiles{$file}->DefaultMenuTitle() ne $menuTitle)
+        {
+        $supportedFiles{$file}->SetDefaultMenuTitle($menuTitle);
+        NaturalDocs::Menu->OnDefaultTitleChange($file);
+        };
+    };
+
+
+#
+#   Function: MostUsedLanguage
+#
+#   Returns the name of the most used language in the source trees.  Does not include text files.
+#
+sub MostUsedLanguage
+    {  return $mostUsedLanguage;  };
+
+
+
+
+###############################################################################
+# Group: Image File Functions
+
+
+#
+#   Function: ImageFileExists
+#   Returns whether the passed image file exists.
+#
+sub ImageFileExists #(FileName file) => bool
+    {
+    my ($self, $file) = @_;
+
+    if (!exists $imageFiles{$file})
+        {  $file = $insensitiveImageFiles{lc($file)};  };
+
+    return (exists $imageFiles{$file} && $imageFiles{$file}->Status() != ::FILE_DOESNTEXIST());
+    };
+
+
+#
+#   Function: ImageFileDimensions
+#   Returns the dimensions of the passed image file as the array ( width, height ).  Returns them both as undef if it cannot be
+#   determined.
+#
+sub ImageFileDimensions #(FileName file) => (int, int)
+    {
+    my ($self, $file) = @_;
+
+    if (!exists $imageFiles{$file})
+        {  $file = $insensitiveImageFiles{lc($file)};  };
+
+    my $object = $imageFiles{$file};
+    if (!$object)
+        {  die "Tried to get the dimensions of an image that doesn't exist.";  };
+
+    if ($object->Width() == -1)
+        {  $self->DetermineImageDimensions($file);  };
+
+    return ($object->Width(), $object->Height());
+    };
+
+
+#
+#   Function: ImageFileCapitalization
+#   Returns the properly capitalized version of the passed image <FileName>.  Image file paths are treated as case insensitive
+#   regardless of whether the underlying operating system is or not, so we have to make sure the final version matches the
+#   capitalization of the actual file.
+#
+sub ImageFileCapitalization #(FileName file) => FileName
+    {
+    my ($self, $file) = @_;
+
+    if (exists $imageFiles{$file})
+        {  return $file;  }
+    elsif (exists $insensitiveImageFiles{lc($file)})
+        {  return $insensitiveImageFiles{lc($file)};  }
+    else
+        {  die "Tried to get the capitalization of an image file that doesn't exist.";  };
+    };
+
+
+#
+#   Function: AddImageFileReference
+#   Adds a reference to the passed image <FileName>.
+#
+sub AddImageFileReference #(FileName imageFile)
+    {
+    my ($self, $imageFile) = @_;
+
+    if (!exists $imageFiles{$imageFile})
+        {  $imageFile = $insensitiveImageFiles{lc($imageFile)};  };
+
+    my $imageFileInfo = $imageFiles{$imageFile};
+
+    if ($imageFileInfo == undef || $imageFileInfo->Status() == ::FILE_DOESNTEXIST())
+        {  die "Tried to add a reference to a non-existant image file.";  };
+
+    if ($imageFileInfo->AddReference() == 1)
+        {
+        delete $imageFilesToPurge{$imageFile};
+
+        if (!$imageFileInfo->WasUsed() ||
+            $imageFileInfo->Status() == ::FILE_NEW() ||
+            $imageFileInfo->Status() == ::FILE_CHANGED())
+            {  $imageFilesToUpdate{$imageFile} = 1;  };
+        };
+    };
+
+
+#
+#   Function: DeleteImageFileReference
+#   Deletes a reference from the passed image <FileName>.
+#
+sub DeleteImageFileReference #(FileName imageFile)
+    {
+    my ($self, $imageFile) = @_;
+
+    if (!exists $imageFiles{$imageFile})
+        {  $imageFile = $insensitiveImageFiles{lc($imageFile)};  };
+
+    if (!exists $imageFiles{$imageFile})
+        {  die "Tried to delete a reference to a non-existant image file.";  };
+
+    if ($imageFiles{$imageFile}->DeleteReference() == 0)
+        {
+        delete $imageFilesToUpdate{$imageFile};
+
+        if ($imageFiles{$imageFile}->WasUsed())
+            {  $imageFilesToPurge{$imageFile} = 1;  };
+        };
+    };
+
+
+#
+#   Function: ImageFilesToUpdate
+#   Returns an existence hashref of image <FileNames> that need to be updated.  *Do not change.*
+#
+sub ImageFilesToUpdate
+    {  return \%imageFilesToUpdate;  };
+
+
+#
+#   Function: ImageFilesToPurge
+#   Returns an existence hashref of image <FileNames> that need to be updated.  *Do not change.*
+#
+sub ImageFilesToPurge
+    {  return \%imageFilesToPurge;  };
+
+
+
+###############################################################################
+# Group: Support Functions
+
+#
+#   Function: GetAllSupportedFiles
+#
+#   Gets all the supported files in the passed directory and its subdirectories and puts them into <supportedFiles>.  The only
+#   attribute that will be set is <NaturalDocs::Project::SourceFile->LastModified()>.  Also sets <mostUsedLanguage>.
+#
+sub GetAllSupportedFiles
+    {
+    my ($self) = @_;
+
+    my @directories = @{NaturalDocs::Settings->InputDirectories()};
+    my $isCaseSensitive = NaturalDocs::File->IsCaseSensitive();
+
+    # Keys are language names, values are counts.
+    my %languageCounts;
+
+
+    # Make an existence hash of excluded directories.
+
+    my %excludedDirectories;
+    my $excludedDirectoryArrayRef = NaturalDocs::Settings->ExcludedInputDirectories();
+
+    foreach my $excludedDirectory (@$excludedDirectoryArrayRef)
+        {
+        if ($isCaseSensitive)
+            {  $excludedDirectories{$excludedDirectory} = 1;  }
+        else
+            {  $excludedDirectories{lc($excludedDirectory)} = 1;  };
+        };
+
+
+    my $imagesOnly;
+    my $language;
+
+    while (scalar @directories)
+        {
+        my $directory = pop @directories;
+
+        opendir DIRECTORYHANDLE, $directory;
+        my @entries = readdir DIRECTORYHANDLE;
+        closedir DIRECTORYHANDLE;
+
+        @entries = NaturalDocs::File->NoUpwards(@entries);
+
+        foreach my $entry (@entries)
+            {
+            my $fullEntry = NaturalDocs::File->JoinPaths($directory, $entry);
+
+            # If an entry is a directory, recurse.
+            if (-d $fullEntry)
+                {
+                # Join again with the noFile flag set in case the platform handles them differently.
+                $fullEntry = NaturalDocs::File->JoinPaths($directory, $entry, 1);
+
+                if ($isCaseSensitive)
+                    {
+                    if (!exists $excludedDirectories{$fullEntry})
+                        {  push @directories, $fullEntry;  };
+                    }
+                else
+                    {
+                    if (!exists $excludedDirectories{lc($fullEntry)})
+                        {  push @directories, $fullEntry;  };
+                    };
+                }
+
+            # Otherwise add it if it's a supported extension.
+            else
+                {
+                my $extension = NaturalDocs::File->ExtensionOf($entry);
+
+                if (exists $imageFileExtensions{lc($extension)})
+                    {
+                    my $fileObject = NaturalDocs::Project::ImageFile->New( (stat($fullEntry))[9], ::FILE_NEW(), 0 );
+                    $imageFiles{$fullEntry} = $fileObject;
+
+                    my $lcFullEntry = lc($fullEntry);
+
+                    if (!exists $insensitiveImageFiles{$lcFullEntry} ||
+                        ($fullEntry cmp $insensitiveImageFiles{$lcFullEntry}) < 0)
+                        {
+                        $insensitiveImageFiles{$lcFullEntry} = $fullEntry;
+                        };
+                    }
+                elsif (!$imagesOnly && ($language = NaturalDocs::Languages->LanguageOf($fullEntry)) )
+                    {
+                    my $fileObject = NaturalDocs::Project::SourceFile->New();
+                    $fileObject->SetLastModified(( stat($fullEntry))[9] );
+                    $supportedFiles{$fullEntry} = $fileObject;
+
+                    $languageCounts{$language->Name()}++;
+                    };
+                };
+            };
+
+
+        # After we run out of source directories, add the image directories.
+
+        if (scalar @directories == 0 && !$imagesOnly)
+            {
+            $imagesOnly = 1;
+            @directories = @{NaturalDocs::Settings->ImageDirectories()};
+            };
+        };
+
+
+    my $topCount = 0;
+
+    while (my ($language, $count) = each %languageCounts)
+        {
+        if ($count > $topCount && $language ne 'Text File')
+            {
+            $topCount = $count;
+            $mostUsedLanguage = $language;
+            };
+        };
+    };
+
+
+#
+#   Function: DetermineImageDimensions
+#
+#   Attempts to determine the dimensions of the passed image and apply them to their object in <imageFiles>.  Will set them to
+#   undef if they can't be determined.
+#
+sub DetermineImageDimensions #(FileName imageFile)
+    {
+    my ($self, $imageFile) = @_;
+
+    my $imageFileObject = $imageFiles{$imageFile};
+    if (!defined $imageFileObject)
+        {  die "Tried to determine image dimensions of a file with no object.";  };
+
+    my $extension = lc( NaturalDocs::File->ExtensionOf($imageFile) );
+    my ($width, $height);
+
+    if ($imageFileExtensions{$extension})
+        {
+        open(FH_IMAGEFILE, '<' . $imageFile)
+            or die 'Could not open ' . $imageFile . "\n";
+        binmode(FH_IMAGEFILE);
+
+        my $raw;
+
+        if ($extension eq 'gif')
+            {
+            read(FH_IMAGEFILE, $raw, 6);
+
+            if ($raw eq 'GIF87a' || $raw eq 'GIF89a')
+                {
+                read(FH_IMAGEFILE, $raw, 4);
+                ($width, $height) = unpack('vv', $raw);
+                };
+            }
+
+        elsif ($extension eq 'png')
+            {
+            read(FH_IMAGEFILE, $raw, 8);
+
+            if ($raw eq "\x89PNG\x0D\x0A\x1A\x0A")
+                {
+                seek(FH_IMAGEFILE, 4, 1);
+                read(FH_IMAGEFILE, $raw, 4);
+
+                if ($raw eq 'IHDR')
+                    {
+                    read(FH_IMAGEFILE, $raw, 8);
+                    ($width, $height) = unpack('NN', $raw);
+                    };
+                };
+            }
+
+        elsif ($extension eq 'bmp')
+            {
+            read(FH_IMAGEFILE, $raw, 2);
+
+            if ($raw eq 'BM')
+                {
+                seek(FH_IMAGEFILE, 16, 1);
+                read(FH_IMAGEFILE, $raw, 8);
+
+                ($width, $height) = unpack('VV', $raw);
+                };
+            }
+
+        elsif ($extension eq 'jpg' || $extension eq 'jpeg')
+            {
+            read(FH_IMAGEFILE, $raw, 2);
+            my $isOkay = ($raw eq "\xFF\xD8");
+
+            while ($isOkay)
+                {
+                read(FH_IMAGEFILE, $raw, 4);
+                my ($marker, $code, $length) = unpack('CCn', $raw);
+
+                $isOkay = ($marker eq 0xFF);
+
+                if ($isOkay)
+                    {
+                    if ($code >= 0xC0 && $code <= 0xC3)
+                        {
+                        read(FH_IMAGEFILE, $raw, 5);
+                        ($height, $width) = unpack('xnn', $raw);
+                        last;
+                        }
+
+                    else
+                        {
+                        $isOkay = seek(FH_IMAGEFILE, $length - 2, 1);
+                        };
+                    };
+                };
+            };
+
+        close(FH_IMAGEFILE);
+        };
+
+
+    # Sanity check the values.  Although images can theoretically be bigger than 5000, most won't.  The worst that happens in this
+    # case is just that they don't get length and width values in the output anyway.
+    if ($width > 0 && $width < 5000 && $height > 0 && $height < 5000)
+        {  $imageFileObject->SetDimensions($width, $height);  }
+    else
+        {  $imageFileObject->SetDimensions(undef, undef);  };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Project/ImageFile.pm b/docs/tool/Modules/NaturalDocs/Project/ImageFile.pm
new file mode 100644
index 00000000..bcf9b556
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Project/ImageFile.pm
@@ -0,0 +1,160 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Project::ImageFile
+#
+###############################################################################
+#
+#   A simple information class about project image files.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Project::ImageFile;
+
+
+
+###############################################################################
+# Group: Implementation
+
+#
+#   Constants: Members
+#
+#   The class is implemented as a blessed arrayref.  The following constants are used as indexes.
+#
+#       LAST_MODIFIED - The integer timestamp of when the file was last modified.
+#       STATUS - <FileStatus> since the last build.
+#       REFERENCE_COUNT - The number of references to the image from the source files.
+#       WAS_USED - Whether the image was used the last time Natural Docs was run.
+#       WIDTH - The image width.  Undef if can't be determined, -1 if haven't attempted to determine yet.
+#       HEIGHT - The image height.  Undef if can't be determined, -1 if haven't attempted to determine yet.
+#
+
+use NaturalDocs::DefineMembers 'LAST_MODIFIED', 'LastModified()', 'SetLastModified()',
+                                                 'STATUS', 'Status()', 'SetStatus()',
+                                                 'REFERENCE_COUNT', 'ReferenceCount()',
+                                                 'WAS_USED', 'WasUsed()', 'SetWasUsed()',
+                                                 'WIDTH', 'Width()',
+                                                 'HEIGHT', 'Height()';
+
+
+#
+#   Topic: WasUsed versus References
+#
+#   <WasUsed()> is a simple true/false that notes whether this image file was used the last time Natural Docs was run.
+#   <ReferenceCount()> is a counter for the number of times it's used *this* run.  As such, it starts at zero regardless of whether
+#   <WasUsed()> is set or not.
+#
+
+
+###############################################################################
+# Group: Functions
+
+#
+#   Function: New
+#
+#   Creates and returns a new file object.
+#
+#   Parameters:
+#
+#       lastModified - The image file's last modification timestamp
+#       status - The <FileStatus>.
+#       wasUsed - Whether this image file was used the *last* time Natural Docs was run.
+#
+sub New #(timestamp lastModified, FileStatus status, bool wasUsed)
+    {
+    my ($package, $lastModified, $status, $width, $height, $wasUsed) = @_;
+
+    my $object = [ ];
+    $object->[LAST_MODIFIED] = $lastModified;
+    $object->[STATUS] = $status;
+    $object->[REFERENCE_COUNT] = 0;
+    $object->[WAS_USED] = $wasUsed;
+    $object->[WIDTH] = -1;
+    $object->[HEIGHT] = -1;
+
+    bless $object, $package;
+
+    return $object;
+    };
+
+
+#
+#   Functions: Member Functions
+#
+#   LastModified - Returns the integer timestamp of when the file was last modified.
+#   SetLastModified - Sets the file's last modification timestamp.
+#   Status - Returns the <FileStatus> since the last build.
+#   SetStatus - Sets the <FileStatus> since the last build.
+#
+
+#
+#   Function: ReferenceCount
+#   Returns the current number of references to this image file during *this* Natural Docs execution.
+#
+
+#
+#   Function: AddReference
+#   Increases the number of references to this image file by one.  Returns the new reference count.
+#
+sub AddReference
+    {
+    my $self = shift;
+
+    $self->[REFERENCE_COUNT]++;
+    return $self->[REFERENCE_COUNT];
+    };
+
+#
+#   Function: DeleteReference
+#   Decreases the number of references to this image file by one.  Returns the new reference count.
+#
+sub DeleteReference
+    {
+    my $self = shift;
+    $self->[REFERENCE_COUNT]--;
+
+    if ($self->[REFERENCE_COUNT] < 0)
+        {  die "Deleted more references to an image file than existed.";  };
+
+    return $self->[REFERENCE_COUNT];
+    };
+
+
+#
+#   Functions: Member Functions
+#
+#   WasUsed - Returns whether this image file was used during the *last* Natural Docs execution.
+#   SetWasUsed - Sets whether this image file was used during the *last* Natural Docs execution.
+#   Width - Returns the width in pixels, undef if it can't be determined, and -1 if determination hasn't been attempted yet.
+#   Height - Returns the width in pixels, undef if it can't be determined, and -1 if determination hasn't been attempted yet.
+#
+
+
+#
+#   Function: SetDimensions
+#   Sets the width and height of the image.  Set to undef if they can't be determined.
+#
+sub SetDimensions #(int width, int height)
+    {
+    my ($self, $width, $height) = @_;
+
+    # If either are undef, both should be undef.  This will also convert zeroes to undef.
+    if (!$width || !$height)
+        {
+        $self->[WIDTH] = undef;
+        $self->[HEIGHT] = undef;
+        }
+    else
+        {
+        $self->[WIDTH] = $width;
+        $self->[HEIGHT] = $height;
+        };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Project/SourceFile.pm b/docs/tool/Modules/NaturalDocs/Project/SourceFile.pm
new file mode 100644
index 00000000..fd649d9c
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Project/SourceFile.pm
@@ -0,0 +1,113 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Project::SourceFile
+#
+###############################################################################
+#
+#   A simple information class about project files.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Project::SourceFile;
+
+
+
+###############################################################################
+# Group: Implementation
+
+#
+#   Constants: Members
+#
+#   The class is implemented as a blessed arrayref.  The following constants are used as indexes.
+#
+#       HAS_CONTENT             - Whether the file contains Natural Docs content or not.
+#       LAST_MODIFIED           - The integer timestamp of when the file was last modified.
+#       STATUS                       - <FileStatus> since the last build.
+#       DEFAULT_MENU_TITLE  - The file's default title in the menu.
+#
+
+# DEPENDENCY: New() depends on its parameter list being in the same order as these constants.  If the order changes, New()
+# needs to be changed.
+use NaturalDocs::DefineMembers 'HAS_CONTENT', 'LAST_MODIFIED', 'STATUS', 'DEFAULT_MENU_TITLE';
+
+
+###############################################################################
+# Group: Functions
+
+#
+#   Function: New
+#
+#   Creates and returns a new file object.
+#
+#   Parameters:
+#
+#       hasContent         - Whether the file contains Natural Docs content or not.
+#       lastModified         - The integer timestamp of when the file was last modified.
+#       status                 - The <FileStatus> since the last build.
+#       defaultMenuTitle  - The file's title in the menu.
+#
+#   Returns:
+#
+#       A reference to the new object.
+#
+sub New #(hasContent, lastModified, status, defaultMenuTitle)
+    {
+    # DEPENDENCY: This function depends on its parameter list being in the same order as the member constants.  If either order
+    # changes, this function needs to be changed.
+
+    my $package = shift;
+
+    my $object = [ @_ ];
+    bless $object, $package;
+
+    return $object;
+    };
+
+# Function: HasContent
+# Returns whether the file contains Natural Docs content or not.
+sub HasContent
+    {  return $_[0]->[HAS_CONTENT];  };
+
+# Function: SetHasContent
+# Sets whether the file contains Natural Docs content or not.
+sub SetHasContent #(hasContent)
+    {  $_[0]->[HAS_CONTENT] = $_[1];  };
+
+# Function: LastModified
+# Returns the integer timestamp of when the file was last modified.
+sub LastModified
+    {  return $_[0]->[LAST_MODIFIED];  };
+
+# Function: SetLastModified
+# Sets the file's last modification timestamp.
+sub SetLastModified #(lastModified)
+    {  $_[0]->[LAST_MODIFIED] = $_[1];  };
+
+# Function: Status
+# Returns the <FileStatus> since the last build.
+sub Status
+    {  return $_[0]->[STATUS];  };
+
+# Function: SetStatus
+# Sets the <FileStatus> since the last build.
+sub SetStatus #(status)
+    {  $_[0]->[STATUS] = $_[1];  };
+
+# Function: DefaultMenuTitle
+# Returns the file's default title on the menu.
+sub DefaultMenuTitle
+    {  return $_[0]->[DEFAULT_MENU_TITLE];  };
+
+# Function: SetDefaultMenuTitle
+# Sets the file's default title on the menu.
+sub SetDefaultMenuTitle #(menuTitle)
+    {  $_[0]->[DEFAULT_MENU_TITLE] = $_[1];  };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/ReferenceString.pm b/docs/tool/Modules/NaturalDocs/ReferenceString.pm
new file mode 100644
index 00000000..a5aeed2f
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/ReferenceString.pm
@@ -0,0 +1,334 @@
+###############################################################################
+#
+#   Package: NaturalDocs::ReferenceString
+#
+###############################################################################
+#
+#   A package to manage <ReferenceString> handling throughout the program.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::ReferenceString;
+
+use vars '@ISA', '@EXPORT';
+@ISA = 'Exporter';
+@EXPORT = ( 'BINARYREF_NOTYPE', 'BINARYREF_NORESOLVINGFLAGS',
+
+                     'REFERENCE_TEXT', 'REFERENCE_CH_CLASS', 'REFERENCE_CH_PARENT',
+
+                     'RESOLVE_RELATIVE', 'RESOLVE_ABSOLUTE', 'RESOLVE_NOPLURAL', 'RESOLVE_NOUSING' );
+
+
+#
+#   Constants: Binary Format Flags
+#
+#   These flags can be combined to specify the format when using <ToBinaryFile()> and <FromBinaryFile()>.  All are exported
+#   by default.
+#
+#   BINARYREF_NOTYPE - Do not include the <ReferenceType>.
+#   BINARYREF_NORESOLVEFLAGS - Do not include the <Resolving Flags>.
+#
+use constant BINARYREF_NOTYPE => 0x01;
+use constant BINARYREF_NORESOLVINGFLAGS => 0x02;
+
+
+#
+#   Constants: ReferenceType
+#
+#   The type of a reference.
+#
+#       REFERENCE_TEXT - The reference appears in the text of the documentation.
+#       REFERENCE_CH_CLASS - A class reference handled by <NaturalDocs::ClassHierarchy>.
+#       REFERENCE_CH_PARENT - A parent class reference handled by <NaturalDocs::ClassHierarchy>.
+#
+#   Dependencies:
+#
+#       - <ToBinaryFile()> and <FromBinaryFile()> require that these values fit into a UInt8, i.e. are <= 255.
+#
+use constant REFERENCE_TEXT => 1;
+use constant REFERENCE_CH_CLASS => 2;
+use constant REFERENCE_CH_PARENT => 3;
+
+
+#
+#   Constants: Resolving Flags
+#
+#   Used to influence the method of resolving references in <NaturalDocs::SymbolTable>.
+#
+#       RESOLVE_RELATIVE - The reference text is truly relative, rather than Natural Docs' semi-relative.
+#       RESOLVE_ABSOLUTE - The reference text is always absolute.  No local or relative references.
+#       RESOLVE_NOPLURAL - The reference text may not be interpreted as a plural, and thus match singular forms as well.
+#       RESOLVE_NOUSING - The reference text may not include "using" statements when being resolved.
+#
+#       If neither <RESOLVE_RELATIVE> or <RESOLVE_ABSOLUTE> is specified, Natural Docs' semi-relative kicks in instead,
+#       which is where links are interpreted as local, then global, then relative.  <RESOLVE_RELATIVE> states that links are
+#       local, then relative, then global.
+#
+#   Dependencies:
+#
+#       - <ToBinaryFile()> and <FromBinaryFile()> require that these values fit into a UInt8, i.e. are <= 255.
+#
+use constant RESOLVE_RELATIVE => 0x01;
+use constant RESOLVE_ABSOLUTE => 0x02;
+use constant RESOLVE_NOPLURAL => 0x04;
+use constant RESOLVE_NOUSING => 0x08;
+
+
+#
+#
+#   Function: MakeFrom
+#
+#   Encodes the passed information as a <ReferenceString>.  The format of the string should be treated as opaque.  However, the
+#   characteristic you can rely on is that the same string will always be made from the same parameters, and thus it's suitable
+#   for comparison and use as hash keys.
+#
+#   Parameters:
+#
+#       type - The <ReferenceType>.
+#       symbol - The <SymbolString> of the reference.
+#       language - The name of the language that defines the file this reference appears in.
+#       scope - The scope <SymbolString> the reference appears in, or undef if none.
+#       using - An arrayref of scope <SymbolStrings> that are also available for checking due to the equivalent a "using" statement,
+#                  or undef if none.
+#       resolvingFlags - The <Resolving Flags> to use with this reference.  They are ignored if the type is <REFERENCE_TEXT>.
+#
+#   Returns:
+#
+#       The encoded <ReferenceString>.
+#
+sub MakeFrom #(ReferenceType type, SymbolString symbol, string language, SymbolString scope, SymbolString[]* using, flags resolvingFlags)
+    {
+    my ($self, $type, $symbol, $language, $scope, $using, $resolvingFlags) = @_;
+
+    if ($type == ::REFERENCE_TEXT() || $resolvingFlags == 0)
+       {  $resolvingFlags = undef;  };
+
+    # The format is [type] 0x1E [resolving flags] 0x1E [symbol] 0x1E [scope] ( 0x1E [using] )*
+    # If there is no scope and/or using, the separator characters still remain.
+
+    # DEPENDENCY: SymbolString->FromText() removed all 0x1E characters.
+    # DEPENDENCY: SymbolString->FromText() doesn't use 0x1E characters in its encoding.
+
+    my $string = $type . "\x1E" . $symbol . "\x1E" . $language . "\x1E" . $resolvingFlags . "\x1E";
+
+    if (defined $scope)
+        {
+        $string .= $scope;
+        };
+
+    $string .= "\x1E";
+
+    if (defined $using)
+        {
+        $string .= join("\x1E", @$using);
+        };
+
+    return $string;
+    };
+
+
+#
+#   Function: ToBinaryFile
+#
+#   Writes a <ReferenceString> to the passed filehandle.  Can also encode an undef.
+#
+#   Parameters:
+#
+#       fileHandle - The filehandle to write to.
+#       referenceString - The <ReferenceString> to write, or undef.
+#       binaryFormatFlags - Any <Binary Format Flags> you want to use to influence encoding.
+#
+#   Format:
+#
+#       > [SymbolString: Symbol or undef for an undef reference]
+#       > [AString16: language]
+#       > [SymbolString: Scope or undef for none]
+#       >
+#       > [SymbolString: Using or undef for none]
+#       > [SymbolString: Using or undef for no more]
+#       > ...
+#       >
+#       > [UInt8: Type unless BINARYREF_NOTYPE is set]
+#       > [UInt8: Resolving Flags unless BINARYREF_NORESOLVINGFLAGS is set]
+#
+#   Dependencies:
+#
+#       - <ReferenceTypes> must fit into a UInt8.  All values must be <= 255.
+#       - All <Resolving Flags> must fit into a UInt8.  All values must be <= 255.
+#
+sub ToBinaryFile #(FileHandle fileHandle, ReferenceString referenceString, flags binaryFormatFlags)
+    {
+    my ($self, $fileHandle, $referenceString, $binaryFormatFlags) = @_;
+
+    my ($type, $symbol, $language, $scope, $using, $resolvingFlags) = $self->InformationOf($referenceString);
+
+    # [SymbolString: Symbol or undef for an undef reference]
+
+    NaturalDocs::SymbolString->ToBinaryFile($fileHandle, $symbol);
+
+    # [AString16: language]
+
+    print $fileHandle pack('nA*', length $language, $language);
+
+    # [SymbolString: scope or undef if none]
+
+    NaturalDocs::SymbolString->ToBinaryFile($fileHandle, $scope);
+
+    # [SymbolString: using or undef if none/no more] ...
+
+    if (defined $using)
+        {
+        foreach my $usingScope (@$using)
+            {  NaturalDocs::SymbolString->ToBinaryFile($fileHandle, $usingScope);  };
+        };
+
+    NaturalDocs::SymbolString->ToBinaryFile($fileHandle, undef);
+
+    # [UInt8: Type unless BINARYREF_NOTYPE is set]
+
+    if (!($binaryFormatFlags & BINARYREF_NOTYPE))
+        {  print $fileHandle pack('C', $type);  };
+
+    # [UInt8: Resolving Flags unless BINARYREF_NORESOLVINGFLAGS is set]
+
+    if (!($binaryFormatFlags & BINARYREF_NORESOLVINGFLAGS))
+        {  print $fileHandle pack('C', $type);  };
+    };
+
+
+#
+#   Function: FromBinaryFile
+#
+#   Reads a <ReferenceString> or undef from the passed filehandle.
+#
+#   Parameters:
+#
+#       fileHandle - The filehandle to read from.
+#       binaryFormatFlags - Any <Binary Format Flags> you want to use to influence decoding.
+#       type - The <ReferenceType> to use if <BINARYREF_NOTYPE> is set.
+#       resolvingFlags - The <Resolving Flags> to use if <BINARYREF_NORESOLVINGFLAGS> is set.
+#
+#   Returns:
+#
+#       The <ReferenceString> or undef.
+#
+#   See Also:
+#
+#       See <ToBinaryFile()> for format and dependencies.
+#
+sub FromBinaryFile #(FileHandle fileHandle, flags binaryFormatFlags, ReferenceType type, flags resolvingFlags)
+    {
+    my ($self, $fileHandle, $binaryFormatFlags, $type, $resolvingFlags) = @_;
+    my $raw;
+
+    # [SymbolString: Symbol or undef for an undef reference]
+
+    my $symbol = NaturalDocs::SymbolString->FromBinaryFile($fileHandle);
+
+    if (!defined $symbol)
+        {  return undef;  };
+
+
+    # [AString16: language]
+
+    read($fileHandle, $raw, 2);
+    my $languageLength = unpack('n', $raw);
+
+    my $language;
+    read($fileHandle, $language, $languageLength);
+
+
+    # [SymbolString: scope or undef if none]
+
+    my $scope = NaturalDocs::SymbolString->FromBinaryFile($fileHandle);
+
+    # [SymbolString: using or undef if none/no more] ...
+
+    my $usingSymbol;
+    my @using;
+
+    while ($usingSymbol = NaturalDocs::SymbolString->FromBinaryFile($fileHandle))
+        {  push @using, $usingSymbol;  };
+
+    if (scalar @using)
+        {  $usingSymbol = \@using;  }
+    else
+        {  $usingSymbol = undef;  };
+
+    # [UInt8: Type unless BINARYREF_NOTYPE is set]
+
+    if (!($binaryFormatFlags & BINARYREF_NOTYPE))
+        {
+        my $raw;
+        read($fileHandle, $raw, 1);
+        $type = unpack('C', $raw);
+        };
+
+    # [UInt8: Resolving Flags unless BINARYREF_NORESOLVINGFLAGS is set]
+
+    if (!($binaryFormatFlags & BINARYREF_NORESOLVINGFLAGS))
+        {
+        my $raw;
+        read($fileHandle, $raw, 1);
+        $resolvingFlags = unpack('C', $raw);
+        };
+
+    return $self->MakeFrom($type, $symbol, $language, $scope, $usingSymbol, $resolvingFlags);
+    };
+
+
+#
+#   Function: InformationOf
+#
+#   Returns the information encoded in a <ReferenceString>.
+#
+#   Parameters:
+#
+#       referenceString - The <ReferenceString> to decode.
+#
+#   Returns:
+#
+#       The array ( type, symbol, language, scope, using, resolvingFlags ).
+#
+#       type - The <ReferenceType>.
+#       symbol - The <SymbolString>.
+#       language - The name of the language that defined the file the reference was defined in.
+#       scope - The scope <SymbolString>, or undef if none.
+#       using - An arrayref of scope <SymbolStrings> that the reference also has access to via "using" statements, or undef if none.
+#       resolvingFlags - The <Resolving Flags> of the reference.
+#
+sub InformationOf #(ReferenceString referenceString)
+    {
+    my ($self, $referenceString) = @_;
+
+    my ($type, $symbolString, $language, $resolvingFlags, $scopeString, @usingStrings) = split(/\x1E/, $referenceString);
+
+    if (!length $resolvingFlags)
+        {  $resolvingFlags = undef;  };
+
+    return ( $type, $symbolString, $language, $scopeString, [ @usingStrings ], $resolvingFlags );
+    };
+
+
+#
+#   Function: TypeOf
+#
+#   Returns the <ReferenceType> encoded in the reference string.  This is faster than <InformationOf()> if this is
+#   the only information you need.
+#
+sub TypeOf #(ReferenceString referenceString)
+    {
+    my ($self, $referenceString) = @_;
+
+    $referenceString =~ /^([^\x1E]+)/;
+    return $1;
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Settings.pm b/docs/tool/Modules/NaturalDocs/Settings.pm
new file mode 100644
index 00000000..8d1fc11b
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Settings.pm
@@ -0,0 +1,1418 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Settings
+#
+###############################################################################
+#
+#   A package to handle the command line and various other program settings.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use Cwd ();
+
+use NaturalDocs::Settings::BuildTarget;
+
+use strict;
+use integer;
+
+package NaturalDocs::Settings;
+
+
+###############################################################################
+# Group: Information
+
+=pod begin nd
+
+    Topic: Usage and Dependencies
+
+        - The <Constant Functions> can be called immediately.
+
+        - Prior to initialization, <NaturalDocs::Builder> must have all its output packages registered.
+
+        - To initialize, call <Load()>.  All functions except <InputDirectoryNameOf()> will then be available.
+
+        - <GenerateDirectoryNames()> must be called before <InputDirectoryNameOf()> will work.  Currently it is called by
+          <NaturalDocs::Menu->LoadAndUpdate()>.
+
+
+    Architecture: Internal Overview
+
+        - <Load()> first parses the command line, gathering all the settings and checking for errors.  All <NaturalDocs::Builder>
+          packages must be registered before this is called because it needs their command line options.
+          <NaturalDocs::Project->ReparseEverything()> and <NaturalDocs::Project->RebuildEverything()> are called right away if -r
+          or -ro are used.
+
+        - Output directories are *not* named at this point.  See <Named Directories>.
+
+        - The previous settings from the last time Natural Docs was run are loaded and compared to the current settings.
+          <NaturalDocs::Project->ReparseEverything()> and <NaturalDocs::Project->RebuildEverything()> are called if there are
+          any differences that warrant it.
+
+        - It then waits for <GenerateDirectoryNames()> to be called by <NaturalDocs::Menu>.  The reason for this is that the
+          previous directory names are stored as hints in the menu file, for reasons explained in <Named Directories>.  Once that
+          happens all the unnamed directories have names generated for them so everything is named.  The package is completely
+          set up.
+
+        - The input directories are stored in an array instead of a hash because the order they were declared in matters.  If two
+          people use multiple input directories on separate computers without sharing a menu file, they should at least get consistent
+          directory names by declaring them in the same order.
+
+
+    Architecture: Named Directories
+
+        Ever since Natural Docs introduced multiple input directories in 1.16, they've had to be named.  Since they don't necessarily
+        extend from the same root anymore, they can't share an output directory without the risk of file name conflicts.  There was
+        an early attempt at giving them actual names, but now they're just numbered from 1.
+
+        Directory names aren't generated right away.  It waits for <Menu.txt> to load because that holds the obfuscated names from
+        the last run.  <NaturalDocs::Menu> then calls <GenerateDirectoryNames()> and passes those along as hints.
+        <GenerateDirectoryNames()> then applies them to any matches and generates new ones for any remaining.  This is done so
+        that output page locations can remain consistent when built on multiple computers, so long as the menu file is shared.  I tend
+        to think the menu file is the most likely configuration file to be shared.
+
+
+    Architecture: Removed Directories
+
+        Directories that were part of the previous run but aren't anymore are still stored in the package.  The primary reason, though
+        there may be others, is file purging.  If an input directory is removed, all the output files that were generated from anything
+        in it need to be removed.  To find out what the output file name was for a removed source file, it needs to be able to split it
+        from it's original input directory and know what that directory was named.  If this didn't happen those output files would be
+        orphaned, as was the case prior to 1.32.
+
+=cut
+
+
+
+###############################################################################
+# Group: Variables
+
+
+# handle: PREVIOUS_SETTINGS_FILEHANDLE
+# The file handle used with <PreviousSettings.nd>.
+
+# array: inputDirectories
+# An array of input directories.
+my @inputDirectories;
+
+# array: inputDirectoryNames
+# An array of the input directory names.  Each name corresponds to the directory of the same index in <inputDirectories>.
+my @inputDirectoryNames;
+
+# array: imageDirectories
+# An array of image directories.
+my @imageDirectories;
+
+# array: imageDirectoryNames
+# An array of the image directory names.  Each name corresponds to the directory of the same index in <imageDirectories>.
+my @imageDirectoryNames;
+
+# array: relativeImageDirectories
+# An array of the relative paths for images.  The asterisks found in the command line are not present.
+my @relativeImageDirectories;
+
+# array: excludedInputDirectories
+# An array of input directories to exclude.
+my @excludedInputDirectories;
+
+# array: removedInputDirectories
+# An array of input directories that were once in the command line but are no longer.
+my @removedInputDirectories;
+
+# array: removedInputDirectoryNames
+# An array of the removed input directories' names.  Each name corresponds to the directory of the same index in
+# <removedInputDirectories>.
+my @removedInputDirectoryNames;
+
+# array: removedImageDirectories
+# An array of image directories that were once in the command line but are no longer.
+my @removedImageDirectories;
+
+# array: removedImageDirectoryNames
+# An array of the removed image directories' names.  Each name corresponds to the directory of the same index in
+# <removedImageDirectories>.
+my @removedImageDirectoryNames;
+
+# var: projectDirectory
+# The project directory.
+my $projectDirectory;
+
+# array: buildTargets
+# An array of <NaturalDocs::Settings::BuildTarget>s.
+my @buildTargets;
+
+# var: documentedOnly
+# Whether undocumented code aspects should be included in the output.
+my $documentedOnly;
+
+# int: tabLength
+# The number of spaces in tabs.
+my $tabLength;
+
+# bool: noAutoGroup
+# Whether auto-grouping is turned off.
+my $noAutoGroup;
+
+# bool: onlyFileTitles
+# Whether source files should always use the file name as the title.
+my $onlyFileTitles;
+
+# bool: isQuiet
+# Whether the script should be run in quiet mode or not.
+my $isQuiet;
+
+# bool: rebuildData
+# WHether most data files should be ignored and rebuilt.
+my $rebuildData;
+
+# array: styles
+# An array of style names to use, most important first.
+my @styles;
+
+# var: charset
+# The character encoding of the source files, and thus the output.
+my $charset;
+
+
+###############################################################################
+# Group: Files
+
+
+#
+#   File: PreviousSettings.nd
+#
+#   Stores the previous command line settings.
+#
+#   Format:
+#
+#       > [BINARY_FORMAT]
+#       > [VersionInt: app version]
+#
+#       The file starts with the standard <BINARY_FORMAT> <VersionInt> header.
+#
+#       > [UInt8: tab length]
+#       > [UInt8: documented only (0 or 1)]
+#       > [UInt8: no auto-group (0 or 1)]
+#       > [UInt8: only file titles (0 or 1)]
+#       > [AString16: charset]
+#       >
+#       > [UInt8: number of input directories]
+#       > [AString16: input directory] [AString16: input directory name] ...
+#
+#       A count of input directories, then that number of directory/name pairs.
+#
+#       > [UInt8: number of output targets]
+#       > [AString16: output directory] [AString16: output format command line option] ...
+#
+#       A count of output targets, then that number of directory/format pairs.
+#
+#
+#   Revisions:
+#
+#       1.4:
+#
+#           - Added only file titles.
+#
+#       1.33:
+#
+#           - Added charset.
+#
+#       1.3:
+#
+#           - Removed headers-only, which was a 0/1 UInt8 after tab length.
+#           - Change auto-group level (1 = no, 2 = yes, 3 = full only) to no auto-group (0 or 1).
+#
+#       1.22:
+#
+#           - Added auto-group level.
+#
+#       1.2:
+#
+#           - File was added to the project.  Prior to 1.2, it didn't exist.
+#
+
+
+###############################################################################
+# Group: Action Functions
+
+#
+#   Function: Load
+#
+#   Loads and parses all settings from the command line and configuration files.  Will exit if the options are invalid or the syntax
+#   reference was requested.
+#
+sub Load
+    {
+    my ($self) = @_;
+
+    $self->ParseCommandLine();
+    $self->LoadAndComparePreviousSettings();
+    };
+
+
+#
+#   Function: Save
+#
+#   Saves all settings in configuration files to disk.
+#
+sub Save
+    {
+    my ($self) = @_;
+
+    $self->SavePreviousSettings();
+    };
+
+
+#
+#   Function: GenerateDirectoryNames
+#
+#   Generates names for each of the input and image directories, which can later be retrieved with <InputDirectoryNameOf()>
+#   and <ImageDirectoryNameOf()>.
+#
+#   Parameters:
+#
+#       inputHints - A hashref of suggested input directory names, where the keys are the directories and the values are the names.
+#                        These take precedence over anything generated.  You should include names for directories that are no longer in
+#                        the command line.  This parameter may be undef.
+#       imageHints - Same as inputHints, only for the image directories.
+#
+sub GenerateDirectoryNames #(hashref inputHints, hashref imageHints)
+    {
+    my ($self, $inputHints, $imageHints) = @_;
+
+    my %usedInputNames;
+    my %usedImageNames;
+
+
+    if (defined $inputHints)
+        {
+        # First, we have to convert all non-numeric names to numbers, since they may come from a pre-1.32 menu file.  We do it
+        # here instead of in NaturalDocs::Menu to keep the naming scheme centralized.
+
+        my @names = values %$inputHints;
+        my $hasNonNumeric;
+
+        foreach my $name (@names)
+            {
+            if ($name !~ /^[0-9]+$/)
+                {
+                $hasNonNumeric = 1;
+                last;
+                };
+            };
+
+
+        if ($hasNonNumeric)
+            {
+            # Hash mapping old names to new names.
+            my %conversion;
+
+            # The sequential number to use.  Starts at two because we want 'default' to be one.
+            my $currentNumber = 2;
+
+            # If there's only one name, we set it to one no matter what it was set to before.
+            if (scalar @names == 1)
+                {  $conversion{$names[0]} = 1;  }
+            else
+                {
+                # We sort the list first because we want the end result to be predictable.  This conversion could be happening on many
+                # machines, and they may not all specify the input directories in the same order.  They need to all come up with the
+                # same result.
+                @names = sort @names;
+
+                foreach my $name (@names)
+                    {
+                    if ($name eq 'default')
+                        {  $conversion{$name} = 1;  }
+                    else
+                        {
+                        $conversion{$name} = $currentNumber;
+                        $currentNumber++;
+                        };
+                    };
+                };
+
+            # Convert them to the new names.
+            foreach my $directory (keys %$inputHints)
+                {
+                $inputHints->{$directory} = $conversion{ $inputHints->{$directory} };
+                };
+            };
+
+
+        # Now we apply all the names from the hints, and save any unused ones as removed directories.
+
+        for (my $i = 0; $i < scalar @inputDirectories; $i++)
+            {
+            if (exists $inputHints->{$inputDirectories[$i]})
+                {
+                $inputDirectoryNames[$i] = $inputHints->{$inputDirectories[$i]};
+                $usedInputNames{ $inputDirectoryNames[$i] } = 1;
+                delete $inputHints->{$inputDirectories[$i]};
+                };
+            };
+
+
+        # Any remaining hints are saved as removed directories.
+
+        while (my ($directory, $name) = each %$inputHints)
+            {
+            push @removedInputDirectories, $directory;
+            push @removedInputDirectoryNames, $name;
+            };
+        };
+
+
+    if (defined $imageHints)
+        {
+        # Image directory names were never non-numeric, so there is no conversion.  Apply all the names from the hints.
+
+        for (my $i = 0; $i < scalar @imageDirectories; $i++)
+            {
+            if (exists $imageHints->{$imageDirectories[$i]})
+                {
+                $imageDirectoryNames[$i] = $imageHints->{$imageDirectories[$i]};
+                $usedImageNames{ $imageDirectoryNames[$i] } = 1;
+                delete $imageHints->{$imageDirectories[$i]};
+                };
+            };
+
+
+        # Any remaining hints are saved as removed directories.
+
+        while (my ($directory, $name) = each %$imageHints)
+            {
+            push @removedImageDirectories, $directory;
+            push @removedImageDirectoryNames, $name;
+            };
+        };
+
+
+    # Now we generate names for anything remaining.
+
+    my $inputCounter = 1;
+
+    for (my $i = 0; $i < scalar @inputDirectories; $i++)
+        {
+        if (!defined $inputDirectoryNames[$i])
+            {
+            while (exists $usedInputNames{$inputCounter})
+                {  $inputCounter++;  };
+
+            $inputDirectoryNames[$i] = $inputCounter;
+            $usedInputNames{$inputCounter} = 1;
+
+            $inputCounter++;
+            };
+        };
+
+
+    my $imageCounter = 1;
+
+    for (my $i = 0; $i < scalar @imageDirectories; $i++)
+        {
+        if (!defined $imageDirectoryNames[$i])
+            {
+            while (exists $usedImageNames{$imageCounter})
+                {  $imageCounter++;  };
+
+            $imageDirectoryNames[$i] = $imageCounter;
+            $usedImageNames{$imageCounter} = 1;
+
+            $imageCounter++;
+            };
+        };
+    };
+
+
+
+###############################################################################
+# Group: Information Functions
+
+
+#
+#   Function: InputDirectories
+#
+#   Returns an arrayref of input directories.  Do not change.
+#
+#   This will not return any removed input directories.
+#
+sub InputDirectories
+    {  return \@inputDirectories;  };
+
+#
+#   Function: InputDirectoryNameOf
+#
+#   Returns the generated name of the passed input directory.  <GenerateDirectoryNames()> must be called once before this
+#   function is available.
+#
+#   If a name for a removed input directory is available, it will be returned as well.
+#
+sub InputDirectoryNameOf #(directory)
+    {
+    my ($self, $directory) = @_;
+
+    for (my $i = 0; $i < scalar @inputDirectories; $i++)
+        {
+        if ($directory eq $inputDirectories[$i])
+            {  return $inputDirectoryNames[$i];  };
+        };
+
+    for (my $i = 0; $i < scalar @removedInputDirectories; $i++)
+        {
+        if ($directory eq $removedInputDirectories[$i])
+            {  return $removedInputDirectoryNames[$i];  };
+        };
+
+    return undef;
+    };
+
+
+#
+#   Function: SplitFromInputDirectory
+#
+#   Takes an input file name and returns the array ( inputDirectory, relativePath ).
+#
+#   If the file cannot be split from an input directory, it will try to do it with the removed input directories.
+#
+sub SplitFromInputDirectory #(file)
+    {
+    my ($self, $file) = @_;
+
+    foreach my $directory (@inputDirectories, @removedInputDirectories)
+        {
+        if (NaturalDocs::File->IsSubPathOf($directory, $file))
+            {  return ( $directory, NaturalDocs::File->MakeRelativePath($directory, $file) );  };
+        };
+
+    return ( );
+    };
+
+
+#
+#   Function: ImageDirectories
+#
+#   Returns an arrayref of image directories.  Do not change.
+#
+#   This will not return any removed image directories.
+#
+sub ImageDirectories
+    {  return \@imageDirectories;  };
+
+
+#
+#   Function: ImageDirectoryNameOf
+#
+#   Returns the generated name of the passed image or input directory.  <GenerateDirectoryNames()> must be called once before
+#   this function is available.
+#
+#   If a name for a removed input or image directory is available, it will be returned as well.
+#
+sub ImageDirectoryNameOf #(directory)
+    {
+    my ($self, $directory) = @_;
+
+    for (my $i = 0; $i < scalar @imageDirectories; $i++)
+        {
+        if ($directory eq $imageDirectories[$i])
+            {  return $imageDirectoryNames[$i];  };
+        };
+
+    for (my $i = 0; $i < scalar @removedImageDirectories; $i++)
+        {
+        if ($directory eq $removedImageDirectories[$i])
+            {  return $removedImageDirectoryNames[$i];  };
+        };
+
+    return undef;
+    };
+
+
+#
+#   Function: SplitFromImageDirectory
+#
+#   Takes an input image file name and returns the array ( imageDirectory, relativePath ).
+#
+#   If the file cannot be split from an image directory, it will try to do it with the removed image directories.
+#
+sub SplitFromImageDirectory #(file)
+    {
+    my ($self, $file) = @_;
+
+    foreach my $directory (@imageDirectories, @removedImageDirectories)
+        {
+        if (NaturalDocs::File->IsSubPathOf($directory, $file))
+            {  return ( $directory, NaturalDocs::File->MakeRelativePath($directory, $file) );  };
+        };
+
+    return ( );
+    };
+
+
+#
+#   Function: RelativeImageDirectories
+#
+#   Returns an arrayref of relative image directories.  Do not change.
+#
+sub RelativeImageDirectories
+    {  return \@relativeImageDirectories;  };
+
+
+# Function: ExcludedInputDirectories
+# Returns an arrayref of input directories to exclude.  Do not change.
+sub ExcludedInputDirectories
+    {  return \@excludedInputDirectories;  };
+
+
+# Function: BuildTargets
+# Returns an arrayref of <NaturalDocs::Settings::BuildTarget>s.  Do not change.
+sub BuildTargets
+    {  return \@buildTargets;  };
+
+
+#
+#   Function: OutputDirectoryOf
+#
+#   Returns the output directory of a builder object.
+#
+#   Parameters:
+#
+#       object - The builder object, whose class is derived from <NaturalDocs::Builder::Base>.
+#
+#   Returns:
+#
+#       The builder directory, or undef if the object wasn't found..
+#
+sub OutputDirectoryOf #(object)
+    {
+    my ($self, $object) = @_;
+
+    foreach my $buildTarget (@buildTargets)
+        {
+        if ($buildTarget->Builder() == $object)
+            {  return $buildTarget->Directory();  };
+        };
+
+    return undef;
+    };
+
+
+# Function: Styles
+# Returns an arrayref of the styles associated with the output.
+sub Styles
+    {  return \@styles;  };
+
+# Function: ProjectDirectory
+# Returns the project directory.
+sub ProjectDirectory
+    {  return $projectDirectory;  };
+
+# Function: ProjectDataDirectory
+# Returns the project data directory.
+sub ProjectDataDirectory
+    {  return NaturalDocs::File->JoinPaths($projectDirectory, 'Data', 1);  };
+
+# Function: StyleDirectory
+# Returns the main style directory.
+sub StyleDirectory
+    {  return NaturalDocs::File->JoinPaths($FindBin::RealBin, 'Styles', 1);  };
+
+# Function: JavaScriptDirectory
+# Returns the main JavaScript directory.
+sub JavaScriptDirectory
+    {  return NaturalDocs::File->JoinPaths($FindBin::RealBin, 'JavaScript', 1);  };
+
+# Function: ConfigDirectory
+# Returns the main configuration directory.
+sub ConfigDirectory
+    {  return NaturalDocs::File->JoinPaths($FindBin::RealBin, 'Config', 1);  };
+
+# Function: DocumentedOnly
+# Returns whether undocumented code aspects should be included in the output.
+sub DocumentedOnly
+    {  return $documentedOnly;  };
+
+# Function: TabLength
+# Returns the number of spaces tabs should be expanded to.
+sub TabLength
+    {  return $tabLength;  };
+
+# Function: NoAutoGroup
+# Returns whether auto-grouping is turned off.
+sub NoAutoGroup
+    {  return $noAutoGroup;  };
+
+# Function: OnlyFileTitles
+# Returns whether source files should always use the file name as the title.
+sub OnlyFileTitles
+    {  return $onlyFileTitles;  };
+
+# Function: IsQuiet
+# Returns whether the script should be run in quiet mode or not.
+sub IsQuiet
+    {  return $isQuiet;  };
+
+# Function: RebuildData
+# Returns whether all data files should be ignored and rebuilt.
+sub RebuildData
+    {  return $rebuildData;  };
+
+# Function: CharSet
+# Returns the character set, or undef if none.
+sub CharSet
+    {  return $charset;  };
+
+
+###############################################################################
+# Group: Constant Functions
+
+#
+#   Function: AppVersion
+#
+#   Returns Natural Docs' version number as an integer.  Use <TextAppVersion()> to get a printable version.
+#
+sub AppVersion
+    {
+    my ($self) = @_;
+    return NaturalDocs::Version->FromString($self->TextAppVersion());
+    };
+
+#
+#   Function: TextAppVersion
+#
+#   Returns Natural Docs' version number as plain text.
+#
+sub TextAppVersion
+    {
+    return '1.4';
+    };
+
+#
+#   Function: AppURL
+#
+#   Returns a string of the project's current web address.
+#
+sub AppURL
+    {  return 'http://www.naturaldocs.org';  };
+
+
+
+###############################################################################
+# Group: Support Functions
+
+
+#
+#   Function: ParseCommandLine
+#
+#   Parses and validates the command line.  Will cause the script to exit if the options ask for the syntax reference or
+#   are invalid.
+#
+sub ParseCommandLine
+    {
+    my ($self) = @_;
+
+    my %synonyms = ( 'input'    => '-i',
+                                  'source' => '-i',
+                                  'excludeinput' => '-xi',
+                                  'excludesource' => '-xi',
+                                  'images' => '-img',
+                                  'output'  => '-o',
+                                  'project' => '-p',
+                                  'documentedonly' => '-do',
+                                  'style'    => '-s',
+                                  'rebuild' => '-r',
+                                  'rebuildoutput' => '-ro',
+                                  'tablength' => '-t',
+                                  'quiet'    => '-q',
+                                  'headersonly' => '-ho',
+                                  'help'     => '-h',
+                                  'autogroup' => '-ag',
+                                  'noautogroup' => '-nag',
+                                  'onlyfiletitles' => '-oft',
+                                  'onlyfiletitle' => '-oft',
+                                  'charset' => '-cs',
+                                  'characterset' => '-cs' );
+
+
+    my @errorMessages;
+
+    my $valueRef;
+    my $option;
+
+    my @outputStrings;
+    my @imageStrings;
+
+
+    # Sometimes $valueRef is set to $ignored instead of undef because we don't want certain errors to cause other,
+    # unnecessary errors.  For example, if they set the input directory twice, we want to show that error and swallow the
+    # specified directory without complaint.  Otherwise it would complain about the directory too as if it were random crap
+    # inserted into the command line.
+    my $ignored;
+
+    my $index = 0;
+
+    while ($index < scalar @ARGV)
+        {
+        my $arg = $ARGV[$index];
+
+        if (substr($arg, 0, 1) eq '-')
+            {
+            $option = lc($arg);
+
+            # Support options like -t2 as well as -t 2.
+            if ($option =~ /^([^0-9]+)([0-9]+)$/)
+                {
+                $option = $1;
+                splice(@ARGV, $index + 1, 0, $2);
+                };
+
+            # Convert long forms to short.
+            if (substr($option, 1, 1) eq '-')
+                {
+                # Strip all dashes.
+                my $newOption = $option;
+                $newOption =~ tr/-//d;
+
+                if (exists $synonyms{$newOption})
+                    {  $option = $synonyms{$newOption};  }
+                }
+
+            if ($option eq '-i')
+                {
+                push @inputDirectories, undef;
+                $valueRef = \$inputDirectories[-1];
+                }
+            elsif ($option eq '-xi')
+                {
+                push @excludedInputDirectories, undef;
+                $valueRef = \$excludedInputDirectories[-1];
+                }
+            elsif ($option eq '-img')
+                {
+                push @imageStrings, undef;
+                $valueRef = \$imageStrings[-1];
+                }
+            elsif ($option eq '-p')
+                {
+                if (defined $projectDirectory)
+                    {
+                    push @errorMessages, 'You cannot have more than one project directory.';
+                    $valueRef = \$ignored;
+                    }
+                else
+                    {  $valueRef = \$projectDirectory;  };
+                }
+            elsif ($option eq '-o')
+                {
+                push @outputStrings, undef;
+                $valueRef = \$outputStrings[-1];
+                }
+            elsif ($option eq '-s')
+                {
+                $valueRef = \$styles[0];
+                }
+            elsif ($option eq '-t')
+                {
+                $valueRef = \$tabLength;
+                }
+            elsif ($option eq '-cs')
+                {
+                $valueRef = \$charset;
+                }
+            elsif ($option eq '-ag')
+                {
+                push @errorMessages, 'The -ag setting is no longer supported.  You can use -nag (--no-auto-group) to turn off '
+                                               . "auto-grouping, but there aren't multiple levels anymore.";
+                $valueRef = \$ignored;
+                }
+
+            # Options that aren't followed by content.
+            else
+                {
+                $valueRef = undef;
+
+                if ($option eq '-r')
+                    {
+                    NaturalDocs::Project->ReparseEverything();
+                    NaturalDocs::Project->RebuildEverything();
+                    $rebuildData = 1;
+                    }
+                elsif ($option eq '-ro')
+                    {
+                    NaturalDocs::Project->RebuildEverything();
+                    }
+                elsif ($option eq '-do')
+                    {  $documentedOnly = 1;  }
+                elsif ($option eq '-oft')
+                    {  $onlyFileTitles = 1;  }
+                elsif ($option eq '-q')
+                    {  $isQuiet = 1;  }
+                elsif ($option eq '-ho')
+                    {
+                    push @errorMessages, 'The -ho setting is no longer supported.  You can have Natural Docs skip over the source file '
+                                                   . 'extensions by editing Languages.txt in your project directory.';
+                    }
+                elsif ($option eq '-nag')
+                    {  $noAutoGroup = 1;  }
+                elsif ($option eq '-?' || $option eq '-h')
+                    {
+                    $self->PrintSyntax();
+                    exit;
+                    }
+                else
+                    {  push @errorMessages, 'Unrecognized option ' . $option;  };
+
+                };
+
+            }
+
+        # Is a segment of text, not an option...
+        else
+            {
+            if (defined $valueRef)
+                {
+                # We want to preserve spaces in paths.
+                if (defined $$valueRef)
+                    {  $$valueRef .= ' ';  };
+
+                $$valueRef .= $arg;
+                }
+
+            else
+                {
+                push @errorMessages, 'Unrecognized element ' . $arg;
+                };
+            };
+
+        $index++;
+        };
+
+
+    # Validate the style, if specified.
+
+    if ($styles[0])
+        {
+        my @stylePieces = split(/ +/, $styles[0]);
+        @styles = ( );
+
+        while (scalar @stylePieces)
+            {
+            if (lc($stylePieces[0]) eq 'custom')
+                {
+                push @errorMessages, 'The "Custom" style setting is no longer supported.  Copy your custom style sheet to your '
+                                               . 'project directory and you can refer to it with -s.';
+                shift @stylePieces;
+                }
+            else
+                {
+                # People may use styles with spaces in them.  If a style doesn't exist, we need to join the pieces until we find one that
+                # does or we run out of pieces.
+
+                my $extras = 0;
+                my $success;
+
+                while ($extras < scalar @stylePieces)
+                    {
+                    my $style;
+
+                    if (!$extras)
+                        {  $style = $stylePieces[0];  }
+                    else
+                        {  $style = join(' ', @stylePieces[0..$extras]);  };
+
+                    my $cssFile = NaturalDocs::File->JoinPaths( $self->StyleDirectory(), $style . '.css' );
+                    if (-e $cssFile)
+                        {
+                        push @styles, $style;
+                        splice(@stylePieces, 0, 1 + $extras);
+                        $success = 1;
+                        last;
+                        }
+                    else
+                        {
+                        $cssFile = NaturalDocs::File->JoinPaths( $self->ProjectDirectory(), $style . '.css' );
+
+                        if (-e $cssFile)
+                            {
+                            push @styles, $style;
+                            splice(@stylePieces, 0, 1 + $extras);
+                            $success = 1;
+                            last;
+                            }
+                        else
+                            {  $extras++;  };
+                        };
+                    };
+
+                if (!$success)
+                    {
+                    push @errorMessages, 'The style "' . $stylePieces[0] . '" does not exist.';
+                    shift @stylePieces;
+                    };
+                };
+            };
+        }
+    else
+        {  @styles = ( 'Default' );  };
+
+
+    # Decode and validate the output strings.
+
+    my %outputDirectories;
+
+    foreach my $outputString (@outputStrings)
+        {
+        my ($format, $directory) = split(/ /, $outputString, 2);
+
+        if (!defined $directory)
+            {  push @errorMessages, 'The -o option needs two parameters: -o [format] [directory]';  }
+        else
+            {
+            if (!NaturalDocs::File->PathIsAbsolute($directory))
+                {  $directory = NaturalDocs::File->JoinPaths(Cwd::cwd(), $directory, 1);  };
+
+            $directory = NaturalDocs::File->CanonizePath($directory);
+
+            if (! -e $directory || ! -d $directory)
+                {
+                # They may have forgotten the format portion and the directory name had a space in it.
+                if (-e ($format . ' ' . $directory) && -d ($format . ' ' . $directory))
+                    {
+                    push @errorMessages, 'The -o option needs two parameters: -o [format] [directory]';
+                    $format = undef;
+                    }
+                else
+                    {  push @errorMessages, 'The output directory ' . $directory . ' does not exist.';  }
+                }
+            elsif (exists $outputDirectories{$directory})
+                {  push @errorMessages, 'You cannot specify the output directory ' . $directory . ' more than once.';  }
+            else
+                {  $outputDirectories{$directory} = 1;  };
+
+            if (defined $format)
+                {
+                my $builderPackage = NaturalDocs::Builder->OutputPackageOf($format);
+
+                if (defined $builderPackage)
+                    {
+                    push @buildTargets,
+                            NaturalDocs::Settings::BuildTarget->New($builderPackage->New(), $directory);
+                    }
+                else
+                    {
+                    push @errorMessages, 'The output format ' . $format . ' doesn\'t exist or is not installed.';
+                    $valueRef = \$ignored;
+                    };
+                };
+            };
+        };
+
+    if (!scalar @buildTargets)
+        {  push @errorMessages, 'You did not specify an output directory.';  };
+
+
+    # Decode and validate the image strings.
+
+    foreach my $imageString (@imageStrings)
+        {
+        if ($imageString =~ /^ *\*/)
+            {
+            # The below NaturalDocs::File functions assume everything is canonized.
+            $imageString = NaturalDocs::File->CanonizePath($imageString);
+
+            my ($volume, $directoryString) = NaturalDocs::File->SplitPath($imageString, 1);
+            my @directories = NaturalDocs::File->SplitDirectories($directoryString);
+
+            shift @directories;
+
+            $directoryString = NaturalDocs::File->JoinDirectories(@directories);
+            push @relativeImageDirectories, NaturalDocs::File->JoinPath($volume, $directoryString);
+            }
+        else
+            {
+            if (!NaturalDocs::File->PathIsAbsolute($imageString))
+                {  $imageString = NaturalDocs::File->JoinPaths(Cwd::cwd(), $imageString, 1);  };
+
+            $imageString = NaturalDocs::File->CanonizePath($imageString);
+
+            if (! -e $imageString || ! -d $imageString)
+                {  push @errorMessages, 'The image directory ' . $imageString . ' does not exist.';  };
+
+            push @imageDirectories, $imageString;
+            };
+        };
+
+
+    # Make sure the input and project directories are specified, canonized, and exist.
+
+    if (scalar @inputDirectories)
+        {
+        for (my $i = 0; $i < scalar @inputDirectories; $i++)
+            {
+            if (!NaturalDocs::File->PathIsAbsolute($inputDirectories[$i]))
+                {  $inputDirectories[$i] = NaturalDocs::File->JoinPaths(Cwd::cwd(), $inputDirectories[$i], 1);  };
+
+            $inputDirectories[$i] = NaturalDocs::File->CanonizePath($inputDirectories[$i]);
+
+            if (! -e $inputDirectories[$i] || ! -d $inputDirectories[$i])
+                {  push @errorMessages, 'The input directory ' . $inputDirectories[$i] . ' does not exist.';  };
+            };
+        }
+    else
+        {  push @errorMessages, 'You did not specify an input (source) directory.';  };
+
+    if (defined $projectDirectory)
+        {
+        if (!NaturalDocs::File->PathIsAbsolute($projectDirectory))
+            {  $projectDirectory = NaturalDocs::File->JoinPaths(Cwd::cwd(), $projectDirectory, 1);  };
+
+        $projectDirectory = NaturalDocs::File->CanonizePath($projectDirectory);
+
+        if (! -e $projectDirectory || ! -d $projectDirectory)
+            {  push @errorMessages, 'The project directory ' . $projectDirectory . ' does not exist.';  };
+
+        # Create the Data subdirectory if it doesn't exist.
+        NaturalDocs::File->CreatePath( NaturalDocs::File->JoinPaths($projectDirectory, 'Data', 1) );
+        }
+    else
+        {  push @errorMessages, 'You did not specify a project directory.';  };
+
+
+    # Make sure the excluded input directories are canonized, and add the project and output directories to the list.
+
+    for (my $i = 0; $i < scalar @excludedInputDirectories; $i++)
+        {
+        if (!NaturalDocs::File->PathIsAbsolute($excludedInputDirectories[$i]))
+            {  $excludedInputDirectories[$i] = NaturalDocs::File->JoinPaths(Cwd::cwd(), $excludedInputDirectories[$i], 1);  };
+
+        $excludedInputDirectories[$i] = NaturalDocs::File->CanonizePath($excludedInputDirectories[$i]);
+        };
+
+    push @excludedInputDirectories, $projectDirectory;
+
+    foreach my $buildTarget (@buildTargets)
+        {
+        push @excludedInputDirectories, $buildTarget->Directory();
+        };
+
+
+    # Determine the tab length, and default to four if not specified.
+
+    if (defined $tabLength)
+        {
+        if ($tabLength !~ /^[0-9]+$/)
+            {  push @errorMessages, 'The tab length must be a number.';  };
+        }
+    else
+        {  $tabLength = 4;  };
+
+
+    # Strip any quotes off of the charset.
+    $charset =~ tr/\"//d;
+
+
+    # Exit with the error message if there was one.
+
+    if (scalar @errorMessages)
+        {
+        print join("\n", @errorMessages) . "\nType NaturalDocs -h to see the syntax reference.\n";
+        exit;
+        };
+    };
+
+#
+#   Function: PrintSyntax
+#
+#   Prints the syntax reference.
+#
+sub PrintSyntax
+    {
+    my ($self) = @_;
+
+    # Make sure all line lengths are under 80 characters.
+
+    print
+
+    "Natural Docs, version " . $self->TextAppVersion() . "\n"
+    . $self->AppURL() . "\n"
+    . "This program is licensed under the GPL\n"
+    . "--------------------------------------\n"
+    . "\n"
+    . "Syntax:\n"
+    . "\n"
+    . "    NaturalDocs -i [input (source) directory]\n"
+    . "               (-i [input (source) directory] ...)\n"
+    . "                -o [output format] [output directory]\n"
+    . "               (-o [output format] [output directory] ...)\n"
+    . "                -p [project directory]\n"
+    . "                [options]\n"
+    . "\n"
+    . "Examples:\n"
+    . "\n"
+    . "    NaturalDocs -i C:\\My Project\\Source -o HTML C:\\My Project\\Docs\n"
+    . "                -p C:\\My Project\\Natural Docs\n"
+    . "    NaturalDocs -i /src/project -o HTML /doc/project\n"
+    . "                -p /etc/naturaldocs/project -s Small -q\n"
+    . "\n"
+    . "Required Parameters:\n"
+    . "\n"
+    . " -i [dir]\n--input [dir]\n--source [dir]\n"
+    . "     Specifies an input (source) directory.  Required.\n"
+    . "     Can be specified multiple times.\n"
+    . "\n"
+    . " -o [fmt] [dir]\n--output [fmt] [dir]\n"
+    . "    Specifies an output format and directory.  Required.\n"
+    . "    Can be specified multiple times, but only once per directory.\n"
+    . "    Possible output formats:\n";
+
+    $self->PrintOutputFormats('    - ');
+
+    print
+    "\n"
+    . " -p [dir]\n--project [dir]\n"
+    . "    Specifies the project directory.  Required.\n"
+    . "    There needs to be a unique project directory for every source directory.\n"
+    . "\n"
+    . "Optional Parameters:\n"
+    . "\n"
+    . " -s [style] ([style] [style] ...)\n--style [style] ([style] [style] ...)\n"
+    . "    Specifies the CSS style when building HTML output.  If multiple styles are\n"
+    . "    specified, they will all be included in the order given.\n"
+    . "\n"
+    . " -img [image directory]\n--image [image directory]"
+    . "    Specifies an image directory.  Can be specified multiple times.\n"
+    . "    Start with * to specify a relative directory, as in -img */images.\n"
+    . "\n"
+    . " -do\n--documented-only\n"
+    . "    Specifies only documented code aspects should be included in the output.\n"
+    . "\n"
+    . " -t [len]\n--tab-length [len]\n"
+    . "    Specifies the number of spaces tabs should be expanded to.  This only needs\n"
+    . "    to be set if you use tabs in example code and text diagrams.  Defaults to 4.\n"
+    . "\n"
+    . " -xi [dir]\n--exclude-input [dir]\n--exclude-source [dir]\n"
+    . "    Excludes an input (source) directory from the documentation.\n"
+    . "    Automatically done for the project and output directories.  Can\n"
+    . "    be specified multiple times.\n"
+    . "\n"
+    . " -nag\n--no-auto-group\n"
+    . "    Turns off auto-grouping completely.\n"
+    . "\n"
+    . " -oft\n--only-file-titles\n"
+    . "    Source files will only use the file name as the title.\n"
+    . "\n"
+    . " -r\n--rebuild\n"
+    . "    Rebuilds all output and data files from scratch.\n"
+    . "    Does not affect the menu file.\n"
+    . "\n"
+    . " -ro\n--rebuild-output\n"
+    . "    Rebuilds all output files from scratch.\n"
+    . "\n"
+    . " -q\n--quiet\n"
+    . "    Suppresses all non-error output.\n"
+    . "\n"
+    . " -?\n -h\n--help\n"
+    . "    Displays this syntax reference.\n";
+    };
+
+
+#
+#   Function: PrintOutputFormats
+#
+#   Prints all the possible output formats that can be specified with -o.  Each one will be placed on its own line.
+#
+#   Parameters:
+#
+#       prefix - Characters to prefix each one with, such as for indentation.
+#
+sub PrintOutputFormats #(prefix)
+    {
+    my ($self, $prefix) = @_;
+
+    my $outputPackages = NaturalDocs::Builder::OutputPackages();
+
+    foreach my $outputPackage (@$outputPackages)
+        {
+        print $prefix . $outputPackage->CommandLineOption() . "\n";
+        };
+    };
+
+
+#
+#   Function: LoadAndComparePreviousSettings
+#
+#   Loads <PreviousSettings.nd> and compares the values there with those in the command line.  If differences require it,
+#   sets <rebuildData> and/or <rebuildOutput>.
+#
+sub LoadAndComparePreviousSettings
+    {
+    my ($self) = @_;
+
+    my $fileIsOkay;
+
+    if (!NaturalDocs::Settings->RebuildData())
+        {
+        my $version;
+
+        if (NaturalDocs::BinaryFile->OpenForReading( NaturalDocs::Project->DataFile('PreviousSettings.nd'),
+                                                                           NaturalDocs::Version->FromString('1.4') ))
+            {  $fileIsOkay = 1;  };
+        };
+
+    if (!$fileIsOkay)
+        {
+        # We need to reparse everything because --documented-only may have changed.
+        # We need to rebuild everything because --tab-length may have changed.
+        NaturalDocs::Project->ReparseEverything();
+        NaturalDocs::Project->RebuildEverything();
+        }
+    else
+        {
+        my $raw;
+  
+        # [UInt8: tab expansion]
+        # [UInt8: documented only (0 or 1)]
+        # [UInt8: no auto-group (0 or 1)]
+        # [UInt8: only file titles (0 or 1)]
+        # [AString16: charset]
+
+        my $prevTabLength = NaturalDocs::BinaryFile->GetUInt8();
+        my $prevDocumentedOnly = NaturalDocs::BinaryFile->GetUInt8();
+        my $prevNoAutoGroup = NaturalDocs::BinaryFile->GetUInt8();
+        my $prevOnlyFileTitles = NaturalDocs::BinaryFile->GetUInt8();
+        my $prevCharset = NaturalDocs::BinaryFile->GetAString16();
+
+        if ($prevTabLength != $self->TabLength())
+            {
+            # We need to rebuild all output because this affects all text diagrams.
+            NaturalDocs::Project->RebuildEverything();
+            };
+
+        if ($prevDocumentedOnly == 0)
+            {  $prevDocumentedOnly = undef;  };
+        if ($prevNoAutoGroup == 0)
+            {  $prevNoAutoGroup = undef;  };
+        if ($prevOnlyFileTitles == 0)
+            {  $prevOnlyFileTitles = undef;  };
+
+        if ($prevDocumentedOnly != $self->DocumentedOnly() ||
+            $prevNoAutoGroup != $self->NoAutoGroup() ||
+            $prevOnlyFileTitles != $self->OnlyFileTitles())
+            {
+            NaturalDocs::Project->ReparseEverything();
+            };
+
+        if ($prevCharset ne $charset)
+            {  NaturalDocs::Project->RebuildEverything();  };
+
+
+        # [UInt8: number of input directories]
+
+        my $inputDirectoryCount = NaturalDocs::BinaryFile->GetUInt8();
+
+        while ($inputDirectoryCount)
+            {
+            # [AString16: input directory] [AString16: input directory name] ...
+
+            my $inputDirectory = NaturalDocs::BinaryFile->GetAString16();
+            my $inputDirectoryName = NaturalDocs::BinaryFile->GetAString16();
+
+            # Not doing anything with this for now.
+
+            $inputDirectoryCount--;
+            };
+
+
+        # [UInt8: number of output targets]
+
+        my $outputTargetCount = NaturalDocs::BinaryFile->GetUInt8();
+
+        # Keys are the directories, values are the command line options.
+        my %previousOutputDirectories;
+
+        while ($outputTargetCount)
+            {
+            # [AString16: output directory] [AString16: output format command line option] ...
+
+            my $outputDirectory = NaturalDocs::BinaryFile->GetAString16();
+            my $outputCommand = NaturalDocs::BinaryFile->GetAString16();
+
+            $previousOutputDirectories{$outputDirectory} = $outputCommand;
+
+            $outputTargetCount--;
+            };
+
+        # Check if any targets were added to the command line, or if their formats changed.  We don't care if targets were
+        # removed.
+        my $buildTargets = $self->BuildTargets();
+
+        foreach my $buildTarget (@$buildTargets)
+            {
+            if (!exists $previousOutputDirectories{$buildTarget->Directory()} ||
+                $buildTarget->Builder()->CommandLineOption() ne $previousOutputDirectories{$buildTarget->Directory()})
+                {
+                NaturalDocs::Project->RebuildEverything();
+                last;
+                };
+            };
+
+        NaturalDocs::BinaryFile->Close();
+        };
+    };
+
+
+#
+#   Function: SavePreviousSettings
+#
+#   Saves the settings into <PreviousSettings.nd>.
+#
+sub SavePreviousSettings
+    {
+    my ($self) = @_;
+
+    NaturalDocs::BinaryFile->OpenForWriting(  NaturalDocs::Project->DataFile('PreviousSettings.nd') );
+
+    # [UInt8: tab length]
+    # [UInt8: documented only (0 or 1)]
+    # [UInt8: no auto-group (0 or 1)]
+    # [UInt8: only file titles (0 or 1)]
+    # [AString16: charset]
+    # [UInt8: number of input directories]
+
+    my $inputDirectories = $self->InputDirectories();
+
+    NaturalDocs::BinaryFile->WriteUInt8($self->TabLength());
+    NaturalDocs::BinaryFile->WriteUInt8($self->DocumentedOnly() ? 1 : 0);
+    NaturalDocs::BinaryFile->WriteUInt8($self->NoAutoGroup() ? 1 : 0);
+    NaturalDocs::BinaryFile->WriteUInt8($self->OnlyFileTitles() ? 1 : 0);
+    NaturalDocs::BinaryFile->WriteAString16($charset);
+    NaturalDocs::BinaryFile->WriteUInt8(scalar @$inputDirectories);
+
+    foreach my $inputDirectory (@$inputDirectories)
+        {
+        my $inputDirectoryName = $self->InputDirectoryNameOf($inputDirectory);
+
+        # [AString16: input directory] [AString16: input directory name] ...
+        NaturalDocs::BinaryFile->WriteAString16($inputDirectory);
+        NaturalDocs::BinaryFile->WriteAString16($inputDirectoryName);
+        };
+
+    # [UInt8: number of output targets]
+
+    my $buildTargets = $self->BuildTargets();
+    NaturalDocs::BinaryFile->WriteUInt8(scalar @$buildTargets);
+
+    foreach my $buildTarget (@$buildTargets)
+        {
+        # [AString16: output directory] [AString16: output format command line option] ...
+        NaturalDocs::BinaryFile->WriteAString16( $buildTarget->Directory() );
+        NaturalDocs::BinaryFile->WriteAString16( $buildTarget->Builder()->CommandLineOption() );
+        };
+
+    NaturalDocs::BinaryFile->Close();
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Settings/BuildTarget.pm b/docs/tool/Modules/NaturalDocs/Settings/BuildTarget.pm
new file mode 100644
index 00000000..b1a01b9a
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Settings/BuildTarget.pm
@@ -0,0 +1,66 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Settings::BuildTarget
+#
+###############################################################################
+#
+#   A class that stores information about a build target.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Settings::BuildTarget;
+
+use NaturalDocs::DefineMembers 'BUILDER', 'Builder()', 'SetBuilder()',
+                                                 'DIRECTORY', 'Directory()', 'SetDirectory()';
+
+
+#
+#   Constants: Members
+#
+#   The class is implemented as a blessed arrayref with the members below.
+#
+#       BUILDER      - The <NaturalDocs::Builder::Base>-derived object for the target's output format.
+#       DIRECTORY - The output directory of the target.
+#
+
+#
+#   Function: New
+#
+#   Creates and returns a new object.
+#
+#   Parameters:
+#
+#       builder - The <NaturalDocs::Builder::Base>-derived object for the target's output format.
+#       directory - The directory to place the output files in.
+#
+sub New #(builder, directory)
+    {
+    my ($package, $builder, $directory) = @_;
+
+    my $object = [ ];
+    bless $object, $package;
+
+    $object->SetBuilder($builder);
+    $object->SetDirectory($directory);
+
+    return $object;
+    };
+
+
+#
+#   Functions: Member Functions
+#
+#   Builder - Returns the <NaturalDocs::Builder::Base>-derived object for the target's output format.
+#   SetBuilder - Replaces the <NaturalDocs::Builder::Base>-derived object for the target's output format.
+#   Directory - Returns the directory for the target's output files.
+#   SetDirectory - Replaces the directory for the target's output files.
+#
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/SourceDB.pm b/docs/tool/Modules/NaturalDocs/SourceDB.pm
new file mode 100644
index 00000000..a1928f95
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/SourceDB.pm
@@ -0,0 +1,678 @@
+###############################################################################
+#
+#   Package: NaturalDocs::SourceDB
+#
+###############################################################################
+#
+#   SourceDB is an experimental package meant to unify the tracking of various elements in the source code.
+#
+#   Requirements:
+#
+#       - All extension packages must call <RegisterExtension()> before they can be used.
+#
+#
+#   Architecture: The Idea
+#
+#       For quite a while Natural Docs only needed <SymbolTable>.  However, 1.3 introduced the <ClassHierarchy> package
+#       which duplicated some of its functionality to track classes and parent references.  1.4 now needs <ImageReferenceTable>,
+#       so this package was an attempt to isolate the common functionality so the wheel doesn't have to keep being rewritten as
+#       the scope of Natural Docs expands.
+#
+#       SourceDB is designed around <Extensions> and items.  The purposefully vague "items" are anything in the source code
+#       that we need to track the definitions of.  Extensions are the packages to track them, only they're derived from
+#       <NaturalDocs::SourceDB::Extension> and registered with this package instead of being free standing and duplicating
+#       functionality such as watched files.
+#
+#       The architecture on this package isn't comprehensive yet.  As more extensions are added or previously made free standing
+#       packages are migrated to it it will expand to encompass them.  However, it's still experimental so this concept may
+#       eventually be abandoned for something better instead.
+#
+#
+#   Architecture: Assumptions
+#
+#       SourceDB is built around certain assumptions.
+#
+#       One item per file:
+#
+#           SourceDB assumes that only the first item per file with a particular item string is relevant.  For example, if two functions
+#           have the exact same name, there's no way to link to the second one either in HTML or internally so it doesn't matter for
+#           our purposes.  Likewise, if two references are exactly the same they go to the same target, so it doesn't matter whether
+#           there's one or two or a thousand.  All that matters is that at least one reference exists in this file because you only need
+#           to determine whether the entire file gets rebuilt.  If two items are different in some meaningful way, they should generate
+#           different item strings.
+#
+#       Watched file parsing:
+#
+#           SourceDB assumes the parse method is that the information that was stored from Natural Docs' previous run is loaded, a
+#           file is watched, that file is reparsed, and then <AnalyzeWatchedFileChanges()> is called.  When the file is reparsed all
+#           items within it are added the same as if the file was never parsed before.
+#
+#           If there's a new item this time around, that's fine no matter what.  However, a changed item wouldn't normally be
+#           recorded because the previous run's definition is seen as the first one and subsequent ones are ignored.  Also, deleted
+#           items would normally not be recorded either because we're only adding.
+#
+#           The watched file method fixes this because everything is also added to a second, clean database specifically for the
+#           watched file.  Because it starts clean, it always gets the first definition from the current parse which can then be
+#           compared to the original by <AnalyzeWatchedFileChanges()>.  Because it starts clean you can also compare it to the
+#           main database to see if anything was deleted, because it would appear in the main database but not the watched one.
+#
+#           This means that functions like <ChangeDefinition()> and <DeleteDefinition()> should only be called by
+#           <AnalyzeWatchedFileChanges()>.  Externally only <AddDefinition()> should be called.  <DeleteItem()> is okay to be
+#           called externally because entire items aren't managed by the watched file database, only definitions.
+#
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+
+use NaturalDocs::SourceDB::Extension;
+use NaturalDocs::SourceDB::Item;
+use NaturalDocs::SourceDB::ItemDefinition;
+use NaturalDocs::SourceDB::File;
+use NaturalDocs::SourceDB::WatchedFileDefinitions;
+
+
+package NaturalDocs::SourceDB;
+
+
+###############################################################################
+# Group: Types
+
+
+#
+#   Type: ExtensionID
+#
+#   A unique identifier for each <NaturalDocs::SourceDB> extension as given out by <RegisterExtension()>.
+#
+
+
+
+###############################################################################
+# Group: Variables
+
+
+#
+#   array: extensions
+#
+#   An array of <NaturalDocs::SourceDB::Extension>-derived extensions, as added with <RegisterExtension()>.  The indexes
+#   are the <ExtensionIDs> and the values are package references.
+#
+my @extensions;
+
+#
+#   array: extensionUsesDefinitionObjects
+#
+#   An array where the indexes are <ExtensionIDs> and the values are whether that extension uses its own definition class
+#   derived from <NaturalDocs::SourceDB::ItemDefinition> or it just tracks their existence.
+#
+my @extensionUsesDefinitionObjects;
+
+
+
+#
+#   array: items
+#
+#   The array of source items.  The <ExtensionIDs> are the indexes, and the values are hashrefs mapping the item
+#   string to <NaturalDocs::SourceDB::Item>-derived objects.  Hashrefs may be undef.
+#
+my @items;
+
+
+#
+#   hash: files
+#
+#   A hashref mapping source <FileNames> to <NaturalDocs::SourceDB::Files>.
+#
+my %files;
+
+
+#
+#   object: watchedFile
+#
+#   When a file is being watched for changes, will be a <NaturalDocs::SourceDB::File> for that file.  Is undef otherwise.
+#
+#   When the file is parsed, items are added to both this and the version in <files>.  Thus afterwards we can compare the two to
+#   see if any were deleted since the last time Natural Docs was run, because they would be in the <files> version but not this
+#   one.
+#
+my $watchedFile;
+
+
+#
+#   string: watchedFileName
+#
+#   When a file is being watched for changes, will be the <FileName> of the file being watched.  Is undef otherwise.
+#
+my $watchedFileName;
+
+
+#
+#   object: watchedFileDefinitions
+#
+#   When a file is being watched for changes, will be a <NaturalDocs::SourceDB::WatchedFileDefinitions> object.  Is undef
+#   otherwise.
+#
+#   When the file is parsed, items are added to both this and the version in <items>.  Since only the first definition is kept, this
+#   will always have the definition info from the file whereas the version in <items> will have the first definition as of the last time
+#   Natural Docs was run.  Thus they can be compared to see if the definitions of items that existed the last time around have
+#   changed.
+#
+my $watchedFileDefinitions;
+
+
+
+###############################################################################
+# Group: Extension Functions
+
+
+#
+#   Function: RegisterExtension
+#
+#   Registers a <NaturalDocs::SourceDB::Extension>-derived package and returns a unique <ExtensionID> for it.  All extensions
+#   must call this before they can be used.
+#
+#   Registration Order:
+#
+#       The order in which extensions register is important.  Whenever possible, items are added in the order their extensions
+#       registered.  However, items are changed and deleted in the reverse order.  Take advantage of this to minimize
+#       churn between extensions that are dependent on each other.
+#
+#       For example, when symbols are added or deleted they may cause references to be retargeted and thus their files need to
+#       be rebuilt.  However, adding or deleting references never causes the symbols' files to be rebuilt.  So it makes sense that
+#       symbols should be created before references, and that references should be deleted before symbols.
+#
+#   Parameters:
+#
+#       extension - The package or object of the extension.  Must be derived from <NaturalDocs::SourceDB::Extension>.
+#       usesDefinitionObjects - Whether the extension uses its own class derived from <NaturalDocs::SourceDB::ItemDefinition>
+#                                         or simply tracks each definitions existence.
+#
+#   Returns:
+#
+#       An <ExtensionID> unique to the extension.  This should be saved because it's required in functions such as <AddItem()>.
+#
+sub RegisterExtension #(package extension, bool usesDefinitionObjects) => ExtensionID
+    {
+    my ($self, $extension, $usesDefinitionObjects) = @_;
+
+    push @extensions, $extension;
+    push @extensionUsesDefinitionObjects, $usesDefinitionObjects;
+
+    return scalar @extensions - 1;
+    };
+
+
+
+
+###############################################################################
+# Group: File Functions
+
+
+#
+#   Function: Load
+#
+#   Loads the data of the source database and all the extensions.  Will call <NaturalDocs::SourceDB::Extension->Load()> for
+#   all of them, unless there's a situation where all the source files are going to be reparsed anyway in which case it's not needed.
+#
+sub Load
+    {
+    my $self = shift;
+
+    # No point loading if RebuildData is set.
+    if (!NaturalDocs::Settings->RebuildData())
+        {
+        # If any load fails, stop loading the rest and just reparse all the source files.
+        my $success = 1;
+
+        for (my $extension = 0; $extension < scalar @extensions && $success; $extension++)
+            {
+            $success = $extensions[$extension]->Load();
+            };
+
+        if (!$success)
+            {  NaturalDocs::Project->ReparseEverything();  };
+        };
+    };
+
+
+#
+#   Function: Save
+#
+#   Saves the data of the source database and all its extensions.  Will call <NaturalDocs::SourceDB::Extension->Save()> for all
+#   of them.
+#
+sub Save
+    {
+    my $self = shift;
+
+    for (my $extension = scalar @extensions - 1; $extension >= 0; $extension--)
+        {
+        $extensions[$extension]->Save();
+        };
+    };
+
+
+#
+#   Function: PurgeDeletedSourceFiles
+#
+#   Removes all data associated with deleted source files.
+#
+sub PurgeDeletedSourceFiles
+    {
+    my $self = shift;
+
+    my $filesToPurge = NaturalDocs::Project->FilesToPurge();
+
+    # Extension is the outermost loop because we want the extensions added last to have their definitions removed first to cause
+    # the least amount of churn between interdependent extensions.
+    for (my $extension = scalar @extensions - 1; $extension >= 0; $extension--)
+        {
+        foreach my $file (keys %$filesToPurge)
+            {
+            if (exists $files{$file})
+                {
+                my @items = $files{$file}->ListItems($extension);
+
+                foreach my $item (@items)
+                    {
+                    $self->DeleteDefinition($extension, $item, $file);
+                    };
+                }; # file exists
+            }; # each file
+        }; # each extension
+    };
+
+
+
+
+
+###############################################################################
+# Group: Item Functions
+
+
+#
+#   Function: AddItem
+#
+#   Adds the passed item to the database.  This will not work if the item string already exists.  The item added should *not*
+#   already have definitions attached.  Only use this to add blank items and then call <AddDefinition()> instead.
+#
+#   Parameters:
+#
+#       extension - An <ExtensionID>.
+#       itemString - The string serving as the item identifier.
+#       item - An object derived from <NaturalDocs::SourceDB::Item>.
+#
+#   Returns:
+#
+#       Whether the item was added, that is, whether it was the first time this item was added.
+#
+sub AddItem #(ExtensionID extension, string itemString, NaturalDocs::SourceDB::Item item) => bool
+    {
+    my ($self, $extension, $itemString, $item) = @_;
+
+    if (!defined $items[$extension])
+        {  $items[$extension] = { };  };
+
+    if (!exists $items[$extension]->{$itemString})
+        {
+        if ($item->HasDefinitions())
+            {  die "Tried to add an item to SourceDB that already had definitions.";  };
+
+        $items[$extension]->{$itemString} = $item;
+        return 1;
+        };
+
+    return 0;
+    };
+
+
+#
+#   Function: GetItem
+#
+#   Returns the <NaturalDocs::SourceDB::Item>-derived object for the passed <ExtensionID> and item string, or undef if there
+#   is none.
+#
+sub GetItem #(ExtensionID extension, string itemString) => bool
+    {
+    my ($self, $extensionID, $itemString) = @_;
+
+    if (defined $items[$extensionID])
+        {  return $items[$extensionID]->{$itemString};  }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: DeleteItem
+#
+#   Deletes the record of the passed <ExtensionID> and item string.  Do *not* delete items that still have definitions.  Use
+#   <DeleteDefinition()> first.
+#
+#   Parameters:
+#
+#       extension - The <ExtensionID>.
+#       itemString - The item's identifying string.
+#
+#   Returns:
+#
+#       Whether it was successful, meaning whether an entry existed for it.
+#
+sub DeleteItem #(ExtensionID extension, string itemString) => bool
+    {
+    my ($self, $extension, $itemString) = @_;
+
+    if (defined $items[$extension] && exists $items[$extension]->{$itemString})
+        {
+        if ($items[$extension]->{$itemString}->HasDefinitions())
+            {  die "Tried to delete an item from SourceDB that still has definitions.";  };
+
+        delete $items[$extension]->{$itemString};
+        return 1;
+        }
+    else
+        {  return 0;  };
+    };
+
+
+#
+#   Function: HasItem
+#
+#   Returns whether there is an item defined for the passed <ExtensionID> and item string.
+#
+sub HasItem #(ExtensionID extension, string itemString) => bool
+    {
+    my ($self, $extension, $itemString) = @_;
+
+    if (defined $items[$extension])
+        {  return (exists $items[$extension]->{$itemString});  }
+    else
+        {  return 0;  };
+    };
+
+
+#
+#   Function: GetAllItemsHashRef
+#
+#   Returns a hashref of all the items defined for an extension.  *Do not change the contents.*  The keys are the item strings and
+#   the values are <NaturalDocs::SourceDB::Items> or derived classes.
+#
+sub GetAllItemsHashRef #(ExtensionID extension) => hashref
+    {
+    my ($self, $extension) = @_;
+    return $items[$extension];
+    };
+
+
+
+###############################################################################
+# Group: Definition Functions
+
+
+#
+#   Function: AddDefinition
+#
+#   Adds a definition to an item.  Assumes the item was already created with <AddItem()>.  If there's already a definition for this
+#   file in the item, the new definition will be ignored.
+#
+#   Parameters:
+#
+#       extension - The <ExtensionID>.
+#       itemString - The item string.
+#       file - The <FileName> the definition is in.
+#       definition - If you're using a custom <NaturalDocs::SourceDB::ItemDefinition> class, you must include an object for it here.
+#                       Otherwise this parameter is ignored.
+#
+#   Returns:
+#
+#       Whether the definition was added, which is to say, whether this was the first definition for the passed <FileName>.
+#
+sub AddDefinition #(ExtensionID extension, string itemString, FileName file, optional NaturalDocs::SourceDB::ItemDefinition definition) => bool
+    {
+    my ($self, $extension, $itemString, $file, $definition) = @_;
+
+
+    # Items
+
+    my $item = $self->GetItem($extension, $itemString);
+
+    if (!defined $item)
+        {  die "Tried to add a definition to an undefined item in SourceDB.";  };
+
+    if (!$extensionUsesDefinitionObjects[$extension])
+        {  $definition = 1;  };
+
+    my $result = $item->AddDefinition($file, $definition);
+
+
+    # Files
+
+    if (!exists $files{$file})
+        {  $files{$file} = NaturalDocs::SourceDB::File->New();  };
+
+    $files{$file}->AddItem($extension, $itemString);
+
+
+    # Watched File
+
+    if ($self->WatchingFileForChanges())
+        {
+        $watchedFile->AddItem($extension, $itemString);
+
+        if ($extensionUsesDefinitionObjects[$extension])
+            {  $watchedFileDefinitions->AddDefinition($extension, $itemString, $definition);  };
+        };
+
+
+    return $result;
+    };
+
+
+#
+#   Function: ChangeDefinition
+#
+#   Changes the definition of an item.  This function is only used for extensions that use custom
+#   <NaturalDocs::SourceDB::ItemDefinition>-derived classes.
+#
+#   Parameters:
+#
+#       extension - The <ExtensionID>.
+#       itemString - The item string.
+#       file - The <FileName> the definition is in.
+#       definition - The definition, which must be an object derived from <NaturalDocs::SourceDB::ItemDefinition>.
+#
+sub ChangeDefinition #(ExtensionID extension, string itemString, FileName file, NaturalDocs::SourceDB::ItemDefinition definition)
+    {
+    my ($self, $extension, $itemString, $file, $definition) = @_;
+
+    my $item = $self->GetItem($extension, $itemString);
+
+    if (!defined $item)
+        {  die "Tried to change the definition of an undefined item in SourceDB.";  };
+
+    if (!$extensionUsesDefinitionObjects[$extension])
+        {  die "Tried to change the definition of an item in an extension that doesn't use definition objects in SourceDB.";  };
+
+    if (!$item->HasDefinition($file))
+        {  die "Tried to change a definition that doesn't exist in SourceDB.";  };
+
+    $item->ChangeDefinition($file, $definition);
+    $extensions[$extension]->OnChangedDefinition($itemString, $file);
+    };
+
+
+#
+#   Function: GetDefinition
+#
+#   If the extension uses custom <NaturalDocs::SourceDB::ItemDefinition> classes, returns it for the passed definition or undef
+#   if it doesn't exist.  Otherwise returns whether it exists.
+#
+sub GetDefinition #(ExtensionID extension, string itemString, FileName file) => NaturalDocs::SourceDB::ItemDefinition or bool
+    {
+    my ($self, $extension, $itemString, $file) = @_;
+
+    my $item = $self->GetItem($extension, $itemString);
+
+    if (!defined $item)
+        {  return undef;  };
+
+    return $item->GetDefinition($file);
+    };
+
+
+#
+#   Function: DeleteDefinition
+#
+#   Removes the definition for the passed item.  Returns whether it was successful, meaning whether a definition existed for that
+#   file.
+#
+sub DeleteDefinition #(ExtensionID extension, string itemString, FileName file) => bool
+    {
+    my ($self, $extension, $itemString, $file) = @_;
+
+    my $item = $self->GetItem($extension, $itemString);
+
+    if (!defined $item)
+        {  return 0;  };
+
+    my $result = $item->DeleteDefinition($file);
+
+    if ($result)
+        {
+        $files{$file}->DeleteItem($extension, $itemString);
+        $extensions[$extension]->OnDeletedDefinition($itemString, $file, !$item->HasDefinitions());
+        };
+
+    return $result;
+    };
+
+
+#
+#   Function: HasDefinitions
+#
+#   Returns whether there are any definitions for this item.
+#
+sub HasDefinitions #(ExtensionID extension, string itemString) => bool
+    {
+    my ($self, $extension, $itemString) = @_;
+
+    my $item = $self->GetItem($extension, $itemString);
+
+    if (!defined $item)
+        {  return 0;  };
+
+    return $item->HasDefinitions();
+    };
+
+
+#
+#   Function: HasDefinition
+#
+#   Returns whether there is a definition for the passed <FileName>.
+#
+sub HasDefinition #(ExtensionID extension, string itemString, FileName file) => bool
+    {
+    my ($self, $extension, $itemString, $file) = @_;
+
+    my $item = $self->GetItem($extension, $itemString);
+
+    if (!defined $item)
+        {  return 0;  };
+
+    return $item->HasDefinition($file);
+    };
+
+
+
+###############################################################################
+# Group: Watched File Functions
+
+
+#
+#   Function: WatchFileForChanges
+#
+#   Begins watching a file for changes.  Only one file at a time can be watched.
+#
+#   This should be called before a file is parsed so the file info goes both into the main database and the watched file info.
+#   Afterwards you call <AnalyzeWatchedFileChanges()> so item deletions and definition changes can be detected.
+#
+#   Parameters:
+#
+#       filename - The <FileName> to watch.
+#
+sub WatchFileForChanges #(FileName filename)
+    {
+    my ($self, $filename) = @_;
+
+    $watchedFileName = $filename;
+    $watchedFile = NaturalDocs::SourceDB::File->New();
+    $watchedFileDefinitions = NaturalDocs::SourceDB::WatchedFileDefinitions->New();
+    };
+
+
+#
+#   Function: WatchingFileForChanges
+#
+#   Returns whether we're currently watching a file for changes or not.
+#
+sub WatchingFileForChanges # => bool
+    {
+    my $self = shift;
+    return defined $watchedFileName;
+    };
+
+
+#
+#   Function: AnalyzeWatchedFileChanges
+#
+#   Analyzes the watched file for changes.  Will delete and change definitions as necessary.
+#
+sub AnalyzeWatchedFileChanges
+    {
+    my $self = shift;
+
+    if (!$self->WatchingFileForChanges())
+        {  die "Tried to analyze watched file for changes in SourceDB when no file was being watched.";  };
+    if (!$files{$watchedFileName})
+        {  return;  };
+
+
+    # Process extensions last registered to first.
+
+    for (my $extension = scalar @extensions - 1; $extension >= 0; $extension--)
+        {
+        my @items = $files{$watchedFileName}->ListItems($extension);
+
+        foreach my $item (@items)
+            {
+            if ($watchedFile->HasItem($extension, $item))
+                {
+                if ($extensionUsesDefinitionObjects[$extension])
+                    {
+                    my $originalDefinition = $items[$extension]->GetDefinition($watchedFileName);
+                    my $watchedDefinition = $watchedFileDefinitions->GetDefinition($extension, $item);
+
+                    if (!$originalDefinition->Compare($watchedDefinition))
+                        {  $self->ChangeDefinition($extension, $item, $watchedFileName, $watchedDefinition);  };
+                    }
+                }
+            else # !$watchedFile->HasItem($item)
+                {
+                $self->DeleteDefinition($extension, $item, $watchedFileName);
+                };
+            };
+        };
+
+
+    $watchedFile = undef;
+    $watchedFileName = undef;
+    $watchedFileDefinitions = undef;
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/SourceDB/Extension.pm b/docs/tool/Modules/NaturalDocs/SourceDB/Extension.pm
new file mode 100644
index 00000000..c247ea02
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/SourceDB/Extension.pm
@@ -0,0 +1,84 @@
+###############################################################################
+#
+#   Package: NaturalDocs::SourceDB::Extension
+#
+###############################################################################
+#
+#   A base package for all <SourceDB> extensions.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+
+package NaturalDocs::SourceDB::Extension;
+
+
+###############################################################################
+# Group: Interface Functions
+# These functions must be overridden by the derived class.
+
+
+#
+#   Function: Register
+#
+#   Override this function to register the package with <NaturalDocs::SourceDB->RegisterExtension()>.
+#
+sub Register
+    {
+    die "Called SourceDB::Extension->Register().  This function should be overridden by every extension.";
+    };
+
+
+#
+#   Function: Load
+#
+#   Called by <NaturalDocs::SourceDB->Load()> to load the extension's data.  Returns whether it was successful.
+#
+#   *This function might not be called.*  If there's a situation that would cause all the source files to be reparsed anyway,
+#   <NaturalDocs::SourceDB> may skip calling Load() for the remaining extensions.  You should *not* depend on this function
+#   for any critical initialization that needs to happen every time regardless.
+#
+sub Load # => bool
+    {
+    return 1;
+    };
+
+
+#
+#   Function: Save
+#
+#   Called by <NaturalDocs::SourceDB->Save()> to save the extension's data.
+#
+sub Save
+    {
+    };
+
+
+#
+#   Function: OnDeletedDefinition
+#
+#   Called for each definition deleted by <NaturalDocs::SourceDB>.  This is called *after* the definition has been deleted from
+#   the database, so don't expect to be able to read it.
+#
+sub OnDeletedDefinition #(string itemString, FileName file, bool wasLastDefinition)
+    {
+    };
+
+
+#
+#   Function: OnChangedDefinition
+#
+#   Called for each definition changed by <NaturalDocs::SourceDB>.  This is called *after* the definition has been changed, so
+#   don't expect to be able to read the original value.
+#
+sub OnChangedDefinition #(string itemString, FileName file)
+    {
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/SourceDB/File.pm b/docs/tool/Modules/NaturalDocs/SourceDB/File.pm
new file mode 100644
index 00000000..c364c948
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/SourceDB/File.pm
@@ -0,0 +1,129 @@
+###############################################################################
+#
+#   Package: NaturalDocs::SourceDB::File
+#
+###############################################################################
+#
+#   A class used to index items by file.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+
+package NaturalDocs::SourceDB::File;
+
+use NaturalDocs::DefineMembers 'ITEMS';
+
+
+#
+#   Variables: Members
+#
+#   These constants serve as indexes into the object array.
+#
+#   ITEMS - An arrayref where an <ExtensionID> is the index and the members are existence hashrefs of the item strigs defined
+#               in this file.  The arrayref will always exist, but the hashrefs may be undef.
+#
+
+
+#
+#   Function: New
+#
+#   Returns a new object.
+#
+sub New
+    {
+    my $package = shift;
+
+    my $object = [ ];
+    $object->[ITEMS] = [ ];
+
+    bless $object, $package;
+    return $object;
+    };
+
+
+#
+#   Function: AddItem
+#
+#   Adds an item to this file.  Returns whether this added a new item.
+#
+sub AddItem #(ExtensionID extension, string itemString) => bool
+    {
+    my ($self, $extension, $itemString) = @_;
+
+    if (!defined $self->[ITEMS]->[$extension])
+        {
+        $self->[ITEMS]->[$extension] = { $itemString => 1 };
+        return 1;
+        }
+    elsif (!exists $self->[ITEMS]->[$extension]->{$itemString})
+        {
+        $self->[ITEMS]->[$extension]->{$itemString} = 1;
+        return 1;
+        }
+    else
+        {
+        return 0;
+        };
+    };
+
+
+#
+#   Function: HasItem
+#
+#   Returns whether the item exists in this file.
+#
+sub HasItem #(ExtensionID extension, string itemString) => bool
+    {
+    my ($self, $extension, $itemString) = @_;
+
+    if (defined $self->[ITEMS]->[$extension])
+        {  return exists $self->[ITEMS]->[$extension]->{$itemString};  }
+    else
+        {  return 0;  };
+    };
+
+
+#
+#   Function: DeleteItem
+#
+#   Deletes the passed item.  Returns whether it existed.
+#
+sub DeleteItem #(ExtensionID extension, string itemString) => bool
+    {
+    my ($self, $extension, $itemString) = @_;
+
+    if (!defined $self->[ITEMS]->[$extension])
+        {  return 0;  }
+    elsif (exists $self->[ITEMS]->[$extension]->{$itemString})
+        {
+        delete $self->[ITEMS]->[$extension]->{$itemString};
+        return 1;
+        }
+    else
+        {  return 0;  };
+    };
+
+
+#
+#   Function: ListItems
+#
+#   Returns an array of all the item strings defined for a particular extension, or an empty list if none.
+#
+sub ListItems #(ExtensionID extension) => string array
+    {
+    my ($self, $extension) = @_;
+
+    if (defined $self->[ITEMS]->[$extension])
+        {  return keys %{$self->[ITEMS]->[$extension]};  }
+    else
+        {  return ( );  };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/SourceDB/Item.pm b/docs/tool/Modules/NaturalDocs/SourceDB/Item.pm
new file mode 100644
index 00000000..6654465c
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/SourceDB/Item.pm
@@ -0,0 +1,201 @@
+###############################################################################
+#
+#   Package: NaturalDocs::SourceDB::Item
+#
+###############################################################################
+#
+#   A base class for something being tracked in <NaturalDocs::SourceDB>.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+
+package NaturalDocs::SourceDB::Item;
+
+use NaturalDocs::DefineMembers 'DEFINITIONS';
+
+
+#
+#   Variables: Members
+#
+#   The following constants are indexes into the object array.
+#
+#   DEFINITIONS - A hashref that maps <FileNames> to either <NaturalDocs::SourceDB::ItemDefinition>-derived objects or
+#                         serves as an existence hashref depending on whether the extension only tracks existence.  Will be undef if
+#                         there are none.
+#
+
+
+#
+#   Function: New
+#
+#   Creates and returns a new object.
+#
+sub New
+    {
+    my $class = shift;
+
+    my $object = [ ];
+    bless $object, $class;
+
+    return $object;
+    };
+
+
+
+###############################################################################
+#
+#   Group: Definition Functions
+#
+#   These functions should be called by <NaturalDocs::SourceDB>.  You should not be calling them directly.  Call functions
+#   like <NaturalDocs::SourceDB->AddDefinition()> instead.
+#
+
+
+#
+#   Function: AddDefinition
+#
+#   Adds a definition for the passed <FileName>.  If it's already defined, the new definition will be ignored.
+#
+#   Parameters:
+#
+#       file - The <FileName>.
+#       definition - The definition, which must be an object derived from <NaturalDocs::SourceDB::ItemDefinition> or undef if
+#                       the extension only tracks existence.
+#
+#   Returns:
+#
+#       Whether the definition was added, which is to say, whether this was the first definition for the passed <FileName>.
+#
+sub AddDefinition #(FileName file, optional NaturalDocs::SourceDB::ItemDefinition definition) => bool
+    {
+    my ($self, $file, $definition) = @_;
+
+    if (!defined $self->[DEFINITIONS])
+        {  $self->[DEFINITIONS] = { };  };
+
+    if (!exists $self->[DEFINITIONS]->{$file})
+        {
+        if (!defined $definition)
+            {  $definition = 1;  };
+
+        $self->[DEFINITIONS]->{$file} = $definition;
+        return 1;
+        }
+    else
+        {  return 0;  };
+    };
+
+
+#
+#   Function: ChangeDefinition
+#
+#   Changes the definition for the passed <FileName>.
+#
+#   Parameters:
+#
+#       file - The <FileName>.
+#       definition - The definition, which must be an object derived from <NaturalDocs::SourceDB::ItemDefinition>.
+#
+sub ChangeDefinition #(FileName file, NaturalDocs::SourceDB::ItemDefinition definition)
+    {
+    my ($self, $file, $definition) = @_;
+
+    if (!defined $self->[DEFINITIONS] || !exists $self->[DEFINITIONS]->{$file})
+        {  die "Tried to change a non-existant definition in SourceD::Item.";  };
+
+    $self->[DEFINITIONS]->{$file} = $definition;
+    };
+
+
+#
+#   Function: GetDefinition
+#
+#   Returns the <NaturalDocs::SourceDB::ItemDefinition>-derived object for the passed <FileName>, non-zero if it only tracks
+#   existence, or undef if there is no definition.
+#
+sub GetDefinition #(FileName file) => NaturalDocs::SourceDB::ItemDefinition or bool
+    {
+    my ($self, $file) = @_;
+
+    if (defined $self->[DEFINITIONS])
+        {  return $self->[DEFINITIONS]->{$file};  }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: DeleteDefinition
+#
+#   Removes the definition for the passed <FileName>.  Returns whether it was successful, meaning whether a definition existed
+#   for that file.
+#
+sub DeleteDefinition #(FileName file) => bool
+    {
+    my ($self, $file) = @_;
+
+    if (defined $self->[DEFINITIONS])
+        {
+        if (exists $self->[DEFINITIONS]->{$file})
+            {
+            delete $self->[DEFINITIONS]->{$file};
+
+            if (!scalar keys %{$self->[DEFINITIONS]})
+                {  $self->[DEFINITIONS] = undef;  };
+
+            return 1;
+            };
+        };
+
+    return 0;
+    };
+
+
+#
+#   Function: HasDefinitions
+#
+#   Returns whether there are any definitions for this item.
+#
+sub HasDefinitions # => bool
+    {
+    my $self = shift;
+    return (defined $self->[DEFINITIONS]);
+    };
+
+
+#
+#   Function: HasDefinition
+#
+#   Returns whether there is a definition for the passed <FileName>.
+#
+sub HasDefinition #(FileName file) => bool
+    {
+    my ($self, $file) = @_;
+
+    if (defined $self->[DEFINITIONS])
+        {  return (exists $self->[DEFINITIONS]->{$file});  }
+    else
+        {  return 0;  };
+    };
+
+
+#
+#   Function: GetAllDefinitionsHashRef
+#
+#   Returns a hashref of all the definitions of this item.  *Do not change.*  The keys are the <FileNames>, and the values are
+#   either <NaturalDocs::SourceDB::ItemDefinition>-derived objects or it's just an existence hashref if those aren't used.
+#
+sub GetAllDefinitionsHashRef
+    {
+    my $self = shift;
+    return $self->[DEFINITIONS];
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/SourceDB/ItemDefinition.pm b/docs/tool/Modules/NaturalDocs/SourceDB/ItemDefinition.pm
new file mode 100644
index 00000000..ee053fef
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/SourceDB/ItemDefinition.pm
@@ -0,0 +1,45 @@
+###############################################################################
+#
+#   Package: NaturalDocs::SourceDB::ItemDefinition
+#
+###############################################################################
+#
+#   A base class for all item definitions for extensions that track more than existence.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+
+package NaturalDocs::SourceDB::ItemDefinition;
+
+
+#
+#   Function: Compare
+#
+#   Returns whether the definitions are equal.  This version returns true by default, you must override it in your subclasses
+#   to make the results relevant.  This is important for <NaturalDocs::SourceDB->AnalyzeTrackedFileChanges()>.
+#
+#   This will only be called between objects of the same <ExtensionID>.  If you use multiple derived classes for the same
+#   <ExtensionID>, you will have to take that into account yourself.
+#
+#   Parameters:
+#
+#       other - Another <NaturalDocs::SourceDB::ItemDefinition>-derived object to compare this one to.  It will always be from
+#                  the same <ExtensionID>.
+#
+#   Returns:
+#
+#       Whether they are equal.
+#
+sub Compare #(other)
+    {
+    return 1;
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/SourceDB/WatchedFileDefinitions.pm b/docs/tool/Modules/NaturalDocs/SourceDB/WatchedFileDefinitions.pm
new file mode 100644
index 00000000..956c6644
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/SourceDB/WatchedFileDefinitions.pm
@@ -0,0 +1,159 @@
+###############################################################################
+#
+#   Package: NaturalDocs::SourceDB::WatchedFileDefinitions
+#
+###############################################################################
+#
+#   A class to track the definitions appearing in a watched file.  This is only used for extensions that track definition info with
+#   <NaturalDocs::SourceDB::ItemDefinition>-derived objects.  Do not use it for extensions that only track existence.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+
+package NaturalDocs::SourceDB::WatchedFileDefinitions;
+
+
+#
+#   Variables: Members
+#
+#   This object would only have one member, which is an array, so the object itself serves as that member.
+#
+#   <ExtensionIDs> are used as indexes into this object.  Each entry is a hashref that maps item strings to
+#   <NaturalDocs::SourceDB::ItemDefinition>-derived objects.  This is only done for extensions that use those objects to track
+#   definitions, it's not needed for extensions that only track existence.  If there are no definitions, the entry will be undef.
+#
+
+
+#
+#   Function: New
+#
+#   Creates and returns a new object.
+#
+sub New
+    {
+    my $class = shift;
+
+    my $object = [ ];
+    bless $object, $class;
+
+    return $object;
+    };
+
+
+
+###############################################################################
+# Group: Definition Functions
+#
+
+
+#
+#   Function: AddDefinition
+#
+#   Adds a definition for the passed item string.  If it's already defined, the new definition will be ignored.
+#
+#   Parameters:
+#
+#       extension - The <ExtensionID>.
+#       itemString - The item string.
+#       definition - The definition, which must be an object derived from <NaturalDocs::SourceDB::ItemDefinition>.
+#
+#   Returns:
+#
+#       Whether the definition was added, which is to say, whether this was the first definition for the passed <FileName>.
+#
+sub AddDefinition #(ExtensionID extension, string itemString, NaturalDocs::SourceDB::ItemDefinition definition) => bool
+    {
+    my ($self, $extension, $itemString, $definition) = @_;
+
+    if (!defined $self->[$extension])
+        {  $self->[$extension] = { };  };
+
+    if (!exists $self->[$extension]->{$itemString})
+        {
+        $self->[$extension]->{$itemString} = $definition;
+        return 1;
+        }
+    else
+        {  return 0;  };
+    };
+
+
+#
+#   Function: GetDefinition
+#
+#   Returns the <NaturalDocs::SourceDB::ItemDefinition>-derived object for the passed item string  or undef if there is none.
+#
+sub GetDefinition #(ExtensionID extension, string itemString) => NaturalDocs::SourceDB::ItemDefinition
+    {
+    my ($self, $extension, $itemString) = @_;
+
+    if (defined $self->[$extension])
+        {  return $self->[$extension]->{$itemString};  }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: DeleteDefinition
+#
+#   Removes the definition for the passed item string.  Returns whether it was successful, meaning whether a definition existed
+#   for that item.
+#
+sub DeleteDefinition #(ExtensionID extension, string itemString) => bool
+    {
+    my ($self, $extension, $itemString) = @_;
+
+    if (defined $self->[$extension])
+        {
+        if (exists $self->[$extension]->{$itemString})
+            {
+            delete $self->[$extension]->{$itemString};
+
+            if (!scalar keys %{$self->[$extension]})
+                {  $self->[$extension] = undef;  };
+
+            return 1;
+            };
+        };
+
+    return 0;
+    };
+
+
+#
+#   Function: HasDefinitions
+#
+#   Returns whether there are any definitions for this item.
+#
+sub HasDefinitions #(ExtensionID extension) => bool
+    {
+    my ($self, $extension) = @_;
+
+    return (defined $self->[$extension]);
+    };
+
+
+#
+#   Function: HasDefinition
+#
+#   Returns whether there is a definition for the passed item string.
+#
+sub HasDefinition #(ExtensionID extension, string itemString) => bool
+    {
+    my ($self, $extension, $itemString) = @_;
+
+    if (defined $self->[$extension])
+        {  return (exists $self->[$extension]->{$itemString});  }
+    else
+        {  return 0;  };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/StatusMessage.pm b/docs/tool/Modules/NaturalDocs/StatusMessage.pm
new file mode 100644
index 00000000..7d3de079
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/StatusMessage.pm
@@ -0,0 +1,102 @@
+###############################################################################
+#
+#   Package: NaturalDocs::StatusMessage
+#
+###############################################################################
+#
+#   A package to handle status message updates.  Automatically handles <NaturalDocs::Settings->IsQuiet()>.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::StatusMessage;
+
+
+#
+#   var: message
+#   The message to display.
+#
+my $message;
+
+#
+#   var: total
+#   The number of items to work through.
+#
+my $total;
+
+#
+#   var: completed
+#   The number of items completed.
+#
+my $completed;
+
+#
+#   var: lastMessageTime
+#   The time the last message was posted.
+#
+my $lastMessageTime;
+
+
+#
+#   constant: TIME_BETWEEN_UPDATES
+#   The number of seconds that should occur between updates.
+#
+use constant TIME_BETWEEN_UPDATES => 10;
+
+
+
+#
+#   Function: Start
+#
+#   Starts the status message.
+#
+#   Parameters:
+#
+#       message - The message to post.
+#       total - The number of items that are going to be worked through.
+#
+sub Start #(message, total)
+    {
+    my $self = shift;
+
+    if (!NaturalDocs::Settings->IsQuiet())
+        {
+        ($message, $total) = @_;
+        $completed = 0;
+
+        print $message . "\n";
+
+        $lastMessageTime = time();
+        };
+    };
+
+
+#
+#   Function: CompletedItem
+#
+#   Should be called every time an item is completed.
+#
+sub CompletedItem
+    {
+    my $self = shift;
+
+    if (!NaturalDocs::Settings->IsQuiet())
+        {
+        # We scale completed by 100 since we need to anyway to get the percentage.
+
+        $completed += 100;
+
+        if (time() >= $lastMessageTime + TIME_BETWEEN_UPDATES && $completed != $total * 100)
+            {
+            print $message . ' (' . ($completed / $total) . '%)' . "\n";
+            $lastMessageTime = time();
+            };
+        };
+    };
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/SymbolString.pm b/docs/tool/Modules/NaturalDocs/SymbolString.pm
new file mode 100644
index 00000000..c3b85e64
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/SymbolString.pm
@@ -0,0 +1,212 @@
+###############################################################################
+#
+#   Package: NaturalDocs::SymbolString
+#
+###############################################################################
+#
+#   A package to manage <SymbolString> handling throughout the program.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::SymbolString;
+
+
+#
+#   Function: FromText
+#
+#   Extracts and returns a <SymbolString> from plain text.
+#
+#   This should be the only way to get a <SymbolString> from plain text, as the splitting and normalization must be consistent
+#   throughout the application.
+#
+sub FromText #(string textSymbol)
+    {
+    my ($self, $textSymbol) = @_;
+
+    # The internal format of a symbol is all the normalized identifiers separated by 0x1F characters.
+
+    # Convert whitespace and reserved characters to spaces, and condense multiple consecutive ones.
+    $textSymbol =~ tr/ \t\r\n\x1C\x1D\x1E\x1F/ /s;
+
+    # DEPENDENCY: ReferenceString->MakeFrom() assumes all 0x1E characters were removed.
+    # DEPENDENCY: ReferenceString->MakeFrom() assumes this encoding doesn't use 0x1E characters.
+
+    # Remove spaces unless they're separating two alphanumeric/underscore characters.
+    $textSymbol =~ s/^ //;
+    $textSymbol =~ s/ $//;
+    $textSymbol =~ s/(\W) /$1/g;
+    $textSymbol =~ s/ (\W)/$1/g;
+
+    # Remove trailing empty parenthesis, so Function and Function() are equivalent.
+    $textSymbol =~ s/\(\)$//;
+
+    # Split the string into pieces.
+    my @pieces = split(/(\.|::|->)/, $textSymbol);
+    my $symbolString;
+
+    my $lastWasSeparator = 1;
+
+    foreach my $piece (@pieces)
+        {
+        if ($piece =~ /^(?:\.|::|->)$/)
+            {
+            if (!$lastWasSeparator)
+                {
+                $symbolString .= "\x1F";
+                $lastWasSeparator = 1;
+                };
+            }
+        elsif (length $piece)
+            {
+            $symbolString .= $piece;
+            $lastWasSeparator = 0;
+            };
+        # Ignore empty pieces
+        };
+
+    $symbolString =~ s/\x1F$//;
+
+    return $symbolString;
+    };
+
+
+#
+#   Function: ToText
+#
+#   Converts a <SymbolString> to text, using the passed separator.
+#
+sub ToText #(SymbolString symbolString, string separator)
+    {
+    my ($self, $symbolString, $separator) = @_;
+
+    my @identifiers = $self->IdentifiersOf($symbolString);
+    return join($separator, @identifiers);
+    };
+
+
+#
+#   Function: ToBinaryFile
+#
+#   Writes a <SymbolString> to the passed filehandle.  Can also encode an undef.
+#
+#   Parameters:
+#
+#       fileHandle - The filehandle to write to.
+#       symbol - The <SymbolString> to write, or undef.
+#
+#   Format:
+#
+#       > [UInt8: number of identifiers]
+#       >    [AString16: identifier] [AString16: identifier] ...
+#
+#       Undef is represented by a zero for the number of identifiers.
+#
+sub ToBinaryFile #(FileHandle fileHandle, SymbolString symbol)
+    {
+    my ($self, $fileHandle, $symbol) = @_;
+
+    my @identifiers;
+    if (defined $symbol)
+        {  @identifiers = $self->IdentifiersOf($symbol);  };
+
+    print $fileHandle pack('C', scalar @identifiers);
+
+    foreach my $identifier (@identifiers)
+        {
+        print $fileHandle pack('nA*', length($identifier), $identifier);
+        };
+    };
+
+
+#
+#   Function: FromBinaryFile
+#
+#   Loads a <SymbolString> or undef from the filehandle and returns it.
+#
+#   Parameters:
+#
+#       fileHandle - The filehandle to read from.
+#
+#   Returns:
+#
+#       The <SymbolString> or undef.
+#
+#   See also:
+#
+#       See <ToBinaryFile()> for format and dependencies.
+#
+sub FromBinaryFile #(FileHandle fileHandle)
+    {
+    my ($self, $fileHandle) = @_;
+
+    my $raw;
+
+    # [UInt8: number of identifiers or 0 if none]
+
+    read($fileHandle, $raw, 1);
+    my $identifierCount = unpack('C', $raw);
+
+    my @identifiers;
+
+    while ($identifierCount)
+        {
+        # [AString16: identifier] [AString16: identifier] ...
+
+        read($fileHandle, $raw, 2);
+        my $identifierLength = unpack('n', $raw);
+
+        my $identifier;
+        read($fileHandle, $identifier, $identifierLength);
+
+        push @identifiers, $identifier;
+
+        $identifierCount--;
+        };
+
+    if (scalar @identifiers)
+        {  return $self->Join(@identifiers);  }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: IdentifiersOf
+#
+#   Returns the <SymbolString> as an array of identifiers.
+#
+sub IdentifiersOf #(SymbolString symbol)
+    {
+    my ($self, $symbol) = @_;
+    return split(/\x1F/, $symbol);
+    };
+
+
+#
+#   Function: Join
+#
+#   Takes a list of identifiers and/or <SymbolStrings> and returns it as a new <SymbolString>.
+#
+sub Join #(string/SymbolString identifier/symbol, string/SymolString identifier/symbol, ...)
+    {
+    my ($self, @pieces) = @_;
+
+    # Can't have undefs screwing everything up.
+    while (scalar @pieces && !defined $pieces[0])
+        {  shift @pieces;  };
+
+    # We need to test @pieces first because joining on an empty array returns an empty string rather than undef.
+    if (scalar @pieces)
+       {  return join("\x1F", @pieces);  }
+    else
+        {  return undef;  };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/SymbolTable.pm b/docs/tool/Modules/NaturalDocs/SymbolTable.pm
new file mode 100644
index 00000000..02f010f3
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/SymbolTable.pm
@@ -0,0 +1,1984 @@
+###############################################################################
+#
+#   Package: NaturalDocs::SymbolTable
+#
+###############################################################################
+#
+#   A package that handles all the gory details of managing symbols.  It handles where they are defined, which files
+#   reference them, if any are undefined or duplicated, and loading and saving them to a file.
+#
+#   Usage and Dependencies:
+#
+#       - At any time, <RebuildAllIndexes()> can be called.
+#
+#       - <NaturalDocs::Settings>, <NaturalDocs::Languages>, and <NaturalDocs::Project> must be initialized before use.
+#
+#       - <Load()> must be called to initialize the package.  At this point, the <Information Functions> will return the symbol
+#         table as of the last time Natural Docs was run.
+#
+#       - Note that <Load()> and <Save()> only manage <REFERENCE_TEXT> references.  All other reference types must be
+#         managed by their respective classes.  They should be readded after <Load()> to recreate the state of the last time
+#         Natural Docs was run.
+#
+#       - <Purge()> must be called, and then <NaturalDocs::Parser->ParseForInformation()> on all files that have changed so it
+#         can fully resolve the symbol table via the <Modification Functions>.  Afterwards <PurgeResolvingInfo()> can be called
+#         to reclaim some memory, and the symbol table will reflect the current state of the code.
+#
+#       - <Save()> must be called to commit any changes to the symbol table back to disk.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+
+use NaturalDocs::SymbolTable::Symbol;
+use NaturalDocs::SymbolTable::SymbolDefinition;
+use NaturalDocs::SymbolTable::Reference;
+use NaturalDocs::SymbolTable::File;
+use NaturalDocs::SymbolTable::ReferenceTarget;
+use NaturalDocs::SymbolTable::IndexElement;
+
+use strict;
+use integer;
+
+package NaturalDocs::SymbolTable;
+
+
+
+###############################################################################
+# Group: Variables
+
+#
+#   handle: SYMBOLTABLE_FILEHANDLE
+#
+#   The file handle used with <SymbolTable.nd>.
+#
+
+#
+#   hash: symbols
+#
+#   A hash of all <SymbolStrings>.  The keys are the <SymbolStrings> and the values are <NaturalDocs::SymbolTable::Symbol>
+#   objects.
+#
+#   Prior to <PurgeResolvingInfo()>, both defined symbols and symbols that are merely potential interpretations of references
+#   will be here.  Afterwards, only defined symbols will be here.
+#
+my %symbols;
+
+#
+#   hash: references
+#
+#   A hash of all references in the project.  The keys are <ReferenceStrings> and the values are
+#   <NaturalDocs::SymbolTable::Reference> objects.
+#
+#   Prior to <PurgeResolvingInfo()>, all possible interpretations will be stored for each reference.  Afterwards, only the current
+#   interpretation will be.
+#
+my %references;
+
+#
+#   hash: files
+#
+#   A hash of all the files that define symbols and references in the project.  The keys are the <FileNames>, and the values are
+#   <NaturalDocs::SymbolTable::File> objects.
+#
+#   After <PurgeResolvingInfo()>, this hash will be empty.
+#
+my %files;
+
+#
+#   object: watchedFile
+#
+#   A <NaturalDocs::SymbolTable::File> object of the file being watched for changes.  This is compared to the version in <files>
+#   to see if anything was changed since the last parse.
+#
+my $watchedFile;
+
+#
+#   string: watchedFileName
+#
+#   The <FileName> of the watched file, if any.  If there is no watched file, this will be undef.
+#
+my $watchedFileName;
+
+#
+#   hash: watchedFileSymbolDefinitions
+#
+#   A hashref of the symbol definition information for all the <SymbolStrings> in the watched file.  The keys are the symbol strings,
+#   and the values are <NaturalDocs::SymbolTable::SymbolDefinition> objects.
+#
+my %watchedFileSymbolDefinitions;
+
+
+#
+#   hash: indexes
+#
+#   A hash of generated symbol indexes.  The keys are <TopicTypes> and the values are sorted arrayrefs of
+#   <NaturalDocs::SymbolTable::IndexElements>, or undef if its empty.
+#
+my %indexes;
+
+
+#
+#   hash: indexChanges
+#
+#   A hash of all the indexes that have changed.  The keys are the <TopicTypes> and the entries are undef if they have not
+#   changed, or 1 if they have.  The key will not exist if the <TopicType> has not been checked.
+#
+my %indexChanges;
+
+
+#
+#   hash: indexSectionsWithContent
+#
+#   A hash of which sections in an index have content.  The keys are the <TopicTypes> of each index, and the values are
+#   arrayrefs of bools where the first represents symbols, the second numbers, and the rest A-Z.  If there is no information
+#   available for an index, it's entry will not exist here.
+#
+my %indexSectionsWithContent;
+
+
+#
+#   bool: rebuildIndexes
+#
+#   Whether all indexes should be rebuilt regardless of whether they have been changed.
+#
+my $rebuildIndexes;
+
+
+
+###############################################################################
+# Group: Files
+
+
+#
+#   File: SymbolTable.nd
+#
+#   The storage file for the symbol table.
+#
+#   Format:
+#
+#       > [BINARY_FORMAT]
+#       > [VersionInt: app version]
+#
+#       The file starts with the standard <BINARY_FORMAT> <VersionInt> header.
+#
+#       The first stage of the file is for symbol definitions, analogous to <symbols>.
+#
+#       > [SymbolString: symbol or undef to end] ...
+#       >
+#       > [UInt16: number of definitions]
+#       >
+#       >    [AString16: global definition file] [AString16: TopicType]
+#       >       [AString16: prototype] [AString16: summary]
+#       >
+#       >    [AString16: definition file] ...
+#       >
+#       >    ...
+#
+#       These blocks continue until the <SymbolString> is undef.  Only defined symbols will be included in this file, so
+#       number of definitions will never be zero.  The first one is always the global definition.  If a symbol does not have a
+#       prototype or summary, the UInt16 length of the string will be zero.
+#
+#       The second stage is for references, which is analogous to <references>.  Only <REFERENCE_TEXT> references are
+#       stored in this file, and their <Resolving Flags> are implied so they aren't stored either.
+#
+#       > [ReferenceString (no type, resolving flags): reference or undef to end]
+#       >
+#       > [UInt8: number of definition files]
+#       >    [AString16: definition file] [AString16: definition file] ...
+#
+#       These blocks continue until the <ReferenceString> is undef.  Since there can be multiple using <SymbolStrings>, those
+#       continue until the number of identifiers is zero.  Note that all interpretations are rebuilt rather than stored.
+#
+#   See Also:
+#
+#       <File Format Conventions>
+#
+#   Revisions:
+#
+#       1.3:
+#
+#           - Symbol <TopicTypes> were changed from UInt8s to AString16s, now that <TopicTypes> are strings instead of
+#             integer constants.
+#
+#       1.22:
+#
+#           - File format was completely rebuilt to accommodate the new symbol format and to be in binary.  To see the plain text
+#             format prior to 1.22, check out 1.21's version of this file from CVS.  It is too big a change to note here.
+#
+
+
+#
+#   File: IndexInfo.nd
+#
+#   The storage file for information about the indexes.
+#
+#   Format:
+#
+#       > [Standard Header]
+#
+#       The standard binary file header.
+#
+#       > [AString16: index topic name]
+#       > [uint8: symbols have content (0 or 1)]
+#       > [uint8: numbers have content (0 or 1)]
+#       > [uint8: A has content] [uint8: B has content] ...
+#       > ...
+#
+#       Every index that has information about it is stored with the topic type name first, then 28 uint8s that say whether that
+#       part of the index has content or not.  The first is for symbols, the second is for numbers, and the rest are for A-Z.  If an
+#       index's state is unknown, it won't appear in this file.
+#
+#   Revisions:
+#
+#       1.4:
+#
+#           - The file is introduced.
+#
+
+
+
+###############################################################################
+# Group: File Functions
+
+
+#
+#   Function: Load
+#
+#   Loads all data files from disk.
+#
+sub Load
+    {
+    my ($self) = @_;
+
+    $self->LoadSymbolTable();
+    $self->LoadIndexInfo();
+    };
+
+
+#
+#   Function: LoadSymbolTable
+#
+#   Loads <SymbolTable.nd> from disk.
+#
+sub LoadSymbolTable
+    {
+    my ($self) = @_;
+
+    my $fileIsOkay;
+
+    if (!NaturalDocs::Settings->RebuildData() &&
+        open(SYMBOLTABLE_FILEHANDLE, '<' . NaturalDocs::Project->DataFile('SymbolTable.nd')) )
+        {
+        # See if it's binary.
+        binmode(SYMBOLTABLE_FILEHANDLE);
+
+        my $firstChar;
+        read(SYMBOLTABLE_FILEHANDLE, $firstChar, 1);
+
+        if ($firstChar == ::BINARY_FORMAT())
+            {
+            my $version = NaturalDocs::Version->FromBinaryFile(\*SYMBOLTABLE_FILEHANDLE);
+
+            # 1.3 is incompatible with previous versions.
+
+            if (NaturalDocs::Version->CheckFileFormat( $version, NaturalDocs::Version->FromString('1.3') ))
+                {  $fileIsOkay = 1;  }
+            else
+                {  close(SYMBOLTABLE_FILEHANDLE);  };
+            }
+
+        else
+            {  close(SYMBOLTABLE_FILEHANDLE);  };
+        };
+
+
+    if (!$fileIsOkay)
+        {
+        NaturalDocs::Project->ReparseEverything();
+        return;
+        }
+
+    my $raw;
+
+
+    # Symbols
+
+    for (;;)
+        {
+        # [SymbolString: symbol or undef to end]
+
+        my $symbol = NaturalDocs::SymbolString->FromBinaryFile(\*SYMBOLTABLE_FILEHANDLE);
+
+        if (!defined $symbol)
+            {  last;  };
+
+        my $symbolObject = NaturalDocs::SymbolTable::Symbol->New();
+        $symbols{$symbol} = $symbolObject;
+
+        # [UInt16: number of definitions]
+
+        read(SYMBOLTABLE_FILEHANDLE, $raw, 2);
+        my $definitionCount = unpack('n', $raw);
+
+        do
+            {
+            # [AString16: (global?) definition file]
+
+            read(SYMBOLTABLE_FILEHANDLE, $raw, 2);
+            my $fileLength = unpack('n', $raw);
+
+            my $file;
+            read(SYMBOLTABLE_FILEHANDLE, $file, $fileLength);
+
+            # [AString16: TopicType]
+
+            read(SYMBOLTABLE_FILEHANDLE, $raw, 2);
+            my $typeLength = unpack('n', $raw);
+
+            my $type;
+            read(SYMBOLTABLE_FILEHANDLE, $type, $typeLength);
+
+            # [AString16: prototype]
+
+            read(SYMBOLTABLE_FILEHANDLE, $raw, 2);
+            my $prototypeLength = unpack('n', $raw);
+
+            my $prototype;
+            if ($prototypeLength)
+                {  read(SYMBOLTABLE_FILEHANDLE, $prototype, $prototypeLength);  };
+
+            # [AString16: summary]
+
+            read(SYMBOLTABLE_FILEHANDLE, $raw, 2);
+            my $summaryLength = unpack('n', $raw);
+
+            my $summary;
+            if ($summaryLength)
+                {  read(SYMBOLTABLE_FILEHANDLE, $summary, $summaryLength);  };
+
+            $symbolObject->AddDefinition($file, $type, $prototype, $summary);
+
+            # Add it.
+
+            if (!exists $files{$file})
+                {  $files{$file} = NaturalDocs::SymbolTable::File->New();  };
+
+            $files{$file}->AddSymbol($symbol);
+
+            $definitionCount--;
+            }
+        while ($definitionCount);
+        };
+
+
+    # References
+
+    for (;;)
+        {
+        # [ReferenceString (no type, resolving flags): reference or undef to end]
+
+        my $referenceString = NaturalDocs::ReferenceString->FromBinaryFile(\*SYMBOLTABLE_FILEHANDLE,
+                                                                                                              ::BINARYREF_NOTYPE() |
+                                                                                                              ::BINARYREF_NORESOLVINGFLAGS(),
+                                                                                                              ::REFERENCE_TEXT(), undef);
+
+        if (!defined $referenceString)
+            {  last;  };
+
+        my $referenceObject = NaturalDocs::SymbolTable::Reference->New();
+        $references{$referenceString} = $referenceObject;
+
+        # [UInt8: number of definition files]
+
+        read(SYMBOLTABLE_FILEHANDLE, $raw, 1);
+        my $definitionCount = unpack('C', $raw);
+        do
+            {
+            # [AString16: definition file] [AString16: definition file] ...
+
+            read(SYMBOLTABLE_FILEHANDLE, $raw, 2);
+            my $definitionLength = unpack('n', $raw);
+
+            my $definition;
+            read(SYMBOLTABLE_FILEHANDLE, $definition, $definitionLength);
+
+            # Add it.
+
+            $referenceObject->AddDefinition($definition);
+
+            if (!exists $files{$definition})
+                {  $files{$definition} = NaturalDocs::SymbolTable::File->New();  };
+
+            $files{$definition}->AddReference($referenceString);
+
+            $definitionCount--;
+            }
+        while ($definitionCount);
+
+        $self->GenerateInterpretations($referenceString);
+        $self->InterpretReference($referenceString);
+        };
+
+    close(SYMBOLTABLE_FILEHANDLE);
+    };
+
+
+#
+#   Function: LoadIndexInfo
+#
+#   Loads <IndexInfo.nd> from disk.
+#
+sub LoadIndexInfo
+    {
+    my ($self) = @_;
+
+    if (NaturalDocs::Settings->RebuildData())
+        {  return;  };
+
+    my $version = NaturalDocs::BinaryFile->OpenForReading( NaturalDocs::Project->DataFile('IndexInfo.nd') );
+
+    if (!defined $version)
+        {  return;  }
+
+    # The file format hasn't changed since it was introduced.
+    if (!NaturalDocs::Version->CheckFileFormat($version))
+        {
+        NaturalDocs::BinaryFile->Close();
+        return;
+        };
+
+    my $topicTypeName;
+    while ($topicTypeName = NaturalDocs::BinaryFile->GetAString16())
+        {
+        my $topicType = NaturalDocs::Topics->TypeFromName($topicTypeName);
+        my $content = [ ];
+
+        for (my $i = 0; $i < 28; $i++)
+            {  push @$content, NaturalDocs::BinaryFile->GetUInt8();  };
+
+        if (defined $topicType)  # The name in the file could be from a type that was deleted
+            {  $indexSectionsWithContent{$topicType} = $content;  };
+        };
+
+    NaturalDocs::BinaryFile->Close();
+    };
+
+
+#
+#   Function: Purge
+#
+#   Purges the symbol table of all symbols and references from files that no longer have Natural Docs content.
+#
+sub Purge
+    {
+    my ($self) = @_;
+
+    my $filesToPurge = NaturalDocs::Project->FilesToPurge();
+
+    # We do this in two stages.  First we delete all the references, and then we delete all the definitions.  This causes us to go
+    # through the list twice, but it makes sure no purged files get added to the build list.  For example, if we deleted all of
+    # Purge File A's references and definitions, and Purge File B had a reference to one of those symbols, Purge File B
+    # would be added to the build list because one of its references changed.  By removing all the references in all the files
+    # before removing the definitions, we avoid this.
+
+    foreach my $file (keys %$filesToPurge)
+        {
+        if (exists $files{$file})
+            {
+            my @references = $files{$file}->References();
+            foreach my $reference (@references)
+                {  $self->DeleteReference($reference, $file);  };
+            };
+        };
+
+    foreach my $file (keys %$filesToPurge)
+        {
+        if (exists $files{$file})
+            {
+            my @symbols = $files{$file}->Symbols();
+            foreach my $symbol (@symbols)
+                {  $self->DeleteSymbol($symbol, $file);  };
+
+            delete $files{$file};
+            };
+        };
+    };
+
+
+#
+#   Function: Save
+#
+#   Saves all data files to disk.
+#
+sub Save
+    {
+    my ($self) = @_;
+
+    $self->SaveSymbolTable();
+    $self->SaveIndexInfo();
+    };
+
+
+#
+#   Function: SaveSymbolTable
+#
+#   Saves <SymbolTable.nd> to disk.
+#
+sub SaveSymbolTable
+    {
+    my ($self) = @_;
+
+    open (SYMBOLTABLE_FILEHANDLE, '>' . NaturalDocs::Project->DataFile('SymbolTable.nd'))
+        or die "Couldn't save " . NaturalDocs::Project->DataFile('SymbolTable.nd') . ".\n";
+
+    binmode(SYMBOLTABLE_FILEHANDLE);
+
+    print SYMBOLTABLE_FILEHANDLE '' . ::BINARY_FORMAT();
+
+    NaturalDocs::Version->ToBinaryFile(\*SYMBOLTABLE_FILEHANDLE, NaturalDocs::Settings->AppVersion());
+
+
+    # Symbols
+
+    while (my ($symbol, $symbolObject) = each %symbols)
+        {
+        # Only existing symbols.
+        if ($symbolObject->IsDefined())
+            {
+            # [SymbolString: symbol or undef to end]
+
+            NaturalDocs::SymbolString->ToBinaryFile(\*SYMBOLTABLE_FILEHANDLE, $symbol);
+
+            # [UInt16: number of definitions]
+
+            my @definitions = $symbolObject->Definitions();
+            print SYMBOLTABLE_FILEHANDLE pack('n', scalar @definitions);
+
+            # [AString16: global definition file] [AString16: TopicType]
+
+            print SYMBOLTABLE_FILEHANDLE pack('nA*nA*', length $symbolObject->GlobalDefinition(),
+                                                                                   $symbolObject->GlobalDefinition(),
+                                                                                   length $symbolObject->GlobalType(),
+                                                                                   $symbolObject->GlobalType());
+
+            # [AString16: prototype]
+
+            my $prototype = $symbolObject->GlobalPrototype();
+
+            if (defined $prototype)
+                {  print SYMBOLTABLE_FILEHANDLE pack('nA*', length($prototype), $prototype);  }
+            else
+                {  print SYMBOLTABLE_FILEHANDLE pack('n', 0);  };
+
+            # [AString16: summary]
+
+            my $summary = $symbolObject->GlobalSummary();
+
+            if (defined $summary)
+                {  print SYMBOLTABLE_FILEHANDLE pack('nA*', length($summary), $summary);  }
+            else
+                {  print SYMBOLTABLE_FILEHANDLE pack('n', 0);  };
+
+
+            foreach my $definition (@definitions)
+                {
+                if ($definition ne $symbolObject->GlobalDefinition())
+                    {
+                    # [AString16: definition file] [AString16: TopicType]
+
+                    print SYMBOLTABLE_FILEHANDLE pack('nA*nA*', length $definition, $definition,
+                                                                                           length $symbolObject->TypeDefinedIn($definition),
+                                                                                           $symbolObject->TypeDefinedIn($definition));
+
+                    # [AString16: prototype]
+
+                    my $prototype = $symbolObject->PrototypeDefinedIn($definition);
+
+                    if (defined $prototype)
+                        {  print SYMBOLTABLE_FILEHANDLE pack('nA*', length($prototype), $prototype);  }
+                    else
+                        {  print SYMBOLTABLE_FILEHANDLE pack('n', 0);  };
+
+                    # [AString16: summary]
+
+                    my $summary = $symbolObject->SummaryDefinedIn($definition);
+
+                    if (defined $summary)
+                        {  print SYMBOLTABLE_FILEHANDLE pack('nA*', length($summary), $summary);  }
+                    else
+                        {  print SYMBOLTABLE_FILEHANDLE pack('n', 0);  };
+                    };
+                };
+            };
+        };
+
+     # [SymbolString: symbol or undef to end]
+
+     NaturalDocs::SymbolString->ToBinaryFile(\*SYMBOLTABLE_FILEHANDLE, undef);
+
+
+     # References
+
+    while (my ($reference, $referenceObject) = each %references)
+        {
+        my $type = NaturalDocs::ReferenceString->TypeOf($reference);
+
+        if ($type == ::REFERENCE_TEXT())
+            {
+            # [ReferenceString (no type, resolving flags): reference or undef to end]
+
+            NaturalDocs::ReferenceString->ToBinaryFile(\*SYMBOLTABLE_FILEHANDLE, $reference,
+                                                                             ::BINARYREF_NOTYPE() | ::BINARYREF_NORESOLVINGFLAGS());
+
+            # [UInt8: number of definition files]
+
+            my @definitions = $referenceObject->Definitions();
+            print SYMBOLTABLE_FILEHANDLE pack('C', scalar @definitions);
+
+            # [AString16: definition file] [AString16: definition file] ...
+
+            foreach my $definition (@definitions)
+                {
+                print SYMBOLTABLE_FILEHANDLE pack('nA*', length($definition), $definition);
+                };
+            };
+        };
+
+    # [ReferenceString (no type, resolving flags): reference or undef to end]
+
+    NaturalDocs::ReferenceString->ToBinaryFile(\*SYMBOLTABLE_FILEHANDLE, undef,
+                                                                     ::BINARYREF_NOTYPE() | ::BINARYREF_NORESOLVINGFLAGS());
+
+    close(SYMBOLTABLE_FILEHANDLE);
+    };
+
+
+#
+#   Function: SaveIndexInfo
+#
+#   Saves <IndexInfo.nd> to disk.
+#
+sub SaveIndexInfo
+    {
+    my ($self) = @_;
+
+    NaturalDocs::BinaryFile->OpenForWriting( NaturalDocs::Project->DataFile('IndexInfo.nd') );
+
+    while (my ($topicType, $content) = each %indexSectionsWithContent)
+        {
+        NaturalDocs::BinaryFile->WriteAString16( NaturalDocs::Topics->NameOfType($topicType) );
+
+        for (my $i = 0; $i < 28; $i++)
+            {
+            if ($content->[$i])
+                {  NaturalDocs::BinaryFile->WriteUInt8(1);  }
+            else
+                {  NaturalDocs::BinaryFile->WriteUInt8(0);  };
+            };
+        };
+
+    NaturalDocs::BinaryFile->Close();
+    };
+
+
+
+###############################################################################
+# Group: Modification Functions
+# These functions should not be called after <PurgeResolvingInfo()>.
+
+#
+#   Function: AddSymbol
+#
+#   Adds a symbol definition to the table, if it doesn't already exist.  If the definition changes or otherwise requires the files that
+#   reference it to be updated, the function will call <NaturalDocs::Project->RebuildFile()> to make sure that they are.
+#
+#   Parameters:
+#
+#       symbol  - The <SymbolString>.
+#       file        - The <FileName> where it's defined.
+#       type      - The symbol's <TopicType>.
+#       prototype - The symbol's prototype, if applicable.
+#       summary - The symbol's summary, if applicable.
+#
+sub AddSymbol #(symbol, file, type, prototype, summary)
+    {
+    my ($self, $symbol, $file, $type, $prototype, $summary) = @_;
+
+
+    # If the symbol doesn't exist...
+    if (!exists $symbols{$symbol})
+        {
+        # Create the symbol.  There are no references that could be interpreted as this or else it would have existed already.
+
+        my $newSymbol = NaturalDocs::SymbolTable::Symbol->New();
+        $newSymbol->AddDefinition($file, $type, $prototype, $summary);
+
+        $symbols{$symbol} = $newSymbol;
+
+        $self->OnIndexChange($type);
+        NaturalDocs::Project->RebuildFile($file);
+        }
+
+
+    # If the symbol already exists...
+    else
+        {
+        my $symbolObject = $symbols{$symbol};
+
+        # If the symbol isn't defined, i.e. it was a potential interpretation only...
+        if (!$symbolObject->IsDefined())
+            {
+            $symbolObject->AddDefinition($file, $type, $prototype, $summary);
+
+            # See if this symbol provides a better interpretation of any references.  We can assume this symbol has interpretations
+            # because the object won't exist without either that or definitions.
+
+            my %referencesAndScores = $symbolObject->ReferencesAndScores();
+
+            while (my ($referenceString, $referenceScore) = each %referencesAndScores)
+                {
+                my $referenceObject = $references{$referenceString};
+
+                if (!$referenceObject->HasCurrentInterpretation() ||
+                    $referenceScore > $referenceObject->CurrentScore())
+                    {
+                    $referenceObject->SetCurrentInterpretation($symbol);
+                    $self->OnInterpretationChange($referenceString);
+                    };
+                };
+
+            $self->OnIndexChange($type);
+            NaturalDocs::Project->RebuildFile($file);
+            }
+
+        # If the symbol is defined but not in this file...
+        elsif (!$symbolObject->IsDefinedIn($file))
+            {
+            $symbolObject->AddDefinition($file, $type, $prototype, $summary);
+
+            $self->OnIndexChange($type);
+            NaturalDocs::Project->RebuildFile($file);
+
+            # We don't have to check other files because if the symbol is defined it already has a global definiton,
+            # and everything else is either using that or its own definition, and thus wouldn't be affected by this.
+            };
+
+        # If the symbol was already defined in this file, ignore it.
+
+        };
+
+
+    # Add it to the file index.
+
+    if (!exists $files{$file})
+        {  $files{$file} = NaturalDocs::SymbolTable::File->New();  };
+
+    $files{$file}->AddSymbol($symbol);
+
+
+    # Add it to the watched file, if necessary.
+
+    if (defined $watchedFileName)
+        {
+        $watchedFile->AddSymbol($symbol);
+
+        if (!exists $watchedFileSymbolDefinitions{$symbol})
+            {
+            $watchedFileSymbolDefinitions{$symbol} =
+                 NaturalDocs::SymbolTable::SymbolDefinition->New($type, $prototype, $summary);
+            };
+        };
+    };
+
+
+#
+#   Function: AddReference
+#
+#   Adds a reference to the table, if it doesn't already exist.
+#
+#   Parameters:
+#
+#       type        - The <ReferenceType>.
+#       symbol    - The reference <SymbolString>.
+#       scope      - The scope <SymbolString> it appears in.
+#       using       - An arrayref of scope <SymbolStrings> accessible to the reference via "using" statements, or undef if none.
+#       file          - The <FileName> where the reference appears.  This is not required unless the type is <REFERENCE_TEXT>.
+#       resolvingFlags - The <Resolving Flags> of the reference.  They will be ignored if the type is <REFERENCE_TEXT>.
+#
+#   Alternate Parameters:
+#
+#       referenceString - The <ReferenceString> to add.
+#       file - The <FileName> where the reference appears.  This is not required unless the type is <REFERENCE_TEXT>.
+#
+sub AddReference #(type, symbol, scope, using, file, resolvingFlags) or (referenceString, file)
+    {
+    my ($self, $referenceString, $file);
+
+    if (scalar @_ <= 3)
+        {
+        ($self, $referenceString, $file) = @_;
+        }
+    else
+        {
+        my ($type, $symbol, $scope, $using, $resolvingFlags);
+        ($self, $type, $symbol, $scope, $using, $file, $resolvingFlags) = @_;
+
+        $referenceString = NaturalDocs::ReferenceString->MakeFrom($type, $symbol,
+                                                                                                   NaturalDocs::Languages->LanguageOf($file)->Name(),
+                                                                                                   $scope, $using, $resolvingFlags);
+        };
+
+
+    # If the reference doesn't exist...
+    if (!exists $references{$referenceString})
+        {
+        my $referenceObject = NaturalDocs::SymbolTable::Reference->New();
+
+        $references{$referenceString} = $referenceObject;
+
+        $self->GenerateInterpretations($referenceString);
+        $self->InterpretReference($referenceString);
+        }
+
+
+    if (defined $file)
+        {
+        $references{$referenceString}->AddDefinition($file);
+
+
+        # Add it to the file index.
+
+        if (!exists $files{$file})
+            {  $files{$file} = NaturalDocs::SymbolTable::File->New();  };
+
+        $files{$file}->AddReference($referenceString);
+
+
+        # Add it to the watched file, if necessary.
+
+        if (defined $watchedFileName)
+            {  $watchedFile->AddReference($referenceString);  };
+        };
+    };
+
+
+#
+#   Function: WatchFileForChanges
+#
+#   Tracks a file to see if any symbols or references were changed or deleted in ways that would require other files to be rebuilt.
+#   Assumes that after this function call, the entire file will be parsed again, and thus every symbol and reference will go through
+#   <AddSymbol()> and <AddReference()>.  Afterwards, call <AnalyzeChanges()> to handle any differences.
+#
+#   Parameters:
+#
+#       file - The <FileName> to watch.
+#
+sub WatchFileForChanges #(file)
+    {
+    my ($self, $file) = @_;
+
+    $watchedFile = NaturalDocs::SymbolTable::File->New();
+    $watchedFileName = $file;
+    %watchedFileSymbolDefinitions = ( );
+    };
+
+
+#
+#   Function: AnalyzeChanges
+#
+#   Handles any changes found when reparsing a file using <WatchFileForChanges()>.
+#
+sub AnalyzeChanges
+    {
+    my ($self) = @_;
+
+    if (exists $files{$watchedFileName})
+        {
+
+        # Go through the references and remove any that were deleted.  Ones that were added will have already been added to
+        # the table in AddReference().
+
+        my @references = $files{$watchedFileName}->References();
+        foreach my $reference (@references)
+            {
+            if (!$watchedFile->DefinesReference($reference))
+                {  $self->DeleteReference($reference, $watchedFileName);  };
+            };
+        };
+
+    # We have to check if the watched file exists again because DeleteReference() could have removed it.  I'm still not sure how a
+    # file could have references without symbols, but apparently it's happened in the real world because it's crashed on people.
+    if (exists $files{$watchedFileName})
+        {
+        # Go through the symbols.
+
+        my $rebuildFile;
+
+        my @symbols = $files{$watchedFileName}->Symbols();
+        foreach my $symbol (@symbols)
+            {
+            # Delete symbols that don't exist.
+
+            if (!$watchedFile->DefinesSymbol($symbol))
+                {
+                $self->DeleteSymbol($symbol, $watchedFileName);
+                $rebuildFile = 1;
+                }
+
+            else
+                {
+                my $symbolObject = $symbols{$symbol};
+                my $newSymbolDef = $watchedFileSymbolDefinitions{$symbol};
+
+                # Update symbols that changed.
+
+                if ( $symbolObject->TypeDefinedIn($watchedFileName) ne $newSymbolDef->Type() ||
+                     $symbolObject->PrototypeDefinedIn($watchedFileName) ne $newSymbolDef->Prototype() ||
+                     $symbolObject->SummaryDefinedIn($watchedFileName) ne $newSymbolDef->Summary() )
+                    {
+                    $self->OnIndexChange($symbolObject->TypeDefinedIn($watchedFileName));
+                    $self->OnIndexChange($newSymbolDef->Type());
+                    $rebuildFile = 1;
+
+                    $symbolObject->ChangeDefinition($watchedFileName, $newSymbolDef->Type(), $newSymbolDef->Prototype(),
+                                                                       $newSymbolDef->Summary());
+
+                    # If the symbol definition was the global one, we need to update all files that reference it.  If it wasn't, the only file
+                    # that could references it is itself, and the only way the symbol definition could change in the first place was if it was
+                    # itself changed.
+                    if ($symbolObject->GlobalDefinition() eq $watchedFileName)
+                        {
+                        # Rebuild the files that have references to this symbol
+                        my @references = $symbolObject->References();
+                        foreach my $reference (@references)
+                            {
+                            if ($references{$reference}->CurrentInterpretation() eq $symbol)
+                                {  $self->OnTargetSymbolChange($reference);  };
+                            }; # While references
+                        }; # If global definition is watched file
+                    }; # If the symbol definition changed
+                }; # If the symbol still exists
+            }; # foreach symbol in watched file
+
+        if ($rebuildFile)
+            {  NaturalDocs::Project->RebuildFile($watchedFileName);  };
+
+        };
+
+
+    $watchedFile = undef;
+    $watchedFileName = undef;
+    %watchedFileSymbolDefinitions = ( );
+    };
+
+
+#
+#   Function: DeleteReference
+#
+#   Deletes a reference from the table.
+#
+#   Be careful with this function, as deleting a reference means there are no more of them in the file at all.  The tables do not
+#   keep track of how many times references appear in a file.  In these cases you should instead call <WatchFileForChanges()>,
+#   reparse the file, thus readding all the references, and call <AnalyzeChanges()>.
+#
+#   <REFERENCE_TEXT> references should *always* be managed with <WatchFileForChanges()> and <AnalyzeChanges()>.
+#   This function should only be used externally for other types of references.
+#
+#   Parameters:
+#
+#       referenceString - The <ReferenceString>.
+#       file - The <FileName> where the reference is.  This is not required unless the type is <REFERENCE_TEXT>.
+#
+sub DeleteReference #(referenceString, file)
+    {
+    my ($self, $referenceString, $file) = @_;
+
+
+    # If the reference exists...
+    if (exists $references{$referenceString})
+        {
+        my $referenceObject = $references{$referenceString};
+
+        if (defined $file)
+            {  $referenceObject->DeleteDefinition($file);  };
+
+        # If there are no other definitions, or it doesn't use file definitions to begin with...
+        if (!$referenceObject->IsDefined())
+            {
+            my @interpretations = $referenceObject->Interpretations();
+            foreach my $interpretation (@interpretations)
+                {
+                $symbols{$interpretation}->DeleteReference($referenceString);
+                };
+
+            delete $references{$referenceString};
+            };
+
+
+        if (defined $file)
+            {
+            # Remove it from the file index.
+
+            $files{$file}->DeleteReference($referenceString);
+
+            if (!$files{$file}->HasAnything())
+                {  delete $files{$file};  };
+
+            # We don't need to worry about the watched file, since this function will only be called by AnalyzeChanges() and
+            # LoadAndPurge().
+            };
+        };
+    };
+
+
+#
+#   Function: RebuildAllIndexes
+#
+#   When called, it makes sure all indexes are listed as changed by <IndexChanged()>, regardless of whether they actually did
+#   or not.
+#
+#   This can be called at any time.
+#
+sub RebuildAllIndexes
+    {
+    my $self = shift;
+    $rebuildIndexes = 1;
+    };
+
+
+#
+#   Function: PurgeResolvingInfo
+#
+#   Purges unnecessary information from the symbol table after it is fully resolved.  This will reduce the memory footprint for the
+#   build stage.  After calling this function, you can only call the <Information Functions> and <Save()>.
+#
+sub PurgeResolvingInfo
+    {
+    my ($self) = @_;
+
+    # Go through the symbols.  We don't need to keep around potential symbols anymore, nor do we need what references can
+    # be interpreted as the defined ones.
+
+    while (my ($symbol, $symbolObject) = each %symbols)
+        {
+        if ($symbolObject->IsDefined())
+            {  $symbolObject->DeleteAllReferences();  }
+        else
+            {  delete $symbols{$symbol};  };
+        };
+
+
+    # Go through the references.  We don't need any of the interpretations except for the current.
+
+    foreach my $referenceObject (values %references)
+        {  $referenceObject->DeleteAllInterpretationsButCurrent();  };
+
+
+    # We don't need the information by file at all.
+
+    %files = ( );
+    };
+
+
+#
+#   Function: PurgeIndexes
+#
+#   Clears all generated indexes.
+#
+sub PurgeIndexes
+    {
+    my ($self) = @_;
+    %indexes = ( );
+    };
+
+
+###############################################################################
+# Group: Information Functions
+# These functions should not be called until the symbol table is fully resolved.
+
+
+#
+#   Function: References
+#
+#   Returns what the passed reference information resolve to, if anything.  Note that this only works if the reference had
+#   been previously added to the table via <AddReference()> with the exact same parameters.
+#
+#   Parameters:
+#
+#       type     - The <ReferenceType>.
+#       symbol - The reference <SymbolString>.
+#       scope   - The scope <SymbolString> the reference appears in, or undef if none.
+#       using    - An arrayref of scope <SymbolStrings> available to the reference via using statements.
+#       file       - The source <FileName> the reference appears in, or undef if none.
+#       resolvingFlags - The <Resolving Flags> of the reference.  Ignored if the type is <REFERENCE_TEXT>.
+#
+#   Alternate Parameters:
+#
+#       referenceString - The <ReferenceString> to resolve.
+#       file - The source <FileName> the reference appears in, or undef if none.
+#
+#   Returns:
+#
+#       A <NaturalDocs::SymbolTable::ReferenceTarget> object, or undef if the reference doesn't resolve to anything.
+#
+sub References #(type, symbol, scope, using, file, resolvingFlags) or (referenceString, file)
+    {
+    my ($self, $referenceString, $file);
+
+    if (scalar @_ <= 3)
+        {  ($self, $referenceString, $file) = @_;  }
+    else
+        {
+        my ($type, $symbol, $scope, $using, $resolvingFlags);
+        ($self, $type, $symbol, $scope, $using, $file, $resolvingFlags) = @_;
+
+        $referenceString = NaturalDocs::ReferenceString->MakeFrom($type, $symbol,
+                                                                                                  NaturalDocs::Languages->LanguageOf($file)->Name(),
+                                                                                                  $scope, $using, $resolvingFlags);
+        };
+
+    if (exists $references{$referenceString} && $references{$referenceString}->HasCurrentInterpretation())
+        {
+        my $targetSymbol = $references{$referenceString}->CurrentInterpretation();
+        my $targetObject = $symbols{$targetSymbol};
+
+        my $targetFile;
+        my $targetType;
+        my $targetPrototype;
+        my $targetSummary;
+
+        if (defined $file && $targetObject->IsDefinedIn($file))
+            {
+            $targetFile = $file;
+            $targetType = $targetObject->TypeDefinedIn($file);
+            $targetPrototype = $targetObject->PrototypeDefinedIn($file);
+            $targetSummary = $targetObject->SummaryDefinedIn($file);
+            }
+        else
+            {
+            $targetFile = $targetObject->GlobalDefinition();
+            $targetType = $targetObject->GlobalType();
+            $targetPrototype = $targetObject->GlobalPrototype();
+            $targetSummary = $targetObject->GlobalSummary();
+            };
+
+        return NaturalDocs::SymbolTable::ReferenceTarget->New($targetSymbol, $targetFile, $targetType, $targetPrototype,
+                                                                                             $targetSummary);
+        }
+
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: Lookup
+#
+#   Returns information on the passed <SymbolString>, if it exists.  Note that the symbol must be fully resolved.
+#
+#   Parameters:
+#
+#       symbol - The <SymbolString>.
+#       file - The source <FileName> the reference appears in, or undef if none.
+#
+#   Returns:
+#
+#       A <NaturalDocs::SymbolTable::ReferenceTarget> object, or undef if the symbol isn't defined.
+#
+sub Lookup #(symbol, file)
+    {
+    my ($self, $symbol, $file) = @_;
+
+    my $symbolObject = $symbols{$symbol};
+
+    if (defined $symbolObject)
+        {
+        my $targetFile;
+        my $targetType;
+        my $targetPrototype;
+        my $targetSummary;
+
+        if (defined $file && $symbolObject->IsDefinedIn($file))
+            {
+            $targetFile = $file;
+            $targetType = $symbolObject->TypeDefinedIn($file);
+            $targetPrototype = $symbolObject->PrototypeDefinedIn($file);
+            $targetSummary = $symbolObject->SummaryDefinedIn($file);
+            }
+        else
+            {
+            $targetFile = $symbolObject->GlobalDefinition();
+            $targetType = $symbolObject->GlobalType();
+            $targetPrototype = $symbolObject->GlobalPrototype();
+            $targetSummary = $symbolObject->GlobalSummary();
+            };
+
+        return NaturalDocs::SymbolTable::ReferenceTarget->New($symbol, $targetFile, $targetType, $targetPrototype,
+                                                                                             $targetSummary);
+        }
+
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: Index
+#
+#   Returns a symbol index.
+#
+#   Indexes are generated on demand, but they are stored so subsequent calls for the same index will be fast.  Call
+#   <PurgeIndexes()> to clear the generated indexes.
+#
+#   Parameters:
+#
+#       type  - The <TopicType> of symbol to limit the index to, or undef for none.
+#
+#   Returns:
+#
+#       An arrayref of sections.  The first represents all the symbols, the second the numbers, and the rest A through Z.
+#       Each section is a sorted arrayref of <NaturalDocs::SymbolTable::IndexElement> objects.  If a section has no content,
+#       it will be undef.
+#
+sub Index #(type)
+    {
+    my ($self, $type) = @_;
+
+    if (!exists $indexes{$type})
+        {  $indexes{$type} = $self->MakeIndex($type);  };
+
+    return $indexes{$type};
+    };
+
+
+#
+#   Function: HasIndexes
+#
+#   Determines which indexes out of a list actually have content.
+#
+#   Parameters:
+#
+#       types  - An existence hashref of the <TopicTypes> to check for indexes.
+#
+#   Returns:
+#
+#       An existence hashref of all the specified indexes that have content.  Will return an empty hashref if none.
+#
+sub HasIndexes #(types)
+    {
+    my ($self, $types) = @_;
+
+    # EliminationHash is a copy of all the types, and the types will be deleted as they are found.  This allows us to quit early if
+    # we've found all the types because the hash will be empty.  We'll later return the original hash minus what was left over
+    # in here, which are the ones that weren't found.
+    my %eliminationHash = %$types;
+
+    finddefs:
+    foreach my $symbolObject (values %symbols)
+        {
+        foreach my $definition ($symbolObject->Definitions())
+            {
+            delete $eliminationHash{ $symbolObject->TypeDefinedIn($definition) };
+            delete $eliminationHash{ ::TOPIC_GENERAL() };
+
+            if (!scalar keys %eliminationHash)
+                {  last finddefs;  };
+            };
+        };
+
+    my $result = { %$types };
+
+    foreach my $type (keys %eliminationHash)
+        {  delete $result->{$type};  };
+
+    return $result;
+    };
+
+
+#
+#   Function: IndexChanged
+#
+#   Returns whether the specified index has changed.
+#
+#   Parameters:
+#
+#       type  - The <TopicType> to limit the index to.
+#
+sub IndexChanged #(TopicType type)
+    {
+    my ($self, $type) = @_;
+    return ($rebuildIndexes || defined $indexChanges{$type});
+    };
+
+
+#
+#   Function: IndexSectionsWithContent
+#
+#   Returns an arrayref of whether each section of the specified index has content.  The first entry will be for symbols, the second
+#   for numbers, and the rest A-Z.  Do not change the arrayref.
+#
+sub IndexSectionsWithContent #(TopicType type)
+    {
+    my ($self, $type) = @_;
+
+    if (!exists $indexSectionsWithContent{$type})
+        {
+        # This is okay because Index() stores generated indexes.  It's not an expensive operation unless the index was never asked
+        # for before or it will never be asked for otherwise, and this shouldn't be the case.
+
+        my $index = $self->Index($type);
+        my $content = [ ];
+
+        for (my $i = 0; $i < 28; $i++)
+            {
+            push @$content, (defined $index->[$i] ? 1 : 0);
+            };
+
+        $indexSectionsWithContent{$type} = $content;
+        };
+
+    return $indexSectionsWithContent{$type};
+    };
+
+
+
+###############################################################################
+# Group: Event Handlers
+
+
+#
+#   Function: OnIndexChange
+#
+#   Called whenever a change happens to a symbol that would cause an index to be regenerated.
+#
+#   Parameters:
+#
+#       type - The <TopicType> of the symbol that caused the change.
+#
+sub OnIndexChange #(TopicType type)
+    {
+    my ($self, $type) = @_;
+
+    $indexChanges{$type} = 1;
+    $indexChanges{::TOPIC_GENERAL()} = 1;
+    delete $indexSectionsWithContent{$type};
+    };
+
+
+#
+#   Function: OnInterpretationChange
+#
+#   Called whenever the current interpretation of a reference changes, meaning it switched from one symbol to another.
+#
+#   Parameters:
+#
+#       referenceString - The <ReferenceString> whose current interpretation changed.
+#
+sub OnInterpretationChange #(referenceString)
+    {
+    my ($self, $referenceString) = @_;
+    my $referenceType = NaturalDocs::ReferenceString->TypeOf($referenceString);
+
+    if ($referenceType == ::REFERENCE_TEXT())
+        {
+        my @referenceDefinitions = $references{$referenceString}->Definitions();
+
+        foreach my $referenceDefinition (@referenceDefinitions)
+            {
+            NaturalDocs::Project->RebuildFile($referenceDefinition);
+            };
+        }
+
+    elsif (NaturalDocs::Constants->IsClassHierarchyReference($referenceType))
+        {
+        NaturalDocs::ClassHierarchy->OnInterpretationChange($referenceString);
+        };
+    };
+
+
+#
+#   Function: OnTargetSymbolChange
+#
+#   Called whenever the symbol that serves as the interpretation of a reference changes, but the reference still resolves to
+#   the same symbol.  This would happen if the type, prototype, summary, or which file serves as global definition of the symbol
+#   changes.
+#
+#   Parameters:
+#
+#       referenceString - The <ReferenceString> whose interpretation's symbol changed.
+#
+sub OnTargetSymbolChange #(referenceString)
+    {
+    my ($self, $referenceString) = @_;
+    my $referenceType = NaturalDocs::ReferenceString->TypeOf($referenceString);
+
+    if ($referenceType == ::REFERENCE_TEXT())
+        {
+        my @referenceDefinitions = $references{$referenceString}->Definitions();
+
+        foreach my $referenceDefinition (@referenceDefinitions)
+            {
+            NaturalDocs::Project->RebuildFile($referenceDefinition);
+            };
+        }
+
+    elsif (NaturalDocs::Constants->IsClassHierarchyReference($referenceType))
+        {
+        NaturalDocs::ClassHierarchy->OnTargetSymbolChange($referenceString);
+        };
+    };
+
+
+
+###############################################################################
+# Group: Support Functions
+
+
+#
+#   Function: DeleteSymbol
+#
+#   Removes a symbol definition from the table.  It will call <OnInterpretationChange()> for all references that have it as their
+#   current interpretation.
+#
+#   External code should not attempt to delete symbols using this function.  Instead it should call <WatchFileFoChanges()>,
+#   reparse the file, and call <AnalyzeChanges()>.
+#
+#   Parameters:
+#
+#       symbol - The <SymbolString>.
+#       file       - The <FileName> where the definition is.
+#
+sub DeleteSymbol #(symbol, file)
+    {
+    my ($self, $symbol, $file) = @_;
+
+
+    # If the symbol and definition exist...
+    if (exists $symbols{$symbol} && $symbols{$symbol}->IsDefinedIn($file))
+        {
+        my $symbolObject = $symbols{$symbol};
+        my $wasGlobal = ($symbolObject->GlobalDefinition() eq $file);
+
+        $self->OnIndexChange($symbolObject->TypeDefinedIn($file));
+
+        $symbolObject->DeleteDefinition($file);
+
+        # If this was one definition of many...
+        if ($symbolObject->IsDefined())
+            {
+
+            # If this was the global definition...
+            if ($wasGlobal)
+                {
+                # Update every file that referenced the global symbol; i.e. every file that doesn't have its own definition.
+
+                my @references = $symbolObject->References();
+
+                foreach my $reference (@references)
+                    {
+                    if ($references{$reference}->CurrentInterpretation() eq $symbol)
+                        {
+                        $self->OnTargetSymbolChange($reference);
+                        };
+                    };
+                }
+
+            # If this wasn't the global definition...
+            else
+                {
+                # It's a safe bet that we don't need to do anything here.  The only thing that we even need to look for here is if the
+                # file referenced its own symbol and thus should be rebuilt.  However, if the file is having a symbol deleted, it either
+                # changed or was itself deleted.  If it changed and still has other Natural Docs content, it should already be on the
+                # rebuild list.  If it was deleted or no longer has Natural Docs content, we certainly don't want to add it to the rebuild
+                # list.
+                };
+            }
+
+        # If this is the only definition...
+        else
+            {
+            # If this symbol is the interpretation of any references...
+            if ($symbolObject->HasReferences())
+                {
+                # If this was the current interpretation of any references, reinterpret them and rebuild their files.
+
+                my @references = $symbolObject->References();
+
+                foreach my $reference (@references)
+                    {
+                    if ($references{$reference}->CurrentInterpretation() eq $symbol)
+                        {
+                        $self->InterpretReference($reference);
+                        $self->OnInterpretationChange($reference);
+                        };
+                    };
+                }
+
+            # If there are no interpretations of the symbol...
+            else
+                {
+                # Delete the symbol entirely.
+                delete $symbols{$symbol};
+                };
+            };
+
+        # Remove it from the file index.
+
+        $files{$file}->DeleteSymbol($symbol);
+
+        if (!$files{$file}->HasAnything())
+            {  delete $files{$file};  };
+
+
+        # We don't need to worry about the watched file, since this function will only be called by AnalyzeChanges() and
+        # LoadAndPurge().
+        };
+    };
+
+
+#
+#   Function: GenerateInterpretations
+#
+#   Generates the list of interpretations for the passed reference.  Also creates potential symbols as necessary.
+#
+#   Parameters:
+#
+#       referenceString - The <ReferenceString> to generate the interpretations of.
+#
+sub GenerateInterpretations #(referenceString)
+    {
+    my ($self, $referenceString) = @_;
+
+    my ($type, $symbol, $languageName, $scope, $using, $resolvingFlags) =
+        NaturalDocs::ReferenceString->InformationOf($referenceString);
+
+    # RESOLVE_NOPLURAL is handled by having @singulars be empty.
+    my @singulars;
+    if (!($resolvingFlags & ::RESOLVE_NOPLURAL()))
+        {  @singulars = $self->SingularInterpretationsOf($symbol);  };
+
+    # Since higher scores are better, we'll start at a high number and decrement.
+    my $score = 50000;
+
+
+    # If RESOLVE_RELATIVE is set, we do all the scope relatives before the global.
+    if ($resolvingFlags & ::RESOLVE_RELATIVE())
+        {
+        $score = $self->GenerateRelativeInterpretations($referenceString, $symbol, \@singulars, $scope, $score);
+        }
+
+    # If neither RESOLVE_RELATIVE nor RESOLVE_ABSOLUTE is set, we only do the local before the global.
+    elsif (!($resolvingFlags & ::RESOLVE_ABSOLUTE()))
+        {
+        $self->AddInterpretation($referenceString, NaturalDocs::SymbolString->Join($scope, $symbol), $score);
+        $score--;
+
+        foreach my $singular (@singulars)
+            {
+            $self->AddInterpretation($referenceString, NaturalDocs::SymbolString->Join($scope, $singular), $score);
+            $score--;
+            };
+        };
+
+
+    # Do the global.
+
+    $self->AddInterpretation($referenceString, $symbol, $score);
+    $score--;
+
+    foreach my $singular (@singulars)
+        {
+        $self->AddInterpretation($referenceString, $singular, $score);
+        $score--;
+        };
+
+
+    # If neither RESOLVE_RELATIVE nor RESOLVE_ABSOLUTE is set, we need to do the rest of the scope relatives after the global.
+    if (!($resolvingFlags & ::RESOLVE_RELATIVE()) && !($resolvingFlags & ::RESOLVE_ABSOLUTE()))
+        {
+        $score = $self->GenerateRelativeInterpretations($referenceString, $symbol, \@singulars, $scope, $score, 1);
+        };
+
+
+    # Finally, if RESOLVE_NOUSING isn't set, go through the using scopes.
+    if (!($resolvingFlags & ::RESOLVE_NOUSING()) && defined $using)
+        {
+        foreach my $usingScope (@$using)
+            {
+            if ($resolvingFlags & ::RESOLVE_ABSOLUTE())
+                {
+                $self->AddInterpretation($referenceString, NaturalDocs::SymbolString->Join($usingScope, $symbol), $score);
+                $score--;
+
+                foreach my $singular (@singulars)
+                    {
+                    $self->AddInterpretation($referenceString, NaturalDocs::SymbolString->Join($usingScope, $singular), $score);
+                    $score--;
+                    };
+                }
+            else
+                {
+                $score = $self->GenerateRelativeInterpretations($referenceString, $symbol, \@singulars, $usingScope, $score);
+                };
+            };
+        };
+    };
+
+
+#
+#   Function: GenerateRelativeInterpretations
+#
+#   Generates the list of relative interpretations for the passed reference and packages.  Also creates potential symbols as
+#   necessary.
+#
+#   This function will _not_ create global interpretations.  It _will_ create a local interpretations (symbol + all packages) unless
+#   you set dontUseFull.
+#
+#   Parameters:
+#
+#       referenceString - The <ReferenceString> to generate interpretations for.
+#       symbol - The <SymbolString> to generate interpretations of.
+#       singulars - A reference to an array of singular <SymbolStrings> to also generate interpretations of.  Set to an empty array
+#                       if none.
+#       package - The package <SymbolString> to use.  May be undef.
+#       score - The starting score to apply.
+#       dontUseFull - Whether to not generate an interpretation including the full package identifier.  If set, generated interpretations
+#                           will start one level down.
+#
+#   Returns:
+#
+#       The next unused score.  This is basically the passed score minus the number of interpretations created.
+#
+sub GenerateRelativeInterpretations #(referenceString, symbol, singulars, package, score, dontUseFull)
+    {
+    my ($self, $referenceString, $symbol, $singulars, $package, $score, $dontUseFull) = @_;
+
+    my @packages = NaturalDocs::SymbolString->IdentifiersOf($package);
+
+    # The last package index to include.  This number is INCLUSIVE!
+    my $packageLevel = scalar @packages - 1;
+
+    if ($dontUseFull)
+        {  $packageLevel--;  };
+
+    while ($packageLevel >= 0)
+        {
+        $self->AddInterpretation($referenceString, NaturalDocs::SymbolString->Join(@packages[0..$packageLevel], $symbol),
+                                             $score);
+        $score--;
+
+        foreach my $singular (@$singulars)
+            {
+            $self->AddInterpretation($referenceString, NaturalDocs::SymbolString->Join(@packages[0..$packageLevel], $singular),
+                                                 $score);
+            $score--;
+            };
+
+        $packageLevel--;
+        };
+
+    return $score;
+    };
+
+
+#
+#   Function: SingularInterpretationsOf
+#
+#   Generates singular interpretations of a <SymbolString> if it can be interpreted as a plural.  Not all of them will be valid singular
+#   forms, but that doesn't matter since it's incredibly unlikely an invalid form would exist as a symbol.  What matters is that the
+#   legimate singular is present on the list.
+#
+#   Parameters:
+#
+#       symbol - The <SymbolString>.
+#
+#   Returns:
+#
+#       An array of potential singular interpretations as <SymbolStrings>, in no particular order.  If the symbol can't be interpreted
+#       as a plural, returns an empty array.
+#
+sub SingularInterpretationsOf #(symbol)
+    {
+    my ($self, $symbol) = @_;
+
+    my @identifiers = NaturalDocs::SymbolString->IdentifiersOf($symbol);
+    my $lastIdentifier = pop @identifiers;
+    my $preIdentifiers = NaturalDocs::SymbolString->Join(@identifiers);
+
+    my @results;
+
+    # First cut off any 's or ' at the end, since they can appear after other plural forms.
+    if ($lastIdentifier =~ s/\'s?$//i)
+        {
+        push @results, NaturalDocs::SymbolString->Join($preIdentifiers, $lastIdentifier);
+        };
+
+    # See http://www.gsu.edu/~wwwesl/egw/crump.htm for a good list of potential plural forms.  There are a couple more than
+    # listed below, but they're fairly rare and this is already seriously over-engineered.  This is split by suffix length to make
+    # comparisons more efficient.
+
+    # The fact that this will generate some impossible combinations (leaves => leave, leav, leaf, leafe) doesn't matter.  It's very
+    # unlikely that more than one will manage to match a defined symbol.  Even if they do (leave, leaf), it's incredibly unlikely
+    # that someone has defined an impossible one (leav, leafe).  So it's not so important that we remove impossible combinations,
+    # just that we include all the possible ones.
+
+    my @suffixGroups = ( [ 's', undef,  # boys => boy
+                                       'i', 'us',  # alumni => alumnus
+                                       'a', 'um', # errata => erratum
+                                       'a', 'on' ],  # phenomena => phenomenon
+
+                                    [ 'es', undef,  # foxes => fox
+                                      'ae', 'a' ],  # amoebae => amoeba
+
+                                    [ 'ies', 'y',  # pennies => penny
+                                      'ves', 'f',  # calves => calf
+                                      'ves', 'fe',  # knives => knife
+                                      'men', 'man',  # women => woman
+                                      'ice', 'ouse',  # mice => mouse
+                                      'oes', 'o',  # vetoes => veto
+                                      'ces', 'x',  # matrices => matrix
+                                      'xen', 'x' ],  # oxen => ox
+
+                                    [ 'ices', 'ex',  # indices => index
+                                      'feet', 'foot',  # feet => foot
+                                      'eese', 'oose',  # geese => goose
+                                      'eeth', 'ooth',  # teeth => tooth
+                                      'dren', 'd' ] );  # children => child
+
+    my $suffixLength = 1;
+
+    foreach my $suffixGroup (@suffixGroups)
+        {
+        my $identifierSuffix = lc( substr($lastIdentifier, 0 - $suffixLength) );
+        my $cutIdentifier = substr($lastIdentifier, 0, 0 - $suffixLength);
+
+        for (my $i = 0; $i + 1 < scalar @$suffixGroup; $i += 2)
+            {
+            my $suffix = $suffixGroup->[$i];
+            my $replacement = $suffixGroup->[$i + 1];
+
+            if ($identifierSuffix eq $suffix)
+                {
+                if (defined $replacement)
+                    {
+                    push @results, NaturalDocs::SymbolString->Join($preIdentifiers, $cutIdentifier . $replacement);
+                    push @results, NaturalDocs::SymbolString->Join($preIdentifiers, $cutIdentifier . uc($replacement));
+                    }
+                else
+                    {
+                    push @results, NaturalDocs::SymbolString->Join($preIdentifiers, $cutIdentifier);
+                    };
+                };
+            };
+
+        $suffixLength++;
+        };
+
+    return @results;
+    };
+
+
+#
+#   Function: AddInterpretation
+#
+#   Adds an interpretation to an existing reference.  Creates potential symbols as necessary.
+#
+#   Parameters:
+#
+#       referenceString - The <ReferenceString> to add the interpretation to.
+#       symbol - The <SymbolString> the reference can be interpreted as.
+#       score - The score of the interpretation.
+#
+sub AddInterpretation #(referenceString, symbol, score)
+    {
+    my ($self, $referenceString, $symbol, $score) = @_;
+
+    $references{$referenceString}->AddInterpretation($symbol, $score);
+
+    # Create a potential symbol if it doesn't exist.
+
+    if (!exists $symbols{$symbol})
+        {  $symbols{$symbol} = NaturalDocs::SymbolTable::Symbol->New();  };
+
+    $symbols{$symbol}->AddReference($referenceString, $score);
+    };
+
+
+#
+#   Function: InterpretReference
+#
+#   Interprets the passed reference, matching it to the defined symbol with the highest score.  If the symbol is already
+#   interpreted, it will reinterpret it.  If there are no matches, it will make it an undefined reference.
+#
+#   Parameters:
+#
+#       referenceString - The <ReferenceString> to interpret.
+#
+sub InterpretReference #(referenceString)
+    {
+    my ($self, $referenceString) = @_;
+
+    my $interpretation;
+    my $currentInterpretation;
+    my $score;
+    my $currentScore = -1;
+
+    my $referenceObject = $references{$referenceString};
+
+    my %interpretationsAndScores = $referenceObject->InterpretationsAndScores();
+    while ( ($interpretation, $score) = each %interpretationsAndScores )
+        {
+        if ($score > $currentScore && $symbols{$interpretation}->IsDefined())
+            {
+            $currentScore = $score;
+            $currentInterpretation = $interpretation;
+            };
+        };
+
+    if ($currentScore > -1)
+        {  $referenceObject->SetCurrentInterpretation($currentInterpretation);  }
+    else
+        {  $referenceObject->SetCurrentInterpretation(undef);  };
+    };
+
+
+#
+#   Function: MakeIndex
+#
+#   Generates a symbol index.
+#
+#   Parameters:
+#
+#       type  - The <TopicType> to limit the index to.
+#
+#   Returns:
+#
+#       An arrayref of sections.  The first represents all the symbols, the second the numbers, and the rest A through Z.
+#       Each section is a sorted arrayref of <NaturalDocs::SymbolTable::IndexElement> objects.  If a section has no content,
+#       it will be undef.
+#
+sub MakeIndex #(type)
+    {
+    my ($self, $type) = @_;
+
+
+    # Go through the symbols and generate IndexElements for any that belong in the index.
+
+    # Keys are the symbol strings, values are IndexElements.
+    my %indexSymbols;
+
+    while (my ($symbolString, $object) = each %symbols)
+        {
+        my ($symbol, $package) = $self->SplitSymbolForIndex($symbolString, $object->GlobalType());
+        my @definitions = $object->Definitions();
+
+        foreach my $definition (@definitions)
+            {
+            my $definitionType = $object->TypeDefinedIn($definition);
+
+            if ($type eq ::TOPIC_GENERAL() || $type eq $definitionType)
+                {
+                if (!exists $indexSymbols{$symbol})
+                    {
+                    $indexSymbols{$symbol} =
+                        NaturalDocs::SymbolTable::IndexElement->New($symbol, $package, $definition, $definitionType,
+                                                                                               $object->PrototypeDefinedIn($definition),
+                                                                                               $object->SummaryDefinedIn($definition) );
+                    }
+                else
+                    {
+                    $indexSymbols{$symbol}->Merge($package, $definition, $definitionType,
+                                                                       $object->PrototypeDefinedIn($definition),
+                                                                       $object->SummaryDefinedIn($definition) );
+                    };
+                }; # If type matches
+            }; # Each definition
+        }; # Each symbol
+
+
+    # Generate sortable symbols for each IndexElement, sort them internally, and divide them into sections.
+
+    my $sections = [ ];
+
+    foreach my $indexElement (values %indexSymbols)
+        {
+        $indexElement->Sort();
+        $indexElement->MakeSortableSymbol();
+
+        my $sectionNumber;
+
+        if ($indexElement->SortableSymbol() =~ /^([a-z])/i)
+            {  $sectionNumber = ord(lc($1)) - ord('a') + 2;  }
+        elsif ($indexElement->SortableSymbol() =~ /^[0-9]/)
+            {  $sectionNumber = 1;  }
+        else
+            {  $sectionNumber = 0;  };
+
+        if (!defined $sections->[$sectionNumber])
+            {  $sections->[$sectionNumber] = [ ];  };
+
+        push @{$sections->[$sectionNumber]}, $indexElement;
+        };
+
+
+    # Sort each section.
+
+    for (my $i = 0; $i < scalar @$sections; $i++)
+        {
+        if (defined $sections->[$i])
+            {
+            @{$sections->[$i]} = sort
+                {
+                my $result = ::StringCompare($a->SortableSymbol(), $b->SortableSymbol());
+
+                if ($result == 0)
+                    {  $result = ::StringCompare($a->IgnoredPrefix(), $b->IgnoredPrefix());  };
+
+                return $result;
+                }
+            @{$sections->[$i]};
+            };
+        };
+
+    return $sections;
+    };
+
+
+#
+#   Function: SplitSymbolForIndex
+#
+#   Splits a <SymbolString> into its symbol and package portions for indexing.
+#
+#   Parameters:
+#
+#       symbol - The <SymbolString>.
+#       type - Its <TopicType>.
+#
+#   Returns:
+#
+#       The array ( symbol, package ), which are both <SymbolStrings>.  If the symbol is global, package will be undef.
+#
+sub SplitSymbolForIndex #(symbol, type)
+    {
+    my ($self, $symbol, $type) = @_;
+
+    my $scope = NaturalDocs::Topics->TypeInfo($type)->Scope();
+
+    if ($scope == ::SCOPE_START() || $scope == ::SCOPE_ALWAYS_GLOBAL())
+        {  return ( $symbol, undef );  }
+    else
+        {
+        my @identifiers = NaturalDocs::SymbolString->IdentifiersOf($symbol);
+
+        $symbol = pop @identifiers;
+        my $package = NaturalDocs::SymbolString->Join(@identifiers);
+
+        return ( $symbol, $package );
+        };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/SymbolTable/File.pm b/docs/tool/Modules/NaturalDocs/SymbolTable/File.pm
new file mode 100644
index 00000000..71f93645
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/SymbolTable/File.pm
@@ -0,0 +1,186 @@
+###############################################################################
+#
+#   Package: NaturalDocs::SymbolTable::File
+#
+###############################################################################
+#
+#   A class representing a file, keeping track of what symbols and references are defined in it.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::SymbolTable::File;
+
+
+###############################################################################
+# Group: Implementation
+
+#
+#   Constants: Members
+#
+#   The class is implemented as a blessed arrayref.  The following constants are its members.
+#
+#       SYMBOLS       - An existence hashref of the <SymbolStrings> it defines.
+#       REFERENCES  - An existence hashref of the <ReferenceStrings> in the file.
+#
+
+# DEPENDENCY: New() depends on the order of these constants.  If they change, New() has to be updated.
+use constant SYMBOLS => 0;
+use constant REFERENCES => 1;
+
+
+###############################################################################
+# Group: Modification Functions
+
+
+#
+#   Function: New
+#
+#   Creates and returns a new object.
+#
+sub New
+    {
+    my $package = shift;
+
+    # Let's make it safe, since normally you can pass values to New.  Having them just be ignored would be an obscure error.
+    if (scalar @_)
+        {  die "You can't pass values to NaturalDocs::SymbolTable::File->New()\n";  };
+
+    # DEPENDENCY: This code depends on the order of the member constants.
+    my $object = [ { }, { } ];
+    bless $object, $package;
+
+    return $object;
+    };
+
+
+#
+#   Function: AddSymbol
+#
+#   Adds a <SymbolString> definition.
+#
+#   Parameters:
+#
+#       symbol - The <SymbolString> being added.
+#
+sub AddSymbol #(symbol)
+    {
+    my ($self, $symbol) = @_;
+    $self->[SYMBOLS]{$symbol} = 1;
+    };
+
+
+#
+#   Function: DeleteSymbol
+#
+#   Removes a <SymbolString> definition.
+#
+#   Parameters:
+#
+#       symbol - The <SymbolString> to delete.
+#
+sub DeleteSymbol #(symbol)
+    {
+    my ($self, $symbol) = @_;
+    delete $self->[SYMBOLS]{$symbol};
+    };
+
+
+#
+#   Function: AddReference
+#
+#   Adds a reference definition.
+#
+#   Parameters:
+#
+#       referenceString - The <ReferenceString> being added.
+#
+sub AddReference #(referenceString)
+    {
+    my ($self, $referenceString) = @_;
+    $self->[REFERENCES]{$referenceString} = 1;
+    };
+
+
+#
+#   Function: DeleteReference
+#
+#   Removes a reference definition.
+#
+#   Parameters:
+#
+#       referenceString - The <ReferenceString> to delete.
+#
+sub DeleteReference #(referenceString)
+    {
+    my ($self, $referenceString) = @_;
+    delete $self->[REFERENCES]{$referenceString};
+    };
+
+
+
+###############################################################################
+# Group: Information Functions
+
+
+#
+#   Function: HasAnything
+#
+#   Returns whether the file has any symbol or reference definitions at all.
+#
+sub HasAnything
+    {
+    return (scalar keys %{$_[0]->[SYMBOLS]} || scalar keys %{$_[0]->[REFERENCES]});
+    };
+
+#
+#   Function: Symbols
+#
+#   Returns an array of all the <SymbolStrings> defined in this file.  If none, returns an empty array.
+#
+sub Symbols
+    {
+    return keys %{$_[0]->[SYMBOLS]};
+    };
+
+
+#
+#   Function: References
+#
+#   Returns an array of all the <ReferenceStrings> defined in this file.  If none, returns an empty array.
+#
+sub References
+    {
+    return keys %{$_[0]->[REFERENCES]};
+    };
+
+
+#
+#   Function: DefinesSymbol
+#
+#   Returns whether the file defines the passed <SymbolString> or not.
+#
+sub DefinesSymbol #(symbol)
+    {
+    my ($self, $symbol) = @_;
+    return exists $self->[SYMBOLS]{$symbol};
+    };
+
+
+#
+#   Function: DefinesReference
+#
+#   Returns whether the file defines the passed <ReferenceString> or not.
+#
+sub DefinesReference #(referenceString)
+    {
+    my ($self, $referenceString) = @_;
+    return exists $self->[REFERENCES]{$referenceString};
+    };
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/SymbolTable/IndexElement.pm b/docs/tool/Modules/NaturalDocs/SymbolTable/IndexElement.pm
new file mode 100644
index 00000000..e54ce44c
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/SymbolTable/IndexElement.pm
@@ -0,0 +1,522 @@
+###############################################################################
+#
+#   Class: NaturalDocs::SymbolTable::IndexElement
+#
+###############################################################################
+#
+#   A class representing part of an indexed symbol.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use Tie::RefHash;
+
+use strict;
+use integer;
+
+
+package NaturalDocs::SymbolTable::IndexElement;
+
+
+#
+#   Topic: How IndexElements Work
+#
+#   This is a little tricky, so make sure you understand this.  Indexes are sorted by symbol, then packages, then file.  If there is only
+#   one package for a symbol, or one file definition for a package/symbol, they are added inline to the entry.  However, if there are
+#   multiple packages or files, the function for it returns an arrayref of IndexElements instead.  Which members are defined and
+#   undefined should follow common sense.  For example, if a symbol is defined in multiple packages, the symbol's IndexElement
+#   will not define <File()>, <Type()>, or <Prototype()>; those will be defined in child elements.  Similarly, the child elements will
+#   not define <Symbol()> since it's redundant.
+#
+#   Diagrams may be clearer.  If a member isn't listed for an element, it isn't defined.
+#
+#   A symbol that only has one package and file:
+#   > [Element]
+#   > - Symbol
+#   > - Package
+#   > - File
+#   > - Type
+#   > - Prototype
+#   > - Summary
+#
+#   A symbol that is defined by multiple packages, each with only one file:
+#   > [Element]
+#   > - Symbol
+#   > - Package
+#   >     [Element]
+#   >     - Package
+#   >     - File
+#   >     - Type
+#   >     - Prototype
+#   >     - Summary
+#   >     [Element]
+#   >     - ...
+#
+#   A symbol that is defined by one package, but has multiple files
+#   > [Element]
+#   > - Symbol
+#   > - Package
+#   > - File
+#   >    [Element]
+#   >    - File
+#   >    - Type
+#   >    - Protype
+#   >    - Summary
+#   >    [Element]
+#   >    - ...
+#
+#   A symbol that is defined by multiple packages which have multiple files:
+#   > [Element]
+#   > - Symbol
+#   > - Package
+#   >    [Element]
+#   >    - Package
+#   >    - File
+#   >      [Element]
+#   >      - File
+#   >      - Type
+#   >      - Prototype
+#   >      - Summary
+#   >      [Element]
+#   >      - ...
+#   >    [Element]
+#   >    - ...
+#
+#   Why is it done this way?:
+#
+#   Because it makes it easier to generate nice indexes since all the splitting and combining is done for you.  If a symbol
+#   has only one package, you just want to link to it, you don't want to break out a subindex for just one package.  However, if
+#   it has multiple package, you do want the subindex and to link to each one individually.  Use <HasMultiplePackages()> and
+#   <HasMultipleFiles()> to determine whether you need to add a subindex for it.
+#
+#
+#   Combining Properties:
+#
+#   All IndexElements also have combining properties set.
+#
+#   CombinedType - The general <TopicType> of the entry.  Conflicts combine into <TOPIC_GENERAL>.
+#   PackageSeparator - The package separator symbol of the entry.  Conflicts combine into a dot.
+#
+#   So if an IndexElement only has one definition, <CombinedType()> is the same as the <TopicType> and <PackageSeparator()>
+#   is that of the definition's language.  If other definitions are added and they have the same properties, the combined properties
+#   will remain the same.  However, if they're different, they switch values as noted above.
+#
+#
+#   Sortable Symbol:
+#
+#   <SortableSymbol()> is a pseudo-combining property.  There were a few options for dealing with multiple languages defining
+#   the same symbol but stripping different prefixes off it, but ultimately I decided to go with whatever the language does that
+#   has the most definitions.  There's not likely to be many conflicts here in the real world; probably the only thing would be
+#   defining it in a text file and forgetting to specify the prefixes to strip there too.  So this works.
+#
+#   Ties are broken pretty much randomly, except that text files always lose if its one of the options.
+#
+#   It's a pseudo-combining property because it's done after the IndexElements are all filled in and only stored in the top-level
+#   ones.
+#
+
+
+###############################################################################
+# Group: Implementation
+
+#
+#   Constants: Members
+#
+#   The class is implemented as a blessed arrayref.  The following constants are its members.
+#
+#   SYMBOL - The <SymbolString> without the package portion.
+#   PACKAGE - The package <SymbolString>.  Will be a package <SymbolString>, undef for global, or an arrayref of
+#                    <NaturalDocs::SymbolTable::IndexElement> objects if multiple packages define the symbol.
+#   FILE - The <FileName> the package/symbol is defined in.  Will be the file name or an arrayref of
+#            <NaturalDocs::SymbolTable::IndexElements> if multiple files define the package/symbol.
+#   TYPE - The package/symbol/file <TopicType>.
+#   PROTOTYPE - The package/symbol/file prototype, or undef if not applicable.
+#   SUMMARY - The package/symbol/file summary, or undef if not applicable.
+#   COMBINED_TYPE - The combined <TopicType> of the element.
+#   PACKAGE_SEPARATOR - The combined package separator symbol of the element.
+#   SORTABLE_SYMBOL - The sortable symbol as a text string.
+#   IGNORED_PREFIX - The part of the symbol that was stripped off to make the sortable symbol.
+#
+use NaturalDocs::DefineMembers 'SYMBOL', 'Symbol()',
+                                                 'PACKAGE', 'Package()',
+                                                 'FILE', 'File()',
+                                                 'TYPE', 'Type()',
+                                                 'PROTOTYPE', 'Prototype()',
+                                                 'SUMMARY', 'Summary()',
+                                                 'COMBINED_TYPE', 'CombinedType()',
+                                                 'PACKAGE_SEPARATOR', 'PackageSeparator()',
+                                                 'SORTABLE_SYMBOL', 'SortableSymbol()',
+                                                 'IGNORED_PREFIX', 'IgnoredPrefix()';
+# DEPENDENCY: New() depends on the order of these constants and that there is no inheritance..
+
+
+###############################################################################
+# Group: Modification Functions
+
+#
+#   Function: New
+#
+#   Returns a new object.
+#
+#   This should only be used for creating an entirely new symbol.  You should *not* pass arrayrefs as package or file parameters
+#   if you are calling this externally.  Use <Merge()> instead.
+#
+#   Parameters:
+#
+#       symbol  - The <SymbolString> without the package portion.
+#       package - The package <SymbolString>, or undef for global.
+#       file  - The symbol's definition file.
+#       type  - The symbol's <TopicType>.
+#       prototype  - The symbol's prototype, if applicable.
+#       summary  - The symbol's summary, if applicable.
+#
+#   Optional Parameters:
+#
+#       These parameters don't need to be specified.  You should ignore them when calling this externally.
+#
+#       combinedType - The symbol's combined <TopicType>.
+#       packageSeparator - The symbol's combined package separator symbol.
+#
+sub New #(symbol, package, file, type, prototype, summary, combinedType, packageSeparator)
+    {
+    # DEPENDENCY: This depends on the parameter list being in the same order as the constants.
+
+    my $self = shift;
+
+    my $object = [ @_ ];
+    bless $object, $self;
+
+    if (!defined $object->[COMBINED_TYPE])
+        {  $object->[COMBINED_TYPE] = $object->[TYPE];  };
+
+    if (!defined $object->[PACKAGE_SEPARATOR])
+        {
+        if ($object->[TYPE] eq ::TOPIC_FILE())
+            {  $object->[PACKAGE_SEPARATOR] = '.';  }
+        else
+            {
+            $object->[PACKAGE_SEPARATOR] = NaturalDocs::Languages->LanguageOf($object->[FILE])->PackageSeparator();
+            };
+        };
+
+    return $object;
+    };
+
+
+#
+#   Function: Merge
+#
+#   Adds another definition of the same symbol.  Perhaps it has a different package or defining file.
+#
+#   Parameters:
+#
+#       package - The package <SymbolString>, or undef for global.
+#       file  - The symbol's definition file.
+#       type  - The symbol's <TopicType>.
+#       prototype  - The symbol's protoype if applicable.
+#       summary  - The symbol's summary if applicable.
+#
+sub Merge #(package, file, type, prototype, summary)
+    {
+    my ($self, $package, $file, $type, $prototype, $summary) = @_;
+
+    # If there's only one package...
+    if (!$self->HasMultiplePackages())
+        {
+        # If there's one package and it's the same as the new one...
+        if ($package eq $self->Package())
+            {
+            $self->MergeFile($file, $type, $prototype, $summary);
+            }
+
+        # If there's one package and the new one is different...
+        else
+            {
+            my $selfDefinition = NaturalDocs::SymbolTable::IndexElement->New(undef, $self->Package(), $self->File(),
+                                                                                                                 $self->Type(), $self->Prototype(),
+                                                                                                                 $self->Summary(), $self->CombinedType(),
+                                                                                                                 $self->PackageSeparator());
+            my $newDefinition = NaturalDocs::SymbolTable::IndexElement->New(undef, $package, $file, $type, $prototype,
+                                                                                                                  $summary);
+
+            $self->[PACKAGE] = [ $selfDefinition, $newDefinition ];
+            $self->[FILE] = undef;
+            $self->[TYPE] = undef;
+            $self->[PROTOTYPE] = undef;
+            $self->[SUMMARY] = undef;
+
+            if ($newDefinition->Type() ne $self->CombinedType())
+                {  $self->[COMBINED_TYPE] = ::TOPIC_GENERAL();  };
+            if ($newDefinition->PackageSeparator() ne $self->PackageSeparator())
+                {  $self->[PACKAGE_SEPARATOR] = '.';  };
+            };
+        }
+
+    # If there's more than one package...
+    else
+        {
+        # See if the new package is one of them.
+        my $selfPackages = $self->Package();
+        my $matchingPackage;
+
+        foreach my $testPackage (@$selfPackages)
+            {
+            if ($package eq $testPackage->Package())
+                {
+                $testPackage->MergeFile($file, $type, $prototype, $summary);;
+                return;
+                };
+            };
+
+        my $newDefinition = NaturalDocs::SymbolTable::IndexElement->New(undef, $package, $file, $type, $prototype,
+                                                                                                              $summary);
+        push @{$self->[PACKAGE]}, $newDefinition;
+
+        if ($newDefinition->Type() ne $self->CombinedType())
+            {  $self->[COMBINED_TYPE] = ::TOPIC_GENERAL();  };
+        if ($newDefinition->PackageSeparator() ne $self->PackageSeparator())
+            {  $self->[PACKAGE_SEPARATOR] = '.';  };
+        };
+    };
+
+
+#
+#   Function: Sort
+#
+#   Sorts the package and file lists of the symbol.
+#
+sub Sort
+    {
+    my $self = shift;
+
+    if ($self->HasMultipleFiles())
+        {
+        @{$self->[FILE]} = sort { ::StringCompare($a->File(), $b->File()) } @{$self->File()};
+        }
+
+    elsif ($self->HasMultiplePackages())
+        {
+        @{$self->[PACKAGE]} = sort { ::StringCompare( $a->Package(), $b->Package()) } @{$self->[PACKAGE]};
+
+        foreach my $packageElement ( @{$self->[PACKAGE]} )
+            {
+            if ($packageElement->HasMultipleFiles())
+                {  $packageElement->Sort();  };
+            };
+        };
+    };
+
+
+#
+#   Function: MakeSortableSymbol
+#
+#   Generates <SortableSymbol()> and <IgnoredPrefix()>.  Should only be called after everything is merged.
+#
+sub MakeSortableSymbol
+    {
+    my $self = shift;
+
+    my $finalLanguage;
+
+    if ($self->HasMultiplePackages() || $self->HasMultipleFiles())
+        {
+        # Collect all the files that define this symbol.
+
+        my @files;
+
+        if ($self->HasMultipleFiles())
+            {
+            my $fileElements = $self->File();
+
+            foreach my $fileElement (@$fileElements)
+                {  push @files, $fileElement->File();  };
+            }
+        else # HasMultiplePackages
+            {
+            my $packages = $self->Package();
+
+            foreach my $package (@$packages)
+                {
+                if ($package->HasMultipleFiles())
+                    {
+                    my $fileElements = $package->File();
+
+                    foreach my $fileElement (@$fileElements)
+                        {  push @files, $fileElement->File();  };
+                    }
+                else
+                    {  push @files, $package->File();  };
+                };
+            };
+
+
+        # Determine which language defines it the most.
+
+        # Keys are language objects, values are counts.
+        my %languages;
+        tie %languages, 'Tie::RefHash';
+
+        foreach my $file (@files)
+            {
+            my $language = NaturalDocs::Languages->LanguageOf($file);
+
+            if (exists $languages{$language})
+                {  $languages{$language}++;  }
+            else
+                {  $languages{$language} = 1;  };
+            };
+
+        my $topCount = 0;
+        my @topLanguages;
+
+        while (my ($language, $count) = each %languages)
+            {
+            if ($count > $topCount)
+                {
+                $topCount = $count;
+                @topLanguages = ( $language );
+                }
+            elsif ($count == $topCount)
+                {
+                push @topLanguages, $language;
+                };
+            };
+
+        if (scalar @topLanguages == 1)
+            {  $finalLanguage = $topLanguages[0];  }
+        else
+            {
+            if ($topLanguages[0]->Name() ne 'Text File')
+                {  $finalLanguage = $topLanguages[0];  }
+            else
+                {  $finalLanguage = $topLanguages[1];  };
+            };
+        }
+
+    else # !hasMultiplePackages && !hasMultipleFiles
+        {  $finalLanguage = NaturalDocs::Languages->LanguageOf($self->File());  };
+
+    my $textSymbol = NaturalDocs::SymbolString->ToText($self->Symbol(), $self->PackageSeparator());
+    my $ignoredPrefixLength = $finalLanguage->IgnoredPrefixLength($textSymbol, $self->CombinedType());
+
+    if ($ignoredPrefixLength)
+        {
+        $self->[IGNORED_PREFIX] = substr($textSymbol, 0, $ignoredPrefixLength);
+        $self->[SORTABLE_SYMBOL] = substr($textSymbol, $ignoredPrefixLength);
+        }
+    else
+        {  $self->[SORTABLE_SYMBOL] = $textSymbol;  };
+    };
+
+
+
+###############################################################################
+#
+#   Functions: Information Functions
+#
+#   Symbol - Returns the <SymbolString> without the package portion.
+#   Package - If <HasMultiplePackages()> is true, returns an arrayref of <NaturalDocs::SymbolTable::IndexElement> objects.
+#                  Otherwise returns the package <SymbolString>, or undef if global.
+#   File - If <HasMultipleFiles()> is true, returns an arrayref of <NaturalDocs::SymbolTable::IndexElement> objects.  Otherwise
+#           returns the name of the definition file.
+#   Type - Returns the <TopicType> of the package/symbol/file, if applicable.
+#   Prototype - Returns the prototype of the package/symbol/file, if applicable.
+#   Summary - Returns the summary of the package/symbol/file, if applicable.
+#   CombinedType - Returns the combined <TopicType> of the element.
+#   PackageSeparator - Returns the combined package separator symbol of the element.
+#   SortableSymbol - Returns the sortable symbol as a text string.  Only available after calling <MakeSortableSymbol()>.
+#   IgnoredPrefix - Returns the part of the symbol that was stripped off to make the <SortableSymbol()>, or undef if none.
+#                          Only available after calling <MakeSortableSymbol()>.
+#
+
+#   Function: HasMultiplePackages
+#   Returns whether <Packages()> is broken out into more elements.
+sub HasMultiplePackages
+    {  return ref($_[0]->[PACKAGE]);  };
+
+#   Function: HasMultipleFiles
+#   Returns whether <File()> is broken out into more elements.
+sub HasMultipleFiles
+    {  return ref($_[0]->[FILE]);  };
+
+
+
+
+
+
+###############################################################################
+# Group: Support Functions
+
+#
+#   Function: MergeFile
+#
+#   Adds another definition of the same package/symbol.  Perhaps the file is different.
+#
+#   Parameters:
+#
+#       file  - The package/symbol's definition file.
+#       type  - The package/symbol's <TopicType>.
+#       prototype  - The package/symbol's protoype if applicable.
+#       summary  - The package/symbol's summary if applicable.
+#
+sub MergeFile #(file, type, prototype, summary)
+    {
+    my ($self, $file, $type, $prototype, $summary) = @_;
+
+    # If there's only one file...
+    if (!$self->HasMultipleFiles())
+        {
+        # If there's one file and it's the different from the new one...
+        if ($file ne $self->File())
+            {
+            my $selfDefinition = NaturalDocs::SymbolTable::IndexElement->New(undef, undef, $self->File(), $self->Type(),
+                                                                                                                 $self->Prototype(), $self->Summary(),
+                                                                                                                 $self->CombinedType(),
+                                                                                                                 $self->PackageSeparator());
+            my $newDefinition = NaturalDocs::SymbolTable::IndexElement->New(undef, undef, $file, $type, $prototype,
+                                                                                                                  $summary);
+
+            $self->[FILE] = [ $selfDefinition, $newDefinition ];
+            $self->[TYPE] = undef;
+            $self->[PROTOTYPE] = undef;
+            $self->[SUMMARY] = undef;
+
+            if ($newDefinition->Type() ne $self->CombinedType())
+                {  $self->[COMBINED_TYPE] = ::TOPIC_GENERAL();  };
+            if ($newDefinition->PackageSeparator() ne $self->PackageSeparator())
+                {  $self->[PACKAGE_SEPARATOR] = '.';  };
+            }
+
+        # If the file was the same, just ignore the duplicate in the index.
+        }
+
+    # If there's more than one file...
+    else
+        {
+        # See if the new file is one of them.
+        my $files = $self->File();
+
+        foreach my $testElement (@$files)
+            {
+            if ($testElement->File() eq $file)
+                {
+                # If the new file's already in the index, ignore the duplicate.
+                return;
+                };
+            };
+
+        my $newDefinition = NaturalDocs::SymbolTable::IndexElement->New(undef, undef, $file, $type, $prototype,
+                                                                                                              $summary);
+        push @{$self->[FILE]}, $newDefinition;
+
+        if ($newDefinition->Type() ne $self->CombinedType())
+            {  $self->[COMBINED_TYPE] = ::TOPIC_GENERAL();  };
+        if ($newDefinition->PackageSeparator() ne $self->PackageSeparator())
+            {  $self->[PACKAGE_SEPARATOR] = '.';  };
+        };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/SymbolTable/Reference.pm b/docs/tool/Modules/NaturalDocs/SymbolTable/Reference.pm
new file mode 100644
index 00000000..ed8c3059
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/SymbolTable/Reference.pm
@@ -0,0 +1,273 @@
+###############################################################################
+#
+#   Package: NaturalDocs::SymbolTable::Reference
+#
+###############################################################################
+#
+#   A class representing a symbol or a potential symbol.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::SymbolTable::Reference;
+
+
+###############################################################################
+# Group: Implementation
+
+#
+#   Constants: Members
+#
+#   The class is implemented as a blessed arrayref.  The following constants are its members.
+#
+#       DEFINITIONS                        - An existence hashref of the <FileNames> that define this reference.
+#       INTERPRETATIONS                - A hashref of the possible interpretations of this reference.  The keys are the <SymbolStrings>
+#                                                     and the values are the scores.
+#       CURRENT_INTERPRETATION  - The interpretation currently used as the reference target.  It will be the interpretation with
+#                                                     the highest score that is actually defined.  If none are defined, this item will be undef.
+#
+
+# DEPENDENCY: New() depends on the order of these constants.  If they change, New() has to be updated.
+use constant DEFINITIONS => 0;
+use constant INTERPRETATIONS => 1;
+use constant CURRENT_INTERPRETATION => 2;
+
+
+###############################################################################
+# Group: Modification Functions
+
+
+#
+#   Function: New
+#
+#   Creates and returns a new object.
+#
+sub New
+    {
+    my $package = shift;
+
+    # Let's make it safe, since normally you can pass values to New.  Having them just be ignored would be an obscure error.
+    if (scalar @_)
+        {  die "You can't pass values to NaturalDocs::SymbolTable::Reference->New()\n";  };
+
+    # DEPENDENCY: This code depends on the order of the member constants.
+    my $object = [ { }, { }, undef ];
+    bless $object, $package;
+
+    return $object;
+    };
+
+
+#
+#   Function: AddDefinition
+#
+#   Adds a reference definition.
+#
+#   Parameters:
+#
+#       file   - The <FileName> that defines the reference.
+#
+sub AddDefinition #(file)
+    {
+    my ($self, $file) = @_;
+
+    $self->[DEFINITIONS]{$file} = 1;
+    };
+
+
+#
+#   Function: DeleteDefinition
+#
+#   Removes a reference definition.
+#
+#   Parameters:
+#
+#       file - The <FileName> which has the definition to delete.
+#
+sub DeleteDefinition #(file)
+    {
+    my ($self, $file) = @_;
+
+    delete $self->[DEFINITIONS]{$file};
+    };
+
+
+#
+#   Function: AddInterpretation
+#
+#   Adds a symbol that this reference can be interpreted as.
+#
+#   Parameters:
+#
+#       symbol  - The <SymbolString>.
+#       score     - The score of this interpretation.
+#
+sub AddInterpretation #(symbol, score)
+    {
+    my ($self, $symbol, $score) = @_;
+
+    $self->[INTERPRETATIONS]{$symbol} = $score;
+    };
+
+
+#
+#   Function: DeleteInterpretation
+#
+#   Deletes a symbol that this reference can be interpreted as.
+#
+#   Parameters:
+#
+#       symbol - The <SymbolString> to delete.
+#
+sub DeleteInterpretation #(symbol)
+    {
+    my ($self, $symbol) = @_;
+
+    delete $self->[INTERPRETATIONS]{$symbol};
+    };
+
+
+#
+#   Function: DeleteAllInterpretationsButCurrent
+#
+#   Deletes all interpretations except for the current one.
+#
+sub DeleteAllInterpretationsButCurrent
+    {
+    my $self = shift;
+
+    if ($self->HasCurrentInterpretation())
+        {
+        my $score = $self->CurrentScore();
+
+        # Fastest way to clear a hash except for one item?  Make a new hash with just that item.
+        %{$self->[INTERPRETATIONS]} = ( $self->[CURRENT_INTERPRETATION] => $score );
+        };
+    };
+
+
+#
+#   Function: SetCurrentInterpretation
+#
+#   Changes the current interpretation.  The new one must already have been added via <AddInterpretation()>.
+#
+#   Parameters:
+#
+#       symbol - The <SymbolString>l to make the current interpretation.  Can be set to undef to clear it.
+#
+sub SetCurrentInterpretation #(symbol)
+    {
+    my ($self, $symbol) = @_;
+
+    $self->[CURRENT_INTERPRETATION] = $symbol;
+    };
+
+
+###############################################################################
+# Group: Information Functions
+
+
+#
+#   Function: Definitions
+#
+#   Returns an array of all the <FileNames> that define this reference.  If none do, returns an empty array.
+#
+sub Definitions
+    {
+    return keys %{$_[0]->[DEFINITIONS]};
+    };
+
+
+#
+#   Function: IsDefined
+#
+#   Returns whether the reference has any definitions or not.
+#
+sub IsDefined
+    {
+    return scalar keys %{$_[0]->[DEFINITIONS]};
+    };
+
+
+#
+#   Function: IsDefinedIn
+#
+#   Returns whether the reference is defined in the passed <FileName>.
+#
+sub IsDefinedIn #(file)
+    {
+    my ($self, $file) = @_;
+
+    return exists $self->[DEFINITIONS]{$file};
+    };
+
+
+#
+#   Function: Interpretations
+#
+#   Returns an array of all the <SymbolStrings> that this reference can be interpreted as.  If none, returns an empty array.
+#
+sub Interpretations
+    {
+    return keys %{$_[0]->[INTERPRETATIONS]};
+    };
+
+
+#
+#   Function: InterpretationsAndScores
+#
+#   Returns a hash of all the <SymbolStrings> that this reference can be interpreted as and their scores.  The keys are the <SymbolStrings>
+#   and the values are the scores.  If none, returns an empty hash.
+#
+sub InterpretationsAndScores
+    {
+    return %{$_[0]->[INTERPRETATIONS]};
+    };
+
+
+#
+#   Function: HasCurrentInterpretation
+#
+#   Returns whether the reference has a current interpretation or not.
+#
+sub HasCurrentInterpretation
+    {
+    return defined $_[0]->[CURRENT_INTERPRETATION];
+    };
+
+
+#
+#   Function: CurrentInterpretation
+#
+#   Returns the <SymbolString> of the current interpretation, or undef if none.
+#
+sub CurrentInterpretation
+    {
+    return $_[0]->[CURRENT_INTERPRETATION];
+    };
+
+
+#
+#   Function: CurrentScore
+#
+#   Returns the score of the current interpretation, or undef if none.
+#
+sub CurrentScore
+    {
+    my $self = shift;
+
+    if (defined $self->[CURRENT_INTERPRETATION])
+        {
+        return $self->[INTERPRETATIONS]{ $self->[CURRENT_INTERPRETATION] };
+        }
+    else
+        {  return undef;  };
+    };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/SymbolTable/ReferenceTarget.pm b/docs/tool/Modules/NaturalDocs/SymbolTable/ReferenceTarget.pm
new file mode 100644
index 00000000..a1b2b0a5
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/SymbolTable/ReferenceTarget.pm
@@ -0,0 +1,97 @@
+###############################################################################
+#
+#   Class: NaturalDocs::SymbolTable::ReferenceTarget
+#
+###############################################################################
+#
+#   A class for storing information about a reference target.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::SymbolTable::ReferenceTarget;
+
+
+###############################################################################
+# Group: Implementation
+
+#
+#   Constants: Members
+#
+#   The class is implemented as a blessed arrayref.  The following constants are its members.
+#
+#       SYMBOL  - The target <SymbolString>.
+#       FILE        - The <FileName> the target is defined in.
+#       TYPE       - The target <TopicType>.
+#       PROTOTYPE - The target's prototype, or undef if none.
+#       SUMMARY    - The target's summary, or undef if none.
+#
+
+# DEPENDENCY: New() depends on the order of these constants.  If they change, New() has to be updated.
+use constant SYMBOL => 0;
+use constant FILE => 1;
+use constant TYPE => 2;
+use constant PROTOTYPE => 3;
+use constant SUMMARY => 4;
+
+###############################################################################
+# Group: Functions
+
+
+#
+#   Function: New
+#
+#   Creates and returns a new object.
+#
+#   Parameters:
+#
+#       symbol - The target <SymbolString>.
+#       file       - The <FileName> the target is defined in.
+#       type     - The <TopicType> of the target symbol.
+#       prototype - The target's prototype.  Set to undef if not defined or not applicable.
+#       summary - The target's summary.  Set to undef if not defined or not applicable.
+#
+sub New #(symbol, file, type, prototype, summary)
+    {
+    # DEPENDENCY: This code depends on the order of the member constants.
+
+    my $package = shift;
+
+    my $object = [ @_ ];
+    bless $object, $package;
+
+    return $object;
+    };
+
+
+# Function: Symbol
+# Returns the target's <SymbolString>.
+sub Symbol
+    {  return $_[0]->[SYMBOL];  };
+
+# Function: File
+# Returns the <FileName> the target is defined in.
+sub File
+    {  return $_[0]->[FILE];  };
+
+# Function: Type
+# Returns the target's <TopicType>.
+sub Type
+    {  return $_[0]->[TYPE];  };
+
+# Function: Prototype
+# Returns the target's prototype, or undef if not defined or not applicable.
+sub Prototype
+    {  return $_[0]->[PROTOTYPE];  };
+
+# Function: Summary
+# Returns the target's summary, or undef if not defined or not applicable.
+sub Summary
+    {  return $_[0]->[SUMMARY];  };
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/SymbolTable/Symbol.pm b/docs/tool/Modules/NaturalDocs/SymbolTable/Symbol.pm
new file mode 100644
index 00000000..f12858df
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/SymbolTable/Symbol.pm
@@ -0,0 +1,428 @@
+###############################################################################
+#
+#   Package: NaturalDocs::SymbolTable::Symbol
+#
+###############################################################################
+#
+#   A class representing a symbol or a potential symbol.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::SymbolTable::Symbol;
+
+
+###############################################################################
+# Group: Implementation
+
+#
+#   Constants: Members
+#
+#   The class is implemented as a blessed arrayref.  The following constants are its members.
+#
+#       DEFINITIONS             - A hashref of all the files which define this symbol.  The keys are the <FileNames>, and the values are
+#                                         <NaturalDocs::SymbolTable::SymbolDefinition> objects.  If no files define this symbol, this item will
+#                                          be undef.
+#       GLOBAL_DEFINITION  - The <FileName> which defines the global version of the symbol, which is what is used if
+#                                          a file references the symbol but does not have its own definition.  If there are no definitions, this
+#                                          item will be undef.
+#       REFERENCES              - A hashref of the references that can be interpreted as this symbol.  This doesn't mean these
+#                                          references necessarily are.  The keys are the reference strings, and the values are the scores of
+#                                          the interpretations.  If no references can be interpreted as this symbol, this item will be undef.
+#
+use constant DEFINITIONS => 0;
+use constant GLOBAL_DEFINITION => 1;
+use constant REFERENCES => 2;
+
+
+###############################################################################
+# Group: Modification Functions
+
+#
+#   Function: New
+#
+#   Creates and returns a new object.
+#
+sub New
+    {
+    my $package = shift;
+
+    # Let's make it safe, since normally you can pass values to New.  Having them just be ignored would be an obscure error.
+    if (scalar @_)
+        {  die "You can't pass values to NaturalDocs::SymbolTable::Symbol->New()\n";  };
+
+    my $object = [ undef, undef, undef ];
+    bless $object, $package;
+
+    return $object;
+    };
+
+#
+#   Function: AddDefinition
+#
+#   Adds a symbol definition.  If this is the first definition for this symbol, it will become the global definition.  If the definition
+#   already exists for the file, it will be ignored.
+#
+#   Parameters:
+#
+#       file   - The <FileName> that defines the symbol.
+#       type - The <TopicType> of the definition.
+#       prototype - The prototype of the definition, if applicable.  Undef otherwise.
+#       summary - The summary for the definition, if applicable.  Undef otherwise.
+#
+#   Returns:
+#
+#       Whether this provided the first definition for this symbol.
+#
+sub AddDefinition #(file, type, prototype, summary)
+    {
+    my ($self, $file, $type, $prototype, $summary) = @_;
+
+    my $isFirst;
+
+    if (!defined $self->[DEFINITIONS])
+        {
+        $self->[DEFINITIONS] = { };
+        $self->[GLOBAL_DEFINITION] = $file;
+        $isFirst = 1;
+        };
+
+    if (!exists $self->[DEFINITIONS]{$file})
+        {
+        $self->[DEFINITIONS]{$file} = NaturalDocs::SymbolTable::SymbolDefinition->New($type, $prototype, $summary);
+        };
+
+    return $isFirst;
+    };
+
+
+#
+#   Function: ChangeDefinition
+#
+#   Changes the information about an existing definition.
+#
+#   Parameters:
+#
+#       file   - The <FileName> that defines the symbol.  Must exist.
+#       type - The new <TopicType> of the definition.
+#       prototype - The new prototype of the definition, if applicable.  Undef otherwise.
+#       summary - The new summary of the definition, if applicable.  Undef otherwise.
+#
+sub ChangeDefinition #(file, type, prototype, summary)
+    {
+    my ($self, $file, $type, $prototype, $summary) = @_;
+
+    if (defined $self->[DEFINITIONS] &&
+        exists $self->[DEFINITIONS]{$file})
+        {
+        $self->[DEFINITIONS]{$file}->SetType($type);
+        $self->[DEFINITIONS]{$file}->SetPrototype($prototype);
+        $self->[DEFINITIONS]{$file}->SetSummary($summary);
+        };
+    };
+
+
+#
+#   Function: DeleteDefinition
+#
+#   Removes a symbol definition.  If the definition served as the global definition, a new one will be selected.
+#
+#   Parameters:
+#
+#       file - The <FileName> which contains definition to delete.
+#
+#   Returns:
+#
+#       Whether that was the only definition, and the symbol is now undefined.
+#
+sub DeleteDefinition #(file)
+    {
+    my ($self, $file) = @_;
+
+    # If there are no definitions...
+    if (!defined $self->[DEFINITIONS])
+        {  return undef;  };
+
+    delete $self->[DEFINITIONS]{$file};
+
+    # If there are no more definitions...
+    if (!scalar keys %{$self->[DEFINITIONS]})
+        {
+        $self->[DEFINITIONS] = undef;
+
+        # If definitions was previously defined, and now is empty, we can safely assume that the global definition was just deleted
+        # without checking it against $file.
+
+        $self->[GLOBAL_DEFINITION] = undef;
+
+        return 1;
+        }
+
+    # If there are more definitions and the global one was just deleted...
+    elsif ($self->[GLOBAL_DEFINITION] eq $file)
+        {
+        # Which one becomes global is pretty much random.
+        $self->[GLOBAL_DEFINITION] = (keys %{$self->[DEFINITIONS]})[0];
+        return undef;
+        };
+    };
+
+
+#
+#   Function: AddReference
+#
+#   Adds a reference that can be interpreted as this symbol.  It can be, but not necessarily is.
+#
+#   Parameters:
+#
+#       referenceString - The string of the reference.
+#       score                - The score of this interpretation.
+#
+sub AddReference #(referenceString, score)
+    {
+    my ($self, $referenceString, $score) = @_;
+
+    if (!defined $self->[REFERENCES])
+        {  $self->[REFERENCES] = { };  };
+
+    $self->[REFERENCES]{$referenceString} = $score;
+    };
+
+
+#
+#   Function: DeleteReference
+#
+#   Deletes a reference that can be interpreted as this symbol.
+#
+#   Parameters:
+#
+#       referenceString - The string of the reference to delete.
+#
+sub DeleteReference #(referenceString)
+    {
+    my ($self, $referenceString) = @_;
+
+    # If there are no definitions...
+    if (!defined $self->[REFERENCES])
+        {  return;  };
+
+    delete $self->[REFERENCES]{$referenceString};
+
+    # If there are no more definitions...
+    if (!scalar keys %{$self->[REFERENCES]})
+        {
+        $self->[REFERENCES] = undef;
+        };
+    };
+
+
+#
+#   Function: DeleteAllReferences
+#
+#   Removes all references that can be interpreted as this symbol.
+#
+sub DeleteAllReferences
+    {
+    $_[0]->[REFERENCES] = undef;
+    };
+
+
+###############################################################################
+# Group: Information Functions
+
+#
+#   Function: IsDefined
+#
+#   Returns whether the symbol is defined anywhere or not.  If it's not, that means it's just a potential interpretation of a
+#   reference.
+#
+sub IsDefined
+    {
+    return defined $_[0]->[GLOBAL_DEFINITION];
+    };
+
+#
+#   Function: IsDefinedIn
+#
+#   Returns whether the symbol is defined in the passed <FileName>.
+#
+sub IsDefinedIn #(file)
+    {
+    my ($self, $file) = @_;
+    return ($self->IsDefined() && exists $self->[DEFINITIONS]{$file});
+    };
+
+
+#
+#   Function: Definitions
+#
+#   Returns an array of all the <FileNames> that define this symbol.  If none do, will return an empty array.
+#
+sub Definitions
+    {
+    my $self = shift;
+
+    if ($self->IsDefined())
+        {  return keys %{$self->[DEFINITIONS]};  }
+    else
+        {  return ( );  };
+    };
+
+
+#
+#   Function: GlobalDefinition
+#
+#   Returns the <FileName> that contains the global definition of this symbol, or undef if the symbol isn't defined.
+#
+sub GlobalDefinition
+    {
+    return $_[0]->[GLOBAL_DEFINITION];
+    };
+
+
+#
+#   Function: TypeDefinedIn
+#
+#   Returns the <TopicType> of the symbol defined in the passed <FileName>, or undef if it's not defined in that file.
+#
+sub TypeDefinedIn #(file)
+    {
+    my ($self, $file) = @_;
+
+    if ($self->IsDefined())
+        {  return $self->[DEFINITIONS]{$file}->Type();  }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: GlobalType
+#
+#   Returns the <TopicType> of the global definition, or undef if the symbol isn't defined.
+#
+sub GlobalType
+    {
+    my $self = shift;
+
+    my $globalDefinition = $self->GlobalDefinition();
+
+    if (!defined $globalDefinition)
+        {  return undef;  }
+    else
+        {  return $self->[DEFINITIONS]{$globalDefinition}->Type();  };
+    };
+
+
+#
+#   Function: PrototypeDefinedIn
+#
+#   Returns the prototype of symbol defined in the passed <FileName>, or undef if it doesn't exist or is not defined in that file.
+#
+sub PrototypeDefinedIn #(file)
+    {
+    my ($self, $file) = @_;
+
+    if ($self->IsDefined())
+        {  return $self->[DEFINITIONS]{$file}->Prototype();  }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: GlobalPrototype
+#
+#   Returns the prototype of the global definition.  Will be undef if it doesn't exist or the symbol isn't defined.
+#
+sub GlobalPrototype
+    {
+    my $self = shift;
+
+    my $globalDefinition = $self->GlobalDefinition();
+
+    if (!defined $globalDefinition)
+        {  return undef;  }
+    else
+        {  return $self->[DEFINITIONS]{$globalDefinition}->Prototype();  };
+    };
+
+
+#
+#   Function: SummaryDefinedIn
+#
+#   Returns the summary of symbol defined in the passed <FileName>, or undef if it doesn't exist or is not defined in that file.
+#
+sub SummaryDefinedIn #(file)
+    {
+    my ($self, $file) = @_;
+
+    if ($self->IsDefined())
+        {  return $self->[DEFINITIONS]{$file}->Summary();  }
+    else
+        {  return undef;  };
+    };
+
+
+#
+#   Function: GlobalSummary
+#
+#   Returns the summary of the global definition.  Will be undef if it doesn't exist or the symbol isn't defined.
+#
+sub GlobalSummary
+    {
+    my $self = shift;
+
+    my $globalDefinition = $self->GlobalDefinition();
+
+    if (!defined $globalDefinition)
+        {  return undef;  }
+    else
+        {  return $self->[DEFINITIONS]{$globalDefinition}->Summary();  };
+    };
+
+
+#
+#   Function: HasReferences
+#
+#   Returns whether the symbol can be interpreted as any references.
+#
+sub HasReferences
+    {
+    return defined $_[0]->[REFERENCES];
+    };
+
+#
+#   Function: References
+#
+#   Returns an array of all the reference strings that can be interpreted as this symbol.  If none, will return an empty array.
+#
+sub References
+    {
+    if (defined $_[0]->[REFERENCES])
+        {  return keys %{$_[0]->[REFERENCES]};  }
+    else
+        {  return ( );  };
+    };
+
+
+#
+#   Function: ReferencesAndScores
+#
+#   Returns a hash of all the references that can be interpreted as this symbol and their scores.  The keys are the reference
+#   strings, and the values are the scores.  If none, will return an empty hash.
+#
+sub ReferencesAndScores
+    {
+    if (defined $_[0]->[REFERENCES])
+        {  return %{$_[0]->[REFERENCES]};  }
+    else
+        {  return ( );  };
+    };
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/SymbolTable/SymbolDefinition.pm b/docs/tool/Modules/NaturalDocs/SymbolTable/SymbolDefinition.pm
new file mode 100644
index 00000000..3992e1d1
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/SymbolTable/SymbolDefinition.pm
@@ -0,0 +1,96 @@
+###############################################################################
+#
+#   Package: NaturalDocs::SymbolTable::SymbolDefinition
+#
+###############################################################################
+#
+#   A class representing a symbol definition.  This does not store the definition symbol, class, or file.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::SymbolTable::SymbolDefinition;
+
+
+###############################################################################
+# Group: Implementation
+
+#
+#   Constants: Members
+#
+#   The class is implemented as a blessed arrayref.  The following constants are its members.
+#
+#       TYPE  - The symbol <TopicType>.
+#       PROTOTYPE  - The symbol's prototype, if applicable.  Will be undef otherwise.
+#       SUMMARY - The symbol's summary, if applicable.  Will be undef otherwise.
+#
+use constant TYPE => 0;
+use constant PROTOTYPE => 1;
+use constant SUMMARY => 2;
+# New depends on the order of the constants.
+
+
+###############################################################################
+# Group: Functions
+
+#
+#   Function: New
+#
+#   Creates and returns a new object.
+#
+#   Parameters:
+#
+#       type - The symbol <TopicType>.
+#       prototype  - The symbol prototype, if applicable.  Undef otherwise.
+#       summary - The symbol's summary, if applicable.  Undef otherwise.
+#
+sub New #(type, prototype, summary)
+    {
+    # This depends on the parameter list being the same as the constant order.
+
+    my $package = shift;
+
+    my $object = [ @_ ];
+    bless $object, $package;
+
+    return $object;
+    };
+
+
+#   Function: Type
+#   Returns the definition's <TopicType>.
+sub Type
+    {  return $_[0]->[TYPE];  };
+
+# Function: SetType
+# Changes the <TopicType>.
+sub SetType #(type)
+    {  $_[0]->[TYPE] = $_[1];  };
+
+#   Function: Prototype
+#   Returns the definition's prototype, or undef if it doesn't have one.
+sub Prototype
+    {  return $_[0]->[PROTOTYPE];  };
+
+# Function: SetPrototype
+# Changes the prototype.
+sub SetPrototype #(prototype)
+    {  $_[0]->[PROTOTYPE] = $_[1];  };
+
+#   Function: Summary
+#   Returns the definition's summary, or undef if it doesn't have one.
+sub Summary
+    {  return $_[0]->[SUMMARY];  };
+
+# Function: SetSummary
+# Changes the summary.
+sub SetSummary #(summary)
+    {  $_[0]->[SUMMARY] = $_[1];  };
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Topics.pm b/docs/tool/Modules/NaturalDocs/Topics.pm
new file mode 100644
index 00000000..ea42f57b
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Topics.pm
@@ -0,0 +1,1319 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Topics
+#
+###############################################################################
+#
+#   The topic constants and functions to convert them to and from strings used throughout the script.  All constants are exported
+#   by default.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use Text::Wrap ( );
+use Tie::RefHash ( );
+
+use strict;
+use integer;
+
+use NaturalDocs::Topics::Type;
+
+package NaturalDocs::Topics;
+
+use base 'Exporter';
+our @EXPORT = ( 'TOPIC_GENERAL', 'TOPIC_GENERIC', 'TOPIC_GROUP', 'TOPIC_CLASS', 'TOPIC_FILE', 'TOPIC_FUNCTION',
+                          'TOPIC_VARIABLE', 'TOPIC_PROPERTY', 'TOPIC_TYPE', 'TOPIC_ENUMERATION', 'TOPIC_CONSTANT',
+                          'TOPIC_INTERFACE', 'TOPIC_EVENT', 'TOPIC_DELEGATE', 'TOPIC_SECTION' );
+
+
+
+###############################################################################
+# Group: Types
+
+
+#
+#   Type: TopicType
+#
+#   A string representing a topic type as defined in <Topics.txt>.  It's format should be treated as opaque; use <MakeTopicType()>
+#   to get them from topic names.  However, they can be compared for equality with string functions.
+#
+
+
+#
+#   Constants: Default TopicTypes
+#
+#   Exported constants of the default <TopicTypes>, so you don't have to go through <TypeFromName()> every time.
+#
+#   TOPIC_GENERAL - The general <TopicType>, which has the special meaning of none in particular.
+#   TOPIC_GENERIC - Generic <TopicType>.
+#   TOPIC_GROUP - Group <TopicType>.
+#   TOPIC_CLASS - Class <TopicType>.
+#   TOPIC_INTERFACE - Interface <TopicType>.
+#   TOPIC_FILE - File <TopicType>.
+#   TOPIC_SECTION - Section <TopicType>.
+#   TOPIC_FUNCTION - Function <TopicType>.
+#   TOPIC_VARIABLE - Variable <TopicType>.
+#   TOPIC_PROPERTY - Property <TopicType>.
+#   TOPIC_TYPE - Type <TopicType>.
+#   TOPIC_CONSTANT - Constant <TopicType>.
+#   TOPIC_ENUMERATION - Enum <TopicType>.
+#   TOPIC_DELEGATE - Delegate <TopicType>.
+#   TOPIC_EVENT - Event <TopicType>.
+#
+use constant TOPIC_GENERAL => 'general';
+use constant TOPIC_GENERIC => 'generic';
+use constant TOPIC_GROUP => 'group';
+use constant TOPIC_CLASS => 'class';
+use constant TOPIC_INTERFACE => 'interface';
+use constant TOPIC_FILE => 'file';
+use constant TOPIC_SECTION => 'section';
+use constant TOPIC_FUNCTION => 'function';
+use constant TOPIC_VARIABLE => 'variable';
+use constant TOPIC_PROPERTY => 'property';
+use constant TOPIC_TYPE => 'type';
+use constant TOPIC_CONSTANT => 'constant';
+use constant TOPIC_ENUMERATION => 'enumeration';
+use constant TOPIC_DELEGATE => 'delegate';
+use constant TOPIC_EVENT => 'event';
+# Dependency: The values of these constants must match what is generated by MakeTopicType().
+# Dependency: These types must be added to requiredTypeNames so that they always exist.
+
+
+
+
+###############################################################################
+# Group: Variables
+
+
+#
+#   handle: FH_TOPICS
+#
+#   The file handle used when writing to <Topics.txt>.
+#
+
+
+#
+#   hash: types
+#
+#   A hashref that maps <TopicTypes> to <NaturalDocs::Topics::Type>s.
+#
+my %types;
+
+
+#
+#   hash: names
+#
+#   A hashref that maps various forms of the all-lowercase type names to <TopicTypes>.  All are in the same hash because
+#   two names that reduce to the same thing it would cause big problems, and we need to catch that.  Keys include
+#
+#   - Topic names
+#   - Plural topic names
+#   - Alphanumeric-only topic names
+#   - Alphanumeric-only plural topic names
+#
+my %names;
+
+
+#
+#   hash: keywords
+#
+#   A hashref that maps all-lowercase keywords to their <TopicTypes>.  Must not have any of the same keys as
+#   <pluralKeywords>.
+#
+my %keywords;
+
+
+#
+#   hash: pluralKeywords
+#
+#   A hashref that maps all-lowercase plural keywords to their <TopicTypes>.  Must not have any of the same keys as
+#   <keywords>.
+#
+my %pluralKeywords;
+
+
+#
+#   hash: indexable
+#
+#   An existence hash of all the indexable <TopicTypes>.
+#
+my %indexable;
+
+
+#
+#   array: requiredTypeNames
+#
+#   An array of the <TopicType> names which are required to be defined in the main file.  Are in the order they should appear
+#   when reformatting.
+#
+my @requiredTypeNames = ( 'Generic', 'Class', 'Interface', 'Section', 'File', 'Group', 'Function', 'Variable', 'Property', 'Type',
+                                           'Constant', 'Enumeration', 'Event', 'Delegate' );
+
+
+#
+#   array: legacyTypes
+#
+#   An array that converts the legacy topic types, which were numeric constants prior to 1.3, to the current <TopicTypes>.
+#   The legacy types are used as an index into the array.  Note that this does not support list type values.
+#
+my @legacyTypes = ( TOPIC_GENERAL, TOPIC_CLASS, TOPIC_SECTION, TOPIC_FILE, TOPIC_GROUP, TOPIC_FUNCTION,
+                                TOPIC_VARIABLE, TOPIC_GENERIC, TOPIC_TYPE, TOPIC_CONSTANT, TOPIC_PROPERTY );
+
+
+#
+#   array: mainTopicNames
+#
+#   An array of the <TopicType> names that are defined in the main <Topics.txt>.
+#
+my @mainTopicNames;
+
+
+
+###############################################################################
+# Group: Files
+
+
+#
+#   File: Topics.txt
+#
+#   The configuration file that defines or overrides the topic definitions for Natural Docs.  One version sits in Natural Docs'
+#   configuration directory, and another can be in a project directory to add to or override them.
+#
+#   > # [comments]
+#
+#   Everything after a # symbol is ignored.
+#
+#   Except when specifying topic names, everything below is case-insensitive.
+#
+#   > Format: [version]
+#
+#   Specifies the file format version of the file.
+#
+#
+#   Sections:
+#
+#       > Ignore[d] Keyword[s]: [keyword], [keyword] ...
+#       >    [keyword]
+#       >    [keyword], [keyword]
+#       >    ...
+#
+#       Ignores the keywords so that they're not recognized as Natural Docs topics anymore.  Can be specified as a list on the same
+#       line and/or following like a normal Keywords section.
+#
+#       > Topic Type: [name]
+#       > Alter Topic Type: [name]
+#
+#       Creates a new topic type or alters an existing one.  The name can only contain <CFChars> and isn't case sensitive, although
+#       the original case is remembered for presentation.
+#
+#       The name General is reserved.  There are a number of default types that must be defined in the main file as well, but those
+#       are governed by <NaturalDocs::Topics> and are not included here.  The default types can have their keywords or behaviors
+#       changed, though, either by editing the default file or by overriding them in the user file.
+#
+#       Enumeration is a special type.  It is indexed with Types and its definition list members are listed with Constants according
+#       to the rules in <Languages.txt>.
+#
+#
+#   Topic Type Sections:
+#
+#       > Plural: [name]
+#
+#       Specifies the plural name of the topic type.  Defaults to the singular name.  Has the same restrictions as the topic type
+#       name.
+#
+#       > Index: [yes|no]
+#
+#       Whether the topic type gets an index.  Defaults to yes.
+#
+#       > Scope: [normal|start|end|always global]
+#
+#       How the topic affects scope.  Defaults to normal.
+#
+#       normal - The topic stays within the current scope.
+#       start - The topic starts a new scope for all the topics beneath it, like class topics.
+#       end - The topic resets the scope back to global for all the topics beneath it, like section topics.
+#       always global - The topic is defined as a global symbol, but does not change the scope for any other topics.
+#
+#       > Class Hierarchy: [yes|no]
+#
+#       Whether the topic is part of the class hierarchy.  Defaults to no.
+#
+#       > Page Title if First: [yes|no]
+#
+#       Whether the title of this topic becomes the page title if it is the first topic in a file.  Defaults to no.
+#
+#       > Break Lists: [yes|no]
+#
+#       Whether list topics should be broken into individual topics in the output.  Defaults to no.
+#
+#       > Can Group With: [topic type], [topic type], ...
+#
+#       The list of <TopicTypes> the topic can possibly be grouped with.
+#
+#       > [Add] Keyword[s]:
+#       >    [keyword]
+#       >    [keyword], [plural keyword]
+#       >    ...
+#
+#       A list of the topic type's keywords.  Each line after the heading is the keyword and optionally its plural form.  This continues
+#       until the next line in "keyword: value" format.  "Add" isn't required.
+#
+#       - Keywords can only have letters and numbers.  No punctuation or spaces are allowed.
+#       - Keywords are not case sensitive.
+#       - Subsequent keyword sections add to the list.  They don't replace it.
+#       - Keywords can be redefined by other keyword sections.
+#
+#
+#   Revisions:
+#
+#       1.3:
+#
+#           The initial version of this file.
+#
+
+
+###############################################################################
+# Group: File Functions
+
+
+#
+#   Function: Load
+#
+#   Loads both the master and the project version of <Topics.txt>.
+#
+sub Load
+    {
+    my $self = shift;
+
+    # Add the special General topic type.
+
+    $types{::TOPIC_GENERAL()} = NaturalDocs::Topics::Type->New('General', 'General', 1, ::SCOPE_NORMAL(), undef);
+    $names{'general'} = ::TOPIC_GENERAL();
+    $indexable{::TOPIC_GENERAL()} = 1;
+    # There are no keywords for the general topic.
+
+
+    $self->LoadFile(1);  # Main
+
+    # Dependency: All the default topic types must be checked for existence.
+
+    # Check to see if the required types are defined.
+    foreach my $name (@requiredTypeNames)
+        {
+        if (!exists $names{lc($name)})
+            {  NaturalDocs::ConfigFile->AddError('The ' . $name . ' topic type must be defined in the main topics file.');  };
+        };
+
+    my $errorCount = NaturalDocs::ConfigFile->ErrorCount();
+
+    if ($errorCount)
+        {
+        NaturalDocs::ConfigFile->PrintErrorsAndAnnotateFile();
+        NaturalDocs::Error->SoftDeath('There ' . ($errorCount == 1 ? 'is an error' : 'are ' . $errorCount . ' errors')
+                                                    . ' in ' . NaturalDocs::Project->MainConfigFile('Topics.txt'));
+        }
+
+
+    $self->LoadFile();  # User
+
+    $errorCount = NaturalDocs::ConfigFile->ErrorCount();
+
+    if ($errorCount)
+        {
+        NaturalDocs::ConfigFile->PrintErrorsAndAnnotateFile();
+        NaturalDocs::Error->SoftDeath('There ' . ($errorCount == 1 ? 'is an error' : 'are ' . $errorCount . ' errors')
+                                                    . ' in ' . NaturalDocs::Project->UserConfigFile('Topics.txt'));
+        }
+    };
+
+
+#
+#   Function: LoadFile
+#
+#   Loads a particular version of <Topics.txt>.
+#
+#   Parameters:
+#
+#       isMain - Whether the file is the main file or not.
+#
+sub LoadFile #(isMain)
+    {
+    my ($self, $isMain) = @_;
+
+    my ($file, $status);
+
+    if ($isMain)
+        {
+        $file = NaturalDocs::Project->MainConfigFile('Topics.txt');
+        $status = NaturalDocs::Project->MainConfigFileStatus('Topics.txt');
+        }
+    else
+        {
+        $file = NaturalDocs::Project->UserConfigFile('Topics.txt');
+        $status = NaturalDocs::Project->UserConfigFileStatus('Topics.txt');
+        };
+
+    my $version;
+
+    if ($version = NaturalDocs::ConfigFile->Open($file))
+        {
+        # The format hasn't changed since the file was introduced.
+
+        if ($status == ::FILE_CHANGED())
+            {  NaturalDocs::Project->ReparseEverything();  };
+
+        my ($topicTypeKeyword, $topicTypeName, $topicType, $topicTypeObject, $inKeywords, $inIgnoredKeywords);
+
+        # Keys are topic type objects, values are unparsed strings.
+        my %canGroupWith;
+        tie %canGroupWith, 'Tie::RefHash';
+
+        while (my ($keyword, $value) = NaturalDocs::ConfigFile->GetLine())
+            {
+            if ($keyword)
+                {
+                $inKeywords = 0;
+                $inIgnoredKeywords = 0;
+                };
+
+            if ($keyword eq 'topic type')
+                {
+                $topicTypeKeyword = $keyword;
+                $topicTypeName = $value;
+
+                # Resolve conflicts and create the type if necessary.
+
+                $topicType = $self->MakeTopicType($topicTypeName);
+                my $lcTopicTypeName = lc($topicTypeName);
+
+                my $lcTopicTypeAName = $lcTopicTypeName;
+                $lcTopicTypeAName =~ tr/a-z0-9//cd;
+
+                if (!NaturalDocs::ConfigFile->HasOnlyCFChars($topicTypeName))
+                    {
+                    NaturalDocs::ConfigFile->AddError('Topic names can only have ' . NaturalDocs::ConfigFile->CFCharNames() . '.');
+                    }
+                elsif ($topicType eq ::TOPIC_GENERAL())
+                    {
+                    NaturalDocs::ConfigFile->AddError('You cannot define a General topic type.');
+                    }
+                elsif (defined $types{$topicType} || defined $names{$lcTopicTypeName} || defined $names{$lcTopicTypeAName})
+                    {
+                    NaturalDocs::ConfigFile->AddError('Topic type ' . $topicTypeName . ' is already defined or its name is too '
+                                                                     . 'similar to an existing name.  Use Alter Topic Type if you meant to override '
+                                                                     . 'its settings.');
+                    }
+                else
+                    {
+                    $topicTypeObject = NaturalDocs::Topics::Type->New($topicTypeName, $topicTypeName, 1, ::SCOPE_NORMAL(),
+                                                                                                  0, 0);
+
+                    $types{$topicType} = $topicTypeObject;
+                    $names{$lcTopicTypeName} = $topicType;
+                    $names{$lcTopicTypeAName} = $topicType;
+
+                    $indexable{$topicType} = 1;
+
+                    if ($isMain)
+                        {  push @mainTopicNames, $topicTypeName;  };
+                    };
+                }
+
+            elsif ($keyword eq 'alter topic type')
+                {
+                $topicTypeKeyword = $keyword;
+                $topicTypeName = $value;
+
+                # Resolve conflicts and create the type if necessary.
+
+                $topicType = $names{lc($topicTypeName)};
+
+                if (!defined $topicType)
+                    {  NaturalDocs::ConfigFile->AddError('Topic type ' . $topicTypeName . ' doesn\'t exist.');  }
+                elsif ($topicType eq ::TOPIC_GENERAL())
+                    {  NaturalDocs::ConfigFile->AddError('You cannot alter the General topic type.');  }
+                else
+                    {
+                    $topicTypeObject = $types{$topicType};
+                    };
+                }
+
+            elsif ($keyword =~ /^ignored? keywords?$/)
+                {
+                $inIgnoredKeywords = 1;
+
+                my @ignoredKeywords = split(/ ?, ?/, lc($value));
+
+                foreach my $ignoredKeyword (@ignoredKeywords)
+                    {
+                    delete $keywords{$ignoredKeyword};
+                    delete $pluralKeywords{$ignoredKeyword};
+                    };
+                }
+
+            # We continue even if there are errors in the topic type line so that we can find any other errors in the file as well.  We'd
+            # rather them all show up at once instead of them showing up one at a time between Natural Docs runs.  So we just ignore
+            # the settings if $topicTypeObject is undef.
+
+
+            elsif ($keyword eq 'plural')
+                {
+                my $pluralName = $value;
+                my $lcPluralName = lc($pluralName);
+
+                my $lcPluralAName = $lcPluralName;
+                $lcPluralAName =~ tr/a-zA-Z0-9//cd;
+
+                if (!NaturalDocs::ConfigFile->HasOnlyCFChars($pluralName))
+                    {
+                    NaturalDocs::ConfigFile->AddError('Plural names can only have '
+                                                                     . NaturalDocs::ConfigFile->CFCharNames() . '.');
+                    }
+                elsif ($lcPluralAName eq 'general')
+                    {
+                    NaturalDocs::ConfigFile->AddError('You cannot use General as a plural name for ' . $topicTypeName . '.');
+                    }
+                elsif ( (defined $names{$lcPluralName} && $names{$lcPluralName} ne $topicType) ||
+                         (defined $names{$lcPluralAName} && $names{$lcPluralAName} ne $topicType) )
+                    {
+                    NaturalDocs::ConfigFile->AddError($topicTypeName . "'s plural name, " . $pluralName
+                                                                     . ', is already defined or is too similar to an existing name.');
+                    }
+
+                elsif (defined $topicTypeObject)
+                    {
+                    $topicTypeObject->SetPluralName($pluralName);
+
+                    $names{$lcPluralName} = $topicType;
+                    $names{$lcPluralAName} = $topicType;
+                    };
+                }
+
+            elsif ($keyword eq 'index')
+                {
+                $value = lc($value);
+
+                if ($value eq 'yes')
+                    {
+                    if (defined $topicTypeObject)
+                        {
+                        $topicTypeObject->SetIndex(1);
+                        $indexable{$topicType} = 1;
+                        };
+                    }
+                elsif ($value eq 'no')
+                    {
+                    if (defined $topicTypeObject)
+                        {
+                        $topicTypeObject->SetIndex(0);
+                        delete $indexable{$topicType};
+                        };
+                    }
+                else
+                    {
+                    NaturalDocs::ConfigFile->AddError('Index lines can only be "yes" or "no".');
+                    };
+                }
+
+            elsif ($keyword eq 'class hierarchy')
+                {
+                $value = lc($value);
+
+                if ($value eq 'yes')
+                    {
+                    if (defined $topicTypeObject)
+                        {  $topicTypeObject->SetClassHierarchy(1);  };
+                    }
+                elsif ($value eq 'no')
+                    {
+                    if (defined $topicTypeObject)
+                        {  $topicTypeObject->SetClassHierarchy(0);  };
+                    }
+                else
+                    {
+                    NaturalDocs::ConfigFile->AddError('Class Hierarchy lines can only be "yes" or "no".');
+                    };
+                }
+
+            elsif ($keyword eq 'scope')
+                {
+                $value = lc($value);
+
+                if ($value eq 'normal')
+                    {
+                    if (defined $topicTypeObject)
+                        {  $topicTypeObject->SetScope(::SCOPE_NORMAL());  };
+                    }
+                elsif ($value eq 'start')
+                    {
+                    if (defined $topicTypeObject)
+                        {  $topicTypeObject->SetScope(::SCOPE_START());  };
+                    }
+                elsif ($value eq 'end')
+                    {
+                    if (defined $topicTypeObject)
+                        {  $topicTypeObject->SetScope(::SCOPE_END());  };
+                    }
+                elsif ($value eq 'always global')
+                    {
+                    if (defined $topicTypeObject)
+                        {  $topicTypeObject->SetScope(::SCOPE_ALWAYS_GLOBAL());  };
+                    }
+                else
+                    {
+                    NaturalDocs::ConfigFile->AddError('Scope lines can only be "normal", "start", "end", or "always global".');
+                    };
+                }
+
+            elsif ($keyword eq 'page title if first')
+                {
+                $value = lc($value);
+
+                if ($value eq 'yes')
+                    {
+                    if (defined $topicTypeObject)
+                        {  $topicTypeObject->SetPageTitleIfFirst(1);  };
+                    }
+                elsif ($value eq 'no')
+                    {
+                    if (defined $topicTypeObject)
+                        {  $topicTypeObject->SetPageTitleIfFirst(undef);  };
+                    }
+                else
+                    {
+                    NaturalDocs::ConfigFile->AddError('Page Title if First lines can only be "yes" or "no".');
+                    };
+                }
+
+            elsif ($keyword eq 'break lists')
+                {
+                $value = lc($value);
+
+                if ($value eq 'yes')
+                    {
+                    if (defined $topicTypeObject)
+                        {  $topicTypeObject->SetBreakLists(1);  };
+                    }
+                elsif ($value eq 'no')
+                    {
+                    if (defined $topicTypeObject)
+                        {  $topicTypeObject->SetBreakLists(undef);  };
+                    }
+                else
+                    {
+                    NaturalDocs::ConfigFile->AddError('Break Lists lines can only be "yes" or "no".');
+                    };
+                }
+
+            elsif ($keyword eq 'can group with')
+                {
+                if (defined $topicTypeObject)
+                    {  $canGroupWith{$topicTypeObject} = lc($value);  };
+                }
+
+            elsif ($keyword =~ /^(?:add )?keywords?$/)
+                {
+                $inKeywords = 1;
+                }
+
+            elsif (defined $keyword)
+                {  NaturalDocs::ConfigFile->AddError($keyword . ' is not a valid keyword.');  }
+
+            elsif (!$inKeywords && !$inIgnoredKeywords)
+                {
+                NaturalDocs::ConfigFile->AddError('All lines in ' . $topicTypeKeyword . ' sections must begin with a keyword.');
+                }
+
+            else # No keyword but in keyword section.
+                {
+                $value = lc($value);
+
+                if ($value =~ /^([a-z0-9 ]*[a-z0-9]) ?, ?([a-z0-9 ]+)$/)
+                    {
+                    my ($singular, $plural) = ($1, $2);
+
+                    if ($inIgnoredKeywords)
+                        {
+                        delete $keywords{$singular};
+                        delete $keywords{$plural};
+                        delete $pluralKeywords{$singular};
+                        delete $pluralKeywords{$plural};
+                        }
+                    elsif (defined $topicTypeObject)
+                        {
+                        $keywords{$singular} = $topicType;
+                        delete $pluralKeywords{$singular};
+
+                        $pluralKeywords{$plural} = $topicType;
+                        delete $keywords{$plural};
+                        };
+                    }
+                elsif ($value =~ /^[a-z0-9 ]+$/)
+                    {
+                    if ($inIgnoredKeywords)
+                        {
+                        delete $keywords{$value};
+                        delete $pluralKeywords{$value};
+                        }
+                    elsif (defined $topicType)
+                        {
+                        $keywords{$value} = $topicType;
+                        delete $pluralKeywords{$value};
+                        };
+                    }
+                else
+                    {
+                    NaturalDocs::ConfigFile->AddError('Keywords can only have letters, numbers, and spaces.  '
+                                                                     . 'Plurals must be separated by a comma.');
+                    };
+                };
+            };
+
+        NaturalDocs::ConfigFile->Close();
+
+
+        # Parse out the Can Group With lines now that everything's defined.
+
+        while (my ($typeObject, $value) = each %canGroupWith)
+            {
+            my @values = split(/ ?, ?/, $value);
+            my @types;
+
+            foreach my $value (@values)
+                {
+                # We're just going to ignore invalid items.
+                if (exists $names{$value})
+                    {  push @types, $names{$value};  };
+                };
+
+            if (scalar @types)
+                {  $typeObject->SetCanGroupWith(\@types);  };
+            };
+        }
+
+    else # couldn't open file
+        {
+        if ($isMain)
+            {  die "Couldn't open topics file " . $file . "\n";  }
+        else
+            {  NaturalDocs::Project->ReparseEverything();  };
+        };
+    };
+
+
+#
+#   Function: Save
+#
+#   Saves the main and user versions of <Topics.txt>.
+#
+sub Save
+    {
+    my $self = shift;
+
+    $self->SaveFile(1); # Main
+    $self->SaveFile(0); # User
+    };
+
+
+#
+#   Function: SaveFile
+#
+#   Saves a particular version of <Topics.txt>.
+#
+#   Parameters:
+#
+#       isMain - Whether the file is the main file or not.
+#
+sub SaveFile #(isMain)
+    {
+    my ($self, $isMain) = @_;
+
+    my $file;
+
+    if ($isMain)
+        {
+        if (NaturalDocs::Project->MainConfigFileStatus('Topics.txt') == ::FILE_SAME())
+            {  return;  };
+        $file = NaturalDocs::Project->MainConfigFile('Topics.txt');
+        }
+    else
+        {
+        # We have to check the main one two because this lists the topics defined in it.
+        if (NaturalDocs::Project->UserConfigFileStatus('Topics.txt') == ::FILE_SAME() &&
+            NaturalDocs::Project->MainConfigFileStatus('Topics.txt') == ::FILE_SAME())
+            {  return;  };
+        $file = NaturalDocs::Project->UserConfigFile('Topics.txt');
+        };
+
+
+    # Array of topic type names in the order they appear in the file.  If Alter Topic Type is used, the name will end with an asterisk.
+    my @topicTypeOrder;
+
+    # Keys are topic type names, values are property hashrefs.  Hashref keys are the property names, values the value.
+    # For keywords, the key is Keywords and the values are arrayrefs of singular and plural pairs.  If no plural is defined, the entry
+    # will be undef.
+    my %properties;
+
+    # List of ignored keywords specified as Ignore Keywords: [keyword], [keyword], ...
+    my @inlineIgnoredKeywords;
+
+    # List of ignored keywords specified in [keyword], [plural keyword] lines.  Done in pairs, like for regular keywords.
+    my @separateIgnoredKeywords;
+
+    my $inIgnoredKeywords;
+
+    if (NaturalDocs::ConfigFile->Open($file))
+        {
+        # We can assume the file is valid.
+
+        my ($keyword, $value, $topicTypeName);
+
+        while (($keyword, $value) = NaturalDocs::ConfigFile->GetLine())
+            {
+            $keyword = lc($keyword);
+
+            if ($keyword eq 'topic type' || $keyword eq 'alter topic type')
+                {
+                $topicTypeName = $types{ $names{lc($value)} }->Name();
+
+                if ($keyword eq 'alter topic type')
+                    {  $topicTypeName .= '*';  };
+
+                push @topicTypeOrder, $topicTypeName;
+
+                if (!exists $properties{$topicTypeName})
+                    {  $properties{$topicTypeName} = { 'keywords' => [ ] };  };
+                }
+
+            elsif ($keyword eq 'plural')
+                {
+                $properties{$topicTypeName}->{$keyword} = $value;
+                }
+
+            elsif ($keyword eq 'index' ||
+                    $keyword eq 'scope' ||
+                    $keyword eq 'page title if first' ||
+                    $keyword eq 'class hierarchy' ||
+                    $keyword eq 'break lists' ||
+                    $keyword eq 'can group with')
+                {
+                $properties{$topicTypeName}->{$keyword} = lc($value);
+                }
+
+            elsif ($keyword =~ /^(?:add )?keywords?$/)
+                {
+                $inIgnoredKeywords = 0;
+                }
+
+            elsif ($keyword =~ /^ignored? keywords?$/)
+                {
+                $inIgnoredKeywords = 1;
+                if ($value)
+                    {  push @inlineIgnoredKeywords, split(/ ?, ?/, $value);  };
+                }
+
+            elsif (!$keyword)
+                {
+                my ($singular, $plural) = split(/ ?, ?/, lc($value));
+
+                if ($inIgnoredKeywords)
+                    {  push @separateIgnoredKeywords, $singular, $plural;  }
+                else
+                    {  push @{$properties{$topicTypeName}->{'keywords'}}, $singular, $plural;  };
+                };
+            };
+
+        NaturalDocs::ConfigFile->Close();
+        };
+
+
+    if (!open(FH_TOPICS, '>' . $file))
+        {
+        # The main file may be on a shared volume or some other place the user doesn't have write access to.  Since this is only to
+        # reformat the file, we can ignore the failure.
+        if ($isMain)
+            {  return;  }
+        else
+            {  die "Couldn't save " . $file;  };
+        };
+
+    print FH_TOPICS 'Format: ' . NaturalDocs::Settings->TextAppVersion() . "\n\n";
+
+    # Remember the 80 character limit.
+
+    if ($isMain)
+        {
+        print FH_TOPICS
+        "# This is the main Natural Docs topics file.  If you change anything here, it\n"
+        . "# will apply to EVERY PROJECT you use Natural Docs on.  If you'd like to\n"
+        . "# change something for just one project, edit the Topics.txt in its project\n"
+        . "# directory instead.\n";
+        }
+    else
+        {
+        print FH_TOPICS
+        "# This is the Natural Docs topics file for this project.  If you change anything\n"
+        . "# here, it will apply to THIS PROJECT ONLY.  If you'd like to change something\n"
+        . "# for all your projects, edit the Topics.txt in Natural Docs' Config directory\n"
+        . "# instead.\n\n\n";
+
+        if (scalar @inlineIgnoredKeywords || scalar @separateIgnoredKeywords)
+            {
+            if (scalar @inlineIgnoredKeywords == 1 && !scalar @separateIgnoredKeywords)
+                {
+                print FH_TOPICS 'Ignore Keyword: ' . $inlineIgnoredKeywords[0] . "\n";
+                }
+            else
+                {
+                print FH_TOPICS
+                'Ignore Keywords: ' . join(', ', @inlineIgnoredKeywords) . "\n";
+
+                for (my $i = 0; $i < scalar @separateIgnoredKeywords; $i += 2)
+                    {
+                    print FH_TOPICS '   ' . $separateIgnoredKeywords[$i];
+
+                    if (defined $separateIgnoredKeywords[$i + 1])
+                        {  print FH_TOPICS ', ' . $separateIgnoredKeywords[$i + 1];  };
+
+                    print FH_TOPICS "\n";
+                    };
+                };
+            }
+        else
+            {
+            print FH_TOPICS
+            "# If you'd like to prevent keywords from being recognized by Natural Docs, you\n"
+            . "# can do it like this:\n"
+            . "# Ignore Keywords: [keyword], [keyword], ...\n"
+            . "#\n"
+            . "# Or you can use the list syntax like how they are defined:\n"
+            . "# Ignore Keywords:\n"
+            . "#    [keyword]\n"
+            . "#    [keyword], [plural keyword]\n"
+            . "#    ...\n";
+            };
+        };
+
+    print FH_TOPICS # [CFChars]
+    "\n\n"
+    . "#-------------------------------------------------------------------------------\n"
+    . "# SYNTAX:\n"
+    . "#\n";
+
+    if ($isMain)
+        {
+        print FH_TOPICS
+        "# Topic Type: [name]\n"
+        . "#    Creates a new topic type.  Each type gets its own index and behavior\n"
+        . "#    settings.  Its name can have letters, numbers, spaces, and these\n"
+        . "#    charaters: - / . '\n"
+        . "#\n"
+        . "#    The Enumeration type is special.  It's indexed with Types but its members\n"
+        . "#    are indexed with Constants according to the rules in Languages.txt.\n"
+        . "#\n"
+        }
+    else
+        {
+        print FH_TOPICS
+        "# Topic Type: [name]\n"
+        . "# Alter Topic Type: [name]\n"
+        . "#    Creates a new topic type or alters one from the main file.  Each type gets\n"
+        . "#    its own index and behavior settings.  Its name can have letters, numbers,\n"
+        . "#    spaces, and these charaters: - / . '\n"
+        . "#\n";
+        };
+
+    print FH_TOPICS
+    "# Plural: [name]\n"
+    . "#    Sets the plural name of the topic type, if different.\n"
+    . "#\n"
+    . "# Keywords:\n"
+    . "#    [keyword]\n"
+    . "#    [keyword], [plural keyword]\n"
+    . "#    ...\n";
+
+    if ($isMain)
+        {
+        print FH_TOPICS
+        "#    Defines a list of keywords for the topic type.  They may only contain\n"
+        . "#    letters, numbers, and spaces and are not case sensitive.  Plural keywords\n"
+        . "#    are used for list topics.\n";
+        }
+    else
+        {
+        print FH_TOPICS
+        "#    Defines or adds to the list of keywords for the topic type.  They may only\n"
+        . "#    contain letters, numbers, and spaces and are not case sensitive.  Plural\n"
+        . "#    keywords are used for list topics.  You can redefine keywords found in the\n"
+        . "#    main topics file.\n";
+        }
+
+    print FH_TOPICS
+    "#\n"
+    . "# Index: [yes|no]\n"
+    . "#    Whether the topics get their own index.  Defaults to yes.  Everything is\n"
+    . "#    included in the general index regardless of this setting.\n"
+    . "#\n"
+    . "# Scope: [normal|start|end|always global]\n"
+    . "#    How the topics affects scope.  Defaults to normal.\n"
+    . "#    normal        - Topics stay within the current scope.\n"
+    . "#    start         - Topics start a new scope for all the topics beneath it,\n"
+    . "#                    like class topics.\n"
+    . "#    end           - Topics reset the scope back to global for all the topics\n"
+    . "#                    beneath it.\n"
+    . "#    always global - Topics are defined as global, but do not change the scope\n"
+    . "#                    for any other topics.\n"
+    . "#\n"
+    . "# Class Hierarchy: [yes|no]\n"
+    . "#    Whether the topics are part of the class hierarchy.  Defaults to no.\n"
+    . "#\n"
+    . "# Page Title If First: [yes|no]\n"
+    . "#    Whether the topic's title becomes the page title if it's the first one in\n"
+    . "#    a file.  Defaults to no.\n"
+    . "#\n"
+    . "# Break Lists: [yes|no]\n"
+    . "#    Whether list topics should be broken into individual topics in the output.\n"
+    . "#    Defaults to no.\n"
+    . "#\n"
+    . "# Can Group With: [type], [type], ...\n"
+    . "#    Defines a list of topic types that this one can possibly be grouped with.\n"
+    . "#    Defaults to none.\n"
+    . "#-------------------------------------------------------------------------------\n\n";
+
+    my $listToPrint;
+
+    if ($isMain)
+        {
+        print FH_TOPICS
+        "# The following topics MUST be defined in this file:\n"
+        . "#\n";
+        $listToPrint = \@requiredTypeNames;
+        }
+    else
+        {
+        print FH_TOPICS
+        "# The following topics are defined in the main file, if you'd like to alter\n"
+        . "# their behavior or add keywords:\n"
+        . "#\n";
+        $listToPrint = \@mainTopicNames;
+        }
+
+    print FH_TOPICS
+    Text::Wrap::wrap('#    ', '#    ', join(', ', @$listToPrint)) . "\n"
+    . "\n"
+    . "# If you add something that you think would be useful to other developers\n"
+    . "# and should be included in Natural Docs by default, please e-mail it to\n"
+    . "# topics [at] naturaldocs [dot] org.\n";
+
+    # Existence hash.  We do this because we want the required ones to go first by adding them to @topicTypeOrder, but we don't
+    # want them to appear twice.
+    my %doneTopicTypes;
+    my ($altering, $numberOfProperties);
+
+    if ($isMain)
+        {  unshift @topicTypeOrder, @requiredTypeNames;  };
+
+    my @propertyOrder = ('Plural', 'Index', 'Scope', 'Class Hierarchy', 'Page Title If First', 'Break Lists');
+
+    foreach my $topicType (@topicTypeOrder)
+        {
+        if (!exists $doneTopicTypes{$topicType})
+            {
+            if (substr($topicType, -1) eq '*')
+                {
+                print FH_TOPICS "\n\n"
+                . 'Alter Topic Type: ' . substr($topicType, 0, -1) . "\n\n";
+
+                $altering = 1;
+                $numberOfProperties = 0;
+                }
+            else
+                {
+                print FH_TOPICS "\n\n"
+                . 'Topic Type: ' . $topicType . "\n\n";
+
+                $altering = 0;
+                $numberOfProperties = 0;
+                };
+
+            foreach my $property (@propertyOrder)
+                {
+                if (exists $properties{$topicType}->{lc($property)})
+                    {
+                    print FH_TOPICS
+                    '   ' . $property . ': ' . ucfirst( $properties{$topicType}->{lc($property)} ) . "\n";
+
+                    $numberOfProperties++;
+                    };
+                };
+
+            if (exists $properties{$topicType}->{'can group with'})
+                {
+                my @typeStrings = split(/ ?, ?/, lc($properties{$topicType}->{'can group with'}));
+                my @types;
+
+                foreach my $typeString (@typeStrings)
+                    {
+                    if (exists $names{$typeString})
+                        {  push @types, $names{$typeString};  };
+                    };
+
+                if (scalar @types)
+                    {
+                    for (my $i = 0; $i < scalar @types; $i++)
+                        {
+                        my $name = NaturalDocs::Topics->NameOfType($types[$i], 1);
+
+                        if ($i == 0)
+                            {  print FH_TOPICS '   Can Group With: ' . $name;  }
+                        else
+                            {  print FH_TOPICS ', ' . $name;  };
+                        };
+
+                    print FH_TOPICS "\n";
+                    $numberOfProperties++;
+                    };
+                };
+
+            if (scalar @{$properties{$topicType}->{'keywords'}})
+                {
+                if ($numberOfProperties > 1)
+                    {  print FH_TOPICS "\n";  };
+
+                print FH_TOPICS
+                '   ' . ($altering ? 'Add ' : '') . 'Keywords:' . "\n";
+
+                my $keywords = $properties{$topicType}->{'keywords'};
+
+                for (my $i = 0; $i < scalar @$keywords; $i += 2)
+                    {
+                    print FH_TOPICS '      ' . $keywords->[$i];
+
+                    if (defined $keywords->[$i + 1])
+                        {  print FH_TOPICS ', ' . $keywords->[$i + 1];  };
+
+                    print FH_TOPICS "\n";
+                    };
+                };
+
+            $doneTopicTypes{$topicType} = 1;
+            };
+        };
+
+    close(FH_TOPICS);
+    };
+
+
+
+###############################################################################
+# Group: Functions
+
+
+#
+#   Function: KeywordInfo
+#
+#   Returns information about a topic keyword.
+#
+#   Parameters:
+#
+#       keyword - The keyword, which may be plural.
+#
+#   Returns:
+#
+#       The array ( topicType, info, isPlural ), or an empty array if the keyword doesn't exist.
+#
+#       topicType - The <TopicType> of the keyword.
+#       info - The <NaturalDocs::Topics::Type> of its type.
+#       isPlural - Whether the keyword was plural or not.
+#
+sub KeywordInfo #(keyword)
+    {
+    my ($self, $keyword) = @_;
+
+    $keyword = lc($keyword);
+
+    my $type = $keywords{$keyword};
+
+    if (defined $type)
+        {  return ( $type, $types{$type}, undef );  };
+
+    $type = $pluralKeywords{$keyword};
+
+    if (defined $type)
+        {  return ( $type, $types{$type}, 1 );  };
+
+    return ( );
+    };
+
+
+#
+#   Function: NameInfo
+#
+#   Returns information about a topic name.
+#
+#   Parameters:
+#
+#      name - The topic type name, which can be plural and/or alphanumeric only.
+#
+#   Returns:
+#
+#       The array ( topicType, info ), or an empty array if the name doesn't exist.  Note that unlike <KeywordInfo()>, this
+#       does *not* tell you whether the name is plural or not.
+#
+#       topicType - The <TopicType> of the name.
+#       info - The <NaturalDocs::Topics::Type> of the type.
+#
+sub NameInfo #(name)
+    {
+    my ($self, $name) = @_;
+
+    my $type = $names{lc($name)};
+
+    if (defined $type)
+        {  return ( $type, $types{$type} );  }
+    else
+        {  return ( );  };
+    };
+
+
+#
+#   Function: TypeInfo
+#
+#   Returns information about a <TopicType>.
+#
+#   Parameters:
+#
+#      type - The <TopicType>.
+#
+#   Returns:
+#
+#       The <NaturalDocs::Topics::Type> of the type, or undef if it didn't exist.
+#
+sub TypeInfo #(type)
+    {
+    my ($self, $type) = @_;
+    return $types{$type};
+    };
+
+
+#
+#   Function: NameOfType
+#
+#   Returns the name of the passed <TopicType>, or undef if it doesn't exist.
+#
+#   Parameters:
+#
+#       topicType - The <TopicType>.
+#       plural - Whether to return the plural instead of the singular.
+#       alphanumericOnly - Whether to strips everything but alphanumeric characters out.  Case isn't modified.
+#
+#   Returns:
+#
+#       The topic type name, according to what was specified in the parameters, or undef if it doesn't exist.
+#
+sub NameOfType #(topicType, plural, alphanumericOnly)
+    {
+    my ($self, $topicType, $plural, $alphanumericOnly) = @_;
+
+    my $topicObject = $types{$topicType};
+
+    if (!defined $topicObject)
+        {  return undef;  };
+
+    my $topicName = ($plural ? $topicObject->PluralName() : $topicObject->Name());
+
+    if ($alphanumericOnly)
+        {  $topicName =~ tr/a-zA-Z0-9//cd;  };
+
+    return $topicName;
+    };
+
+
+#
+#   Function: TypeFromName
+#
+#   Returns a <TopicType> for the passed topic name.
+#
+#   Parameters:
+#
+#       topicName - The name of the topic, which can be plural and/or alphanumeric only.
+#
+#   Returns:
+#
+#       The <TopicType>.  It does not specify whether the name was plural or not.
+#
+sub TypeFromName #(topicName)
+    {
+    my ($self, $topicName) = @_;
+
+    return $names{lc($topicName)};
+    };
+
+
+#
+#   Function: IsValidType
+#
+#   Returns whether the passed <TopicType> is defined.
+#
+sub IsValidType #(type)
+    {
+    my ($self, $type) = @_;
+    return exists $types{$type};
+    };
+
+
+#
+#   Function: TypeFromLegacy
+#
+#   Returns a <TopicType> for the passed legacy topic type integer.  <TopicTypes> were changed from integer constants to
+#   strings in 1.3.
+#
+sub TypeFromLegacy #(legacyInt)
+    {
+    my ($self, $int) = @_;
+    return $legacyTypes[$int];
+    };
+
+
+#
+#   Function: AllIndexableTypes
+#
+#   Returns an array of all possible indexable <TopicTypes>.
+#
+sub AllIndexableTypes
+    {
+    my ($self) = @_;
+    return keys %indexable;
+    };
+
+
+
+###############################################################################
+# Group: Support Functions
+
+
+#
+#   Function: MakeTopicType
+#
+#   Returns a <TopicType> for the passed topic name.  It does not check to see if it exists already.
+#
+#   Parameters:
+#
+sub MakeTopicType #(topicName)
+    {
+    my ($self, $topicName) = @_;
+
+    # Dependency: The values of the default topic type constants must match what is generated here.
+
+    # Turn everything to lowercase and strip non-alphanumeric characters.
+    $topicName = lc($topicName);
+    $topicName =~ tr/a-z0-9//cd;
+
+    return $topicName;
+    };
+
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Topics/Type.pm b/docs/tool/Modules/NaturalDocs/Topics/Type.pm
new file mode 100644
index 00000000..b807fb1e
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Topics/Type.pm
@@ -0,0 +1,151 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Topics::Type
+#
+###############################################################################
+#
+#   A class storing information about a <TopicType>.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+
+package NaturalDocs::Topics::Type;
+
+use NaturalDocs::DefineMembers 'NAME',                         'Name()',
+                                                 'PLURAL_NAME',             'PluralName()',      'SetPluralName()',
+                                                 'INDEX',                        'Index()',              'SetIndex()',
+                                                 'SCOPE',                       'Scope()',              'SetScope()',
+                                                 'PAGE_TITLE_IF_FIRST', 'PageTitleIfFirst()', 'SetPageTitleIfFirst()',
+                                                 'BREAK_LISTS',             'BreakLists()',        'SetBreakLists()',
+                                                 'CLASS_HIERARCHY',    'ClassHierarchy()',  'SetClassHierarchy()',
+                                                 'CAN_GROUP_WITH';
+
+# Dependency: New() depends on the order of these and that there are no parent classes.
+
+use base 'Exporter';
+our @EXPORT = ('SCOPE_NORMAL', 'SCOPE_START', 'SCOPE_END', 'SCOPE_ALWAYS_GLOBAL');
+
+#
+#   Constants: Members
+#
+#   The object is implemented as a blessed arrayref, with the following constants as its indexes.
+#
+#   NAME - The topic's name.
+#   PLURAL_NAME - The topic's plural name.
+#   INDEX - Whether the topic is indexed.
+#   SCOPE - The topic's <ScopeType>.
+#   PAGE_TITLE_IF_FIRST - Whether the topic becomes the page title if it's first in a file.
+#   BREAK_LISTS - Whether list topics should be broken into individual topics in the output.
+#   CLASS_HIERARCHY - Whether the topic is part of the class hierarchy.
+#   CAN_GROUP_WITH - The existence hashref of <TopicTypes> the type can be grouped with.
+#
+
+
+
+###############################################################################
+# Group: Types
+
+
+#
+#   Constants: ScopeType
+#
+#   The possible values for <Scope()>.
+#
+#   SCOPE_NORMAL - The topic stays in the current scope without affecting it.
+#   SCOPE_START - The topic starts a scope.
+#   SCOPE_END - The topic ends a scope, returning it to global.
+#   SCOPE_ALWAYS_GLOBAL - The topic is always global, but it doesn't affect the current scope.
+#
+use constant SCOPE_NORMAL => 1;
+use constant SCOPE_START => 2;
+use constant SCOPE_END => 3;
+use constant SCOPE_ALWAYS_GLOBAL => 4;
+
+
+
+###############################################################################
+# Group: Functions
+
+
+#
+#   Function: New
+#
+#   Creates and returns a new object.
+#
+#   Parameters:
+#
+#       name - The topic name.
+#       pluralName - The topic's plural name.
+#       index - Whether the topic is indexed.
+#       scope - The topic's <ScopeType>.
+#       pageTitleIfFirst - Whether the topic becomes the page title if it's the first one in a file.
+#       breakLists - Whether list topics should be broken into individual topics in the output.
+#
+sub New #(name, pluralName, index, scope, pageTitleIfFirst, breakLists)
+    {
+    my ($self, @params) = @_;
+
+    # Dependency: Depends on the parameter order matching the member order and that there are no parent classes.
+
+    my $object = [ @params ];
+    bless $object, $self;
+
+    return $object;
+    };
+
+
+#
+#   Functions: Accessors
+#
+#   Name - Returns the topic name.
+#   PluralName - Returns the topic's plural name.
+#   SetPluralName - Replaces the topic's plural name.
+#   Index - Whether the topic is indexed.
+#   SetIndex - Sets whether the topic is indexed.
+#   Scope - Returns the topic's <ScopeType>.
+#   SetScope - Replaces the topic's <ScopeType>.
+#   PageTitleIfFirst - Returns whether the topic becomes the page title if it's first in the file.
+#   SetPageTitleIfFirst - Sets whether the topic becomes the page title if it's first in the file.
+#   BreakLists - Returns whether list topics should be broken into individual topics in the output.
+#   SetBreakLists - Sets whether list topics should be broken into individual topics in the output.
+#   ClassHierarchy - Returns whether the topic is part of the class hierarchy.
+#   SetClassHierarchy - Sets whether the topic is part of the class hierarchy.
+#
+
+
+#
+#   Function: CanGroupWith
+#
+#   Returns whether the type can be grouped with the passed <TopicType>.
+#
+sub CanGroupWith #(TopicType type) -> bool
+    {
+    my ($self, $type) = @_;
+    return ( defined $self->[CAN_GROUP_WITH] && exists $self->[CAN_GROUP_WITH]->{$type} );
+    };
+
+
+#
+#   Function: SetCanGroupWith
+#
+#   Sets the list of <TopicTypes> the type can be grouped with.
+#
+sub SetCanGroupWith #(TopicType[] types)
+    {
+    my ($self, $types) = @_;
+
+    $self->[CAN_GROUP_WITH] = { };
+
+    foreach my $type (@$types)
+        {  $self->[CAN_GROUP_WITH]->{$type} = 1;  };
+    };
+
+
+
+1;
diff --git a/docs/tool/Modules/NaturalDocs/Version.pm b/docs/tool/Modules/NaturalDocs/Version.pm
new file mode 100644
index 00000000..36d2d88c
--- /dev/null
+++ b/docs/tool/Modules/NaturalDocs/Version.pm
@@ -0,0 +1,384 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Version
+#
+###############################################################################
+#
+#   A package for handling version information.  What?  That's right.  Although it should be easy and obvious, version numbers
+#   need to be dealt with in a variety of formats, plus there's compatibility with older releases which handled it differently.  I
+#   wanted to centralize the code after it started getting complicated.  So there ya go.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Version;
+
+
+###############################################################################
+# Group: Functions
+
+
+#
+#   Function: ToString
+#
+#   Converts a <VersionInt> to a string.
+#
+sub ToString #(VersionInt version) => string
+    {
+    my ($self, $version) = @_;
+
+    my ($major, $minor, $month, $day, $year) = $self->ToValues($version);
+
+    if ($minor % 10 == 0)
+        {  $minor /= 10;  };
+
+    if ($day)
+        {  return sprintf('Development Release %02d-%02d-%d (%d.%d base)', $month, $day, $year, $major, $minor);  }
+    else
+        {  return $major . '.' . $minor;  };
+    };
+
+
+#
+#   Function: FromString
+#
+#   Converts a version string to a <VersionInt>.
+#
+sub FromString #(string string) => VersionInt
+    {
+    my ($self, $string) = @_;
+
+    if ($string eq '1')
+        {
+        return $self->FromValues(0, 91, 0, 0, 0);  # 0.91
+        }
+    else
+        {
+        my ($major, $minor, $month, $day, $year);
+
+        if ($string =~ /^(\d{1,2})\.(\d{1,2})$/)
+            {
+            ($major, $minor) = ($1, $2);
+            ($month, $day, $year) = (0, 0, 0);
+            }
+        elsif ($string =~ /^Development Release (\d{1,2})-(\d{1,2})-(\d\d\d\d) \((\d{1,2})\.(\d{1,2}) base\)$/)
+            {
+            ($month, $day, $year, $major, $minor) = ($1, $2, $3, $4, $5);
+
+            # We have to do sanity checking because these can come from user-editable text files.  The version numbers should
+            # already be constrained simply by being forced to have only two digits.
+
+            if ($month > 12 || $month < 1 || $day > 31 || $day < 1 || $year > 2255 || $year < 2000)
+                {  die 'The version string ' . $string . " doesn't have a valid date.\n";  };
+            }
+        else
+            {
+            die 'The version string ' . $string . " isn't in a recognized format.\n";
+            };
+
+        if (length $minor == 1)
+            {  $minor *= 10;  };
+
+        return $self->FromValues($major, $minor, $month, $day, $year);
+        };
+    };
+
+
+#
+#   Function: ToTextFile
+#
+#   Writes a <VersionInt> to a text file.
+#
+#   Parameters:
+#
+#       fileHandle - The handle of the file to write it to.  It should be at the correct location.
+#       version - The <VersionInt> to write.
+#
+sub ToTextFile #(handle fileHandle, VersionInt version)
+    {
+    my ($self, $fileHandle, $version) = @_;
+
+    print $fileHandle $self->ToString($version) . "\n";
+    };
+
+
+#
+#   Function: FromTextFile
+#
+#   Retrieves a <VersionInt> from a text file.
+#
+#   Parameters:
+#
+#       fileHandle - The handle of the file to read it from.  It should be at the correct location.
+#
+#   Returns:
+#
+#       The <VersionInt>.
+#
+sub FromTextFile #(handle fileHandle) => VersionInt
+    {
+    my ($self, $fileHandle) = @_;
+
+    my $version = <$fileHandle>;
+    ::XChomp(\$version);
+
+    return $self->FromString($version);
+    };
+
+
+#
+#   Function: ToBinaryFile
+#
+#   Writes a <VersionInt> to a binary file.
+#
+#   Parameters:
+#
+#       fileHandle - The handle of the file to write it to.  It should be at the correct location.
+#       version - The <VersionInt> to write.
+#
+sub ToBinaryFile #(handle fileHandle, VersionInt version)
+    {
+    my ($self, $fileHandle, $version) = @_;
+
+    my ($major, $minor, $month, $day, $year) = $self->ToValues($version);
+
+    # 1.35 development releases are encoded as 1.36.  Everything else is literal.
+    if ($day && $major == 1 && $minor == 35)
+        {  $minor = 36;  };
+
+    print $fileHandle pack('CC', $major, $minor);
+
+    # Date fields didn't exist with 1.35 stable and earlier.  1.35 development releases are encoded as 1.36, so this works.
+    if ($major > 1 || ($major == 1 && $minor > 35))
+        {
+        if ($day)
+            {  $year -= 2000;  };
+
+        print $fileHandle pack('CCC', $month, $day, $year);
+        };
+    };
+
+
+#
+#   Function: FromBinaryFile
+#
+#   Retrieves a <VersionInt> from a binary file.
+#
+#   Parameters:
+#
+#       fileHandle - The handle of the file to read it from.  It should be at the correct location.
+#
+#   Returns:
+#
+#       The <VersionInt>.
+#
+sub FromBinaryFile #(handle fileHandle) => VersionInt
+    {
+    my ($self, $fileHandle) = @_;
+
+    my ($major, $minor, $month, $day, $year);
+
+    my $raw;
+    read($fileHandle, $raw, 2);
+
+    ($major, $minor) = unpack('CC', $raw);
+
+    # 1.35 stable is the last release without the date fields.  1.35 development releases are encoded as 1.36, so this works.
+    if ($major > 1 || ($major == 1 && $minor > 35))
+        {
+        read($fileHandle, $raw, 3);
+        ($month, $day, $year) = unpack('CCC', $raw);
+
+        if ($day)
+            {  $year += 2000;  };
+        }
+    else
+        {  ($month, $day, $year) = (0, 0, 0);  };
+
+    # Fix the 1.35 development release special encoding.
+    if ($major == 1 && $minor == 36)
+        {  $minor = 35;  };
+
+
+    return $self->FromValues($major, $minor, $month, $day, $year);
+    };
+
+
+#
+#   Function: ToValues
+#
+#   Converts a <VersionInt> to the array ( major, minor, month, day, year ).  The minor version will be in two digit form, so x.2
+#   will return 20.  The date fields will be zero for stable releases.
+#
+sub ToValues #(VersionInt version) => ( int, int, int, int, int )
+    {
+    my ($self, $version) = @_;
+
+    my $major = ($version & 0x00003F80) >> 7;
+    my $minor = ($version & 0x0000007F);
+    my $month = ($version & 0x00780000) >> 19;
+    my $day = ($version & 0x0007C000) >> 14;
+    my $year = ($version & 0x7F800000) >> 23;
+
+    if ($year)
+        {  $year += 2000;  };
+
+    return ( $major, $minor, $month, $day, $year );
+    };
+
+
+#
+#   Function: FromValues
+#
+#   Returns a <VersionInt> created from the passed values.
+#
+#   Parameters:
+#
+#       major - The major version number.  For development releases, it should be the stable version it's based off of.
+#       minor - The minor version number.  It should always be two digits, so x.2 should pass 20.  For development
+#                  releases, it should be the stable version it's based off of.
+#       month - The numeric month of the development release.  For stable releases it should be zero.
+#       day - The day of the development release.  For stable releases it should be zero.
+#       year - The year of the development release.  For stable releases it should be zero.
+#
+#   Returns:
+#
+#       The <VersionInt>.
+#
+sub FromValues #(int major, int minor, int month, int day, int year) => VersionInt
+    {
+    my ($self, $major, $minor, $month, $day, $year) = @_;
+
+    if ($day)
+        {  $year -= 2000;  };
+
+    return ($major << 7) + ($minor) + ($month << 19) + ($day << 14) + ($year << 23);
+    };
+
+
+#
+#   Function: CheckFileFormat
+#
+#   Checks if a file's format is compatible with the current release.
+#
+#   - If the application is a development release or the file is from one, this only returns true if they are from the exact same
+#     development release.
+#   - If neither of them are development releases, this only returns true if the file is from a release between the minimum specified
+#     and the current version.  If there's no minimum it just checks that it's below the current version.
+#
+#   Parameters:
+#
+#       fileVersion - The <VersionInt> of the file format.
+#       minimumVersion - The minimum <VersionInt> required of the file format.  May be undef.
+#
+#   Returns:
+#
+#       Whether the file's format is compatible per the above rules.
+#
+sub CheckFileFormat #(VersionInt fileVersion, optional VersionInt minimumVersion) => bool
+    {
+    my ($self, $fileVersion, $minimumVersion) = @_;
+
+    my $appVersion = NaturalDocs::Settings->AppVersion();
+
+    if ($self->IsDevelopmentRelease($appVersion) || $self->IsDevelopmentRelease($fileVersion))
+        {  return ($appVersion == $fileVersion);  }
+    elsif ($minimumVersion && $fileVersion < $minimumVersion)
+        {  return 0;  }
+    else
+        {  return ($fileVersion <= $appVersion);  };
+    };
+
+
+#
+#   Function: IsDevelopmentRelease
+#
+#   Returns whether the passed <VersionInt> is for a development release.
+#
+sub IsDevelopmentRelease #(VersionInt version) => bool
+    {
+    my ($self, $version) = @_;
+
+    # Return if any of the date fields are set.
+    return ($version & 0x7FFFC000);
+    };
+
+
+
+###############################################################################
+# Group: Implementation
+
+#
+#   About: String Format
+#
+#   Full Releases:
+#
+#       Full releases are in the common major.minor format.  Either part can be up to two digits.  The minor version is interpreted
+#       as decimal places, so 1.3 > 1.22.  There are no leading or trailing zeroes.
+#
+#   Development Releases:
+#
+#       Development releases are in the format "Development Release mm-dd-yyyy (vv.vv base)" where vv.vv is the version
+#       number of the full release it's based off of.  The month and day will have leading zeroes where applicable.  Example:
+#       "Development Release 07-09-2006 (1.35 base)".
+#
+#   0.91 and Earlier:
+#
+#       Text files from releases prior to 0.95 had a separate file format version number that was used instead of the application
+#       version.  These were never changed between 0.85 and 0.91, so they are simply "1".  Text version numbers that are "1"
+#       instead of "1.0" will be interpreted as 0.91.
+#
+
+#
+#   About: Integer Format
+#
+#   <VersionInts> are 32-bit values with the bit distribution below.
+#
+#   > s yyyyyyyy mmmm ddddd vvvvvvv xxxxxxx
+#   > [syyy|yyyy] [ymmm|mddd] [ddvv|vvvv] [vxxx|xxxx]
+#
+#   s - The sign bit.  Always zero, so it's always interpreted as positive.
+#   y - The year bits if it's a development release, zero otherwise.  2000 is added to the value, so the range is from 2000 to 2255.
+#   m - The month bits if it's a development release, zero otherwise.
+#   d - The day bits if it's a development release, zero otherwise.
+#   v - The major version bits.  For development releases, it's the last stable version it was based off of.
+#   x - The minor version bits.  It's always stored as two decimals, so x.2 would store 20 here.  For development releases, it's the
+#        last stable version it was based off of.
+#
+#   It's stored with the development release date at a higher significance than the version because we want a stable release to
+#   always treat a development release as higher than itself, and thus not attempt to read any of the data files.  I'm not tracking
+#   data file formats at the development release level.
+#
+
+#
+#   About: Binary File Format
+#
+#   Current:
+#
+#       Five 8-bit unsigned values, appearing major, minor, month, day, year.  Minor is always stored with two digits, so x.2 would
+#       store 20.  Year is stored minus 2000, so 2006 is stored 6.  Stable releases store zero for all the date fields.
+#
+#   1.35 Development Releases:
+#
+#       1.35-based development releases are stored the same as current releases, but with 1.36 as the version number.  This is
+#       done so previous versions of Natural Docs that didn't include the date fields would still know it's a higher version.  There is
+#       no actual 1.36 release.
+#
+#   1.35 and Earlier:
+#
+#       Two 8-bit unsigned values, appearing major then minor.  Minor is always stored with two digits, so x.2 would store 20.
+#
+
+#
+#   About: Text File Format
+#
+#   In text files, versions are the <String Format> followed by a native line break.
+#
+
+
+1;