about summary refs log tree commit diff
path: root/docs/doctool/Modules/NaturalDocs
diff options
context:
space:
mode:
authorMagnus Auvinen <magnus.auvinen@gmail.com>2007-05-22 15:06:55 +0000
committerMagnus Auvinen <magnus.auvinen@gmail.com>2007-05-22 15:06:55 +0000
commit9ba8e6cf38da5196ed7bc878fe452952f3e10638 (patch)
treeea59837e22970274abac0ece2071e79189256901 /docs/doctool/Modules/NaturalDocs
parent90bcda3c10411ee4c1c65a494ec7c08dfdea01b4 (diff)
downloadzcatch-9ba8e6cf38da5196ed7bc878fe452952f3e10638.tar.gz
zcatch-9ba8e6cf38da5196ed7bc878fe452952f3e10638.zip
moved docs
Diffstat (limited to 'docs/doctool/Modules/NaturalDocs')
-rw-r--r--docs/doctool/Modules/NaturalDocs/Builder.pm237
-rw-r--r--docs/doctool/Modules/NaturalDocs/Builder/Base.pm316
-rw-r--r--docs/doctool/Modules/NaturalDocs/Builder/FramedHTML.pm294
-rw-r--r--docs/doctool/Modules/NaturalDocs/Builder/HTML.pm363
-rw-r--r--docs/doctool/Modules/NaturalDocs/Builder/HTMLBase.pm3075
-rw-r--r--docs/doctool/Modules/NaturalDocs/ClassHierarchy.pm861
-rw-r--r--docs/doctool/Modules/NaturalDocs/ClassHierarchy/Class.pm412
-rw-r--r--docs/doctool/Modules/NaturalDocs/ClassHierarchy/File.pm157
-rw-r--r--docs/doctool/Modules/NaturalDocs/ConfigFile.pm497
-rw-r--r--docs/doctool/Modules/NaturalDocs/Constants.pm229
-rw-r--r--docs/doctool/Modules/NaturalDocs/DefineMembers.pm100
-rw-r--r--docs/doctool/Modules/NaturalDocs/Error.pm305
-rw-r--r--docs/doctool/Modules/NaturalDocs/File.pm521
-rw-r--r--docs/doctool/Modules/NaturalDocs/Languages.pm1471
-rw-r--r--docs/doctool/Modules/NaturalDocs/Languages/ActionScript.pm885
-rw-r--r--docs/doctool/Modules/NaturalDocs/Languages/Ada.pm38
-rw-r--r--docs/doctool/Modules/NaturalDocs/Languages/Advanced.pm801
-rw-r--r--docs/doctool/Modules/NaturalDocs/Languages/Advanced/Scope.pm95
-rw-r--r--docs/doctool/Modules/NaturalDocs/Languages/Advanced/ScopeChange.pm70
-rw-r--r--docs/doctool/Modules/NaturalDocs/Languages/Base.pm743
-rw-r--r--docs/doctool/Modules/NaturalDocs/Languages/CSharp.pm1215
-rw-r--r--docs/doctool/Modules/NaturalDocs/Languages/PLSQL.pm313
-rw-r--r--docs/doctool/Modules/NaturalDocs/Languages/Pascal.pm143
-rw-r--r--docs/doctool/Modules/NaturalDocs/Languages/Perl.pm1338
-rw-r--r--docs/doctool/Modules/NaturalDocs/Languages/Prototype.pm92
-rw-r--r--docs/doctool/Modules/NaturalDocs/Languages/Prototype/Parameter.pm74
-rw-r--r--docs/doctool/Modules/NaturalDocs/Languages/Simple.pm495
-rw-r--r--docs/doctool/Modules/NaturalDocs/Languages/Tcl.pm219
-rw-r--r--docs/doctool/Modules/NaturalDocs/Menu.pm3168
-rw-r--r--docs/doctool/Modules/NaturalDocs/Menu/Entry.pm201
-rw-r--r--docs/doctool/Modules/NaturalDocs/NDMarkup.pm76
-rw-r--r--docs/doctool/Modules/NaturalDocs/Parser.pm1209
-rw-r--r--docs/doctool/Modules/NaturalDocs/Parser/Native.pm926
-rw-r--r--docs/doctool/Modules/NaturalDocs/Parser/ParsedTopic.pm210
-rw-r--r--docs/doctool/Modules/NaturalDocs/Project.pm966
-rw-r--r--docs/doctool/Modules/NaturalDocs/Project/File.pm113
-rw-r--r--docs/doctool/Modules/NaturalDocs/ReferenceString.pm301
-rw-r--r--docs/doctool/Modules/NaturalDocs/Settings.pm1258
-rw-r--r--docs/doctool/Modules/NaturalDocs/Settings/BuildTarget.pm91
-rw-r--r--docs/doctool/Modules/NaturalDocs/StatusMessage.pm102
-rw-r--r--docs/doctool/Modules/NaturalDocs/SymbolString.pm208
-rw-r--r--docs/doctool/Modules/NaturalDocs/SymbolTable.pm1810
-rw-r--r--docs/doctool/Modules/NaturalDocs/SymbolTable/File.pm186
-rw-r--r--docs/doctool/Modules/NaturalDocs/SymbolTable/IndexElement.pm522
-rw-r--r--docs/doctool/Modules/NaturalDocs/SymbolTable/Reference.pm273
-rw-r--r--docs/doctool/Modules/NaturalDocs/SymbolTable/ReferenceTarget.pm97
-rw-r--r--docs/doctool/Modules/NaturalDocs/SymbolTable/Symbol.pm428
-rw-r--r--docs/doctool/Modules/NaturalDocs/SymbolTable/SymbolDefinition.pm96
-rw-r--r--docs/doctool/Modules/NaturalDocs/Topics.pm1351
-rw-r--r--docs/doctool/Modules/NaturalDocs/Topics/Type.pm155
-rw-r--r--docs/doctool/Modules/NaturalDocs/Version.pm201
51 files changed, 29307 insertions, 0 deletions
diff --git a/docs/doctool/Modules/NaturalDocs/Builder.pm b/docs/doctool/Modules/NaturalDocs/Builder.pm
new file mode 100644
index 00000000..7e28fcc2
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Builder.pm
@@ -0,0 +1,237 @@
+###############################################################################
+#
+#   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-2005 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 $numberToPurge = scalar keys %{NaturalDocs::Project->FilesToPurge()};
+    my $numberToBuild = scalar keys %$filesToBuild;
+
+    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;
+    my $numberOfIndexesToPurge = scalar keys %indexesToPurge;
+
+
+    # Start the build process
+
+    foreach my $buildTarget (@$buildTargets)
+        {
+        $buildTarget->Builder()->BeginBuild($numberToPurge || $numberToBuild || $numberOfIndexesToBuild ||
+                                                             $numberOfIndexesToPurge || NaturalDocs::Menu->HasChanged());
+        };
+
+    if ($numberToPurge)
+        {
+        NaturalDocs::StatusMessage->Start('Purging ' . $numberToPurge . ' file' . ($numberToPurge > 1 ? 's' : '') . '...',
+                                                             scalar @$buildTargets);
+
+        foreach my $buildTarget (@$buildTargets)
+            {
+            $buildTarget->Builder()->PurgeFiles();
+            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 ($numberToBuild)
+        {
+        NaturalDocs::StatusMessage->Start('Building ' . $numberToBuild . ' file' . ($numberToBuild > 1 ? 's' : '') . '...',
+                                                             $numberToBuild * scalar @$buildTargets);
+
+        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 * scalar @$buildTargets);
+
+        foreach my $index (keys %indexesToBuild)
+            {
+            foreach my $buildTarget (@$buildTargets)
+                {
+                $buildTarget->Builder()->BuildIndex($index);
+                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($numberToPurge || $numberToBuild || $numberOfIndexesToBuild ||
+                                                           $numberOfIndexesToPurge || NaturalDocs::Menu->HasChanged());
+        };
+    };
+
+
+1;
diff --git a/docs/doctool/Modules/NaturalDocs/Builder/Base.pm b/docs/doctool/Modules/NaturalDocs/Builder/Base.pm
new file mode 100644
index 00000000..2d6cf468
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Builder/Base.pm
@@ -0,0 +1,316 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Builder::Base
+#
+###############################################################################
+#
+#   A base class for all Builder output formats.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2005 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.
+#   - <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.
+#   - <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: 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: 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/doctool/Modules/NaturalDocs/Builder/FramedHTML.pm b/docs/doctool/Modules/NaturalDocs/Builder/FramedHTML.pm
new file mode 100644
index 00000000..7b615e4b
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Builder/FramedHTML.pm
@@ -0,0 +1,294 @@
+###############################################################################
+#
+#   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-2005 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()
+
+            . $self->BuildContent($sourceFile, $parsedFile)
+
+            . $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 $startPage =
+
+        '<!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())
+                {  $startPage .= $self->StringToHTML(NaturalDocs::Menu->Title()) . ' - ';  };
+
+                $startPage .=
+                $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()
+
+            . $self->StandardComments()
+
+            . '<div class=IPageTitle>'
+                . $indexTitle
+            . '</div>';
+
+
+    my $endPage = $self->ClosingBrowserStyles() . '</body></html>';
+
+
+    my $pageCount = $self->BuildIndexPages($type, NaturalDocs::SymbolTable->Index($type), $startPage, $endPage);
+    $self->PurgeIndexFiles($type, $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>'
+
+        . '</head><body class=FramedMenuPage onLoad="NDOnLoad()">'
+            . $self->OpeningBrowserStyles()
+
+            . $self->StandardComments()
+
+            . $self->BuildMenu(undef, undef, 1)
+
+            . '<div class=Footer>'
+                . $self->BuildFooter()
+            . '</div>'
+
+            . $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/doctool/Modules/NaturalDocs/Builder/HTML.pm b/docs/doctool/Modules/NaturalDocs/Builder/HTML.pm
new file mode 100644
index 00000000..92b4bd7f
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Builder/HTML.pm
@@ -0,0 +1,363 @@
+###############################################################################
+#
+#   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-2005 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>'
+
+        . '</head><body class=UnframedPage onLoad="NDOnLoad()">'
+            . $self->OpeningBrowserStyles()
+
+        . $self->StandardComments()
+
+        # I originally had this part done in CSS, but there were too many problems.  Back to good old HTML tables.
+        . '<table border=0 cellspacing=0 cellpadding=0 width=100%><tr>'
+
+            . '<td class=MenuSection valign=top>'
+
+                . $self->BuildMenu($sourceFile, undef, undef)
+
+            . '</td>' . "\n\n"
+
+            . '<td class=ContentSection valign=top>'
+
+                . $self->BuildContent($sourceFile, $parsedFile)
+
+            . '</td>' . "\n\n"
+
+        . '</tr></table>'
+
+        . '<div class=Footer>'
+            . $self->BuildFooter()
+        . '</div>'
+
+        . $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 $startPage =
+
+        '<!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())
+                    {  $startPage .= ' - ' . $self->StringToHTML(NaturalDocs::Menu->Title());  };
+
+            $startPage .=
+            '</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=UnframedPage onLoad="NDOnLoad()">'
+            . $self->OpeningBrowserStyles()
+
+        . $self->StandardComments()
+
+        # I originally had this part done in CSS, but there were too many problems.  Back to good old HTML tables.
+        . '<table border=0 cellspacing=0 cellpadding=0 width=100%><tr>'
+
+            . '<td class=MenuSection valign=top>'
+
+                . $self->BuildMenu(undef, $type, undef)
+
+            . '</td>'
+
+            . '<td class=IndexSection valign=top>'
+                . '<div class=IPageTitle>'
+                    . $indexTitle
+                . '</div>';
+
+
+    my $endPage =
+            '</td>'
+
+        . '</tr></table>'
+
+        . '<div class=Footer>'
+            . $self->BuildFooter()
+        . '</div>'
+
+            . $self->ClosingBrowserStyles()
+        . '</body></html>';
+
+
+    my $pageCount = $self->BuildIndexPages($type, NaturalDocs::SymbolTable->Index($type), $startPage, $endPage);
+    $self->PurgeIndexFiles($type, $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>.
+#
+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/<!--START_ND_MENU-->.*?<!--END_ND_MENU-->/$self->BuildMenu($sourceFile, undef, undef)/es;
+
+        $content =~ s/<!--START_ND_FOOTER-->.*?<!--END_ND_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, undef);
+    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/<!--START_ND_MENU-->.*?<!--END_ND_MENU-->/$newMenu/es;
+
+        $content =~ s/<div class=Footer>.*<\/div>/"<div class=Footer>" . $newFooter . "<\/div>"/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/doctool/Modules/NaturalDocs/Builder/HTMLBase.pm b/docs/doctool/Modules/NaturalDocs/Builder/HTMLBase.pm
new file mode 100644
index 00000000..52653a90
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Builder/HTMLBase.pm
@@ -0,0 +1,3075 @@
+###############################################################################
+#
+#   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-2005 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';
+
+
+###############################################################################
+# 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' );
+
+#
+#   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
+#   <BuildIndexPages()>, 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: Implemented Interface Functions
+#
+#   The behavior of these functions is shared between HTML output formats.
+#
+
+
+#
+#   Function: PurgeFiles
+#
+#   Deletes the output files associated with the purged source files.
+#
+sub PurgeFiles
+    {
+    my $self = shift;
+
+    my $filesToPurge = NaturalDocs::Project->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);
+        };
+    };
+
+
+#
+#   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() )
+        {
+        if (!-d $directory)
+            {  NaturalDocs::File->CreatePath($directory);  };
+        };
+    };
+
+
+#
+#   Function: EndBuild
+#
+#   Synchronizes the projects CSS and JavaScript files.
+#
+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 file.
+
+    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);
+        };
+    };
+
+
+
+###############################################################################
+# 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.
+#       isFramed - Whether the menu will appear in a frame.  If so, it assumes the <base> HTML tag is set to make links go to the
+#                       appropriate frame.
+#
+#       Both sourceFile and indexType may be undef.
+#
+#   Returns:
+#
+#       The side menu in HTML.
+#
+sub BuildMenu #(FileName sourceFile, TopicType indexType, bool isFramed) -> string htmlMenu
+    {
+    my ($self, $sourceFile, $indexType, $isFramed) = @_;
+
+    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 = '<!--START_ND_MENU-->';
+
+
+    if (!exists $prebuiltMenus{$outputDirectory})
+        {
+        my $segmentOutput;
+
+        ($segmentOutput, $menuRootLength) =
+            $self->BuildMenuSegment($outputDirectory, $isFramed, NaturalDocs::Menu->Content());
+
+        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>';
+            };
+
+        $prebuiltMenus{$outputDirectory} = $titleOutput . $segmentOutput;
+        $output .= $titleOutput . $segmentOutput;
+        }
+    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"
+
+        # Using ToggleMenu here causes IE to sometimes say display is nothing instead of "block" or "none" on the first click.
+        # Whatever.  This is just as good.
+
+        . 'if (document.getElementById)'
+            . '{';
+
+            if (scalar @$toExpand)
+                {
+                $output .=
+
+                'for (var menu = 1; menu < ' . $menuGroupNumber . '; menu++)'
+                    . '{'
+                    . 'if (menu != ' . join(' && menu != ', @$toExpand) . ')'
+                        . '{'
+                        . 'document.getElementById("MGroupContent" + menu).style.display = "none";'
+                        . '};'
+                    . '};'
+                }
+            else
+                {
+                $output .=
+
+                'for (var menu = 1; menu < ' . $menuGroupNumber . '; menu++)'
+                    . '{'
+                    . 'document.getElementById("MGroupContent" + menu).style.display = "none";'
+                    . '};'
+                };
+
+            $output .=
+            '}'
+
+        . '// --></script>';
+        };
+
+    # Comment needed for UpdateFile().
+    $output .= '<!--END_ND_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.
+#       isFramed - Whether the menu will be in a HTML frame or not.  Assumes that if it is, the <base> HTML tag will be set so that
+#                       links are directed to the proper frame.
+#       menuSegment - An arrayref specifying the segment of the menu to build.  Either pass the menu itself or the contents
+#                               of a group.
+#
+#   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>.
+#
+sub BuildMenuSegment #(outputDirectory, isFramed, menuSegment)
+    {
+    my ($self, $outputDirectory, $isFramed, $menuSegment) = @_;
+
+    my ($output, $groupLength);
+
+    foreach my $entry (@$menuSegment)
+        {
+        if ($entry->Type() == ::MENU_GROUP())
+            {
+            my ($entryOutput, $entryLength) =
+                $self->BuildMenuSegment($outputDirectory, $isFramed, $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 . '\')"'
+                         . ($isFramed ? ' 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() . '"' . ($isFramed ? ' 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();
+
+    my $output;
+    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)
+            . ($i == 0 ? ' id=MainTopic' : '') . '>'
+
+            . '<div class=CTopic>'
+
+            . '<' . $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++;
+        };
+
+    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;
+
+    # In a nice efficiency twist, these buggers will hold the opening div tags if true, undef if false.  Not that this script is known
+    # for its efficiency.  Not that Perl is known for its efficiency.  Anyway...
+    my $isMarkedAttr;
+    my $entrySizeAttr = ' class=SEntrySize';
+    my $descriptionSizeAttr = ' class=SDescriptionSize';
+
+    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;
+                $isMarkedAttr = undef;
+                }
+            elsif ($topic->Type() eq ::TOPIC_GROUP())
+                {
+                if ($inGroup)
+                    {  $indent--;  };
+
+                $inGroup = 0;
+                $isMarkedAttr = undef;
+                };
+
+
+            $output .=
+             '<tr' . $isMarkedAttr . '><td' . $entrySizeAttr . '>'
+                . '<div class=S' . ($index == 0 ? 'Main' : NaturalDocs::Topics->NameOfType($topic->Type(), 0, 1)) . '>'
+                    . '<div class=SEntry>';
+
+
+            # Add any remaining modifiers to the HTML in the form of div tags.  This modifier approach isn't the most elegant
+            # thing, but there's not a lot of options.  It works.
+
+            if ($indent)
+                {  $output .= '<div class=SIndent' . $indent . '>';  };
+
+
+            # 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>';
+
+
+            # Close the modifiers.
+
+            if ($indent)
+                {  $output .= '</div>';  };
+
+            $output .=
+                    '</div>' # Entry
+                . '</div>' # Type
+
+            . '</td><td' . $descriptionSizeAttr . '>'
+
+                . '<div class=S' . ($index == 0 ? 'Main' : NaturalDocs::Topics->NameOfType($topic->Type(), 0, 1)) . '>'
+                    . '<div class=SDescription>';
+
+
+            # Add the modifiers to the HTML yet again.
+
+            if ($indent)
+                {  $output .= '<div class=SIndent' . $indent . '>';  };
+
+
+            if (defined $parsedFile->[$index]->Body())
+                {
+                $output .= $self->NDMarkupToHTML($sourceFile, $parsedFile->[$index]->Summary(),
+                                                                     $parsedFile->[$index]->Symbol(), $parsedFile->[$index]->Package(),
+                                                                     $parsedFile->[$index]->Type(), $parsedFile->[$index]->Using());
+                };
+
+
+            # Close the modifiers again.
+
+            if ($indent)
+                {  $output .= '</div>';  };
+
+
+            $output .=
+                    '</div>' # Description
+                . '</div>' # Type
+
+            . '</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;
+                    };
+                };
+
+            if (!defined $isMarkedAttr)
+                {  $isMarkedAttr = ' class=SMarked';  }
+            else
+                {  $isMarkedAttr = undef;  };
+
+            $entrySizeAttr = undef;
+            $descriptionSizeAttr = undef;
+
+            $index++;
+            };
+
+        $output .=
+        '</table>'
+    . '</div>' # Border
+    . '</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.
+#
+sub BuildFooter
+    {
+    my $self = shift;
+    my $footer = NaturalDocs::Menu->Footer();
+
+    if (defined $footer)
+        {
+        if (substr($footer, -1, 1) ne '.')
+            {  $footer .= '.';  };
+
+        $footer =~ s/\(c\)/&copy;/gi;
+        $footer =~ s/\(tm\)/&trade;/gi;
+        $footer =~ s/\(r\)/&reg;/gi;
+
+        $footer .= '&nbsp; Generated by <a href="' . NaturalDocs::Settings->AppURL() . '">Natural Docs</a>.'
+        }
+    else
+        {
+        $footer = 'Generated by <a href="' . NaturalDocs::Settings->AppURL() . '">Natural Docs</a>';
+        };
+
+    return '<!--START_ND_FOOTER-->' . $footer . '<!--END_ND_FOOTER-->';
+    };
+
+
+#
+#   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)
+                {
+                # Remove links, since people can't/shouldn't be clicking on tooltips anyway.
+                $summary =~ s/<\/?(?:link|url)>//g;
+
+                # The fact that we don't have scope or using shouldn't matter because we removed the links.
+                $summary = $self->NDMarkupToHTML($file, $summary, undef, undef, $type, undef);
+
+                # XXX - Hack.  We want to remove e-mail links as well, but keep their obfuscation.  So we leave the tags in there for
+                # the NDMarkupToHTML call, then strip out the link part afterwards.  The text obfuscation should still be in place.
+
+                $summary =~ s/<\/?a[^>]+>//g;
+
+                $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.
+#       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.
+#       beginPage - All the content of the HTML page up to where the index content should appear.
+#       endPage - All the content of the HTML page past where the index should appear.
+#
+#   Returns:
+#
+#       The number of pages in the index.
+#
+sub BuildIndexPages #(type, index, beginPage, endPage)
+    {
+    my ($self, $type, $indexSections, $beginPage, $endPage) = @_;
+
+    # Build the content.
+
+    my ($indexHTMLSections, $tooltipHTMLSections) = $self->BuildIndexSections($indexSections, $self->IndexFileOf($type, 1));
+
+
+    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 . $endPage;
+                close(INDEXFILEHANDLE);
+                $tooltips = undef;
+                };
+
+            $indexFileName = $self->IndexFileOf($type, $page);
+
+            open(INDEXFILEHANDLE, '>' . $indexFileName)
+                or die "Couldn't create output file " . $indexFileName . ".\n";
+
+            print INDEXFILEHANDLE $beginPage . $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 . $endPage;
+        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
+            $beginPage
+            . $self->BuildIndexNavigationBar($type, 1, \@pageLocations)
+            . 'There are no entries in the ' . lc( NaturalDocs::Topics->NameOfType($type) ) . ' index.'
+            . $endPage;
+
+        close(INDEXFILEHANDLE);
+        };
+
+
+    return $page;
+    };
+
+
+#
+#   Function: BuildIndexSections
+#
+#   Builds and returns index's 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.
+#       outputFile - The output file the index is going to be stored in.  Since there may be multiple files, just send the first file.  The
+#                        path is what matters, not the file name.
+#
+#   Returns:
+#
+#       The arrayref ( indexSections, tooltipSections ).
+#
+#       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 #(index, outputFile)
+    {
+    my ($self, $indexSections, $outputFile) = @_;
+
+    $self->ResetToolTips();
+
+    my $contentSections = [ ];
+    my $tooltipSections = [ ];
+
+    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';  };
+
+                $contentSections->[$section] .= $self->BuildIndexElement($indexSections->[$section]->[$i], $outputFile, $id);
+                };
+
+            $tooltipSections->[$section] .= $self->BuildToolTips();
+            $self->ResetToolTips(1);
+            };
+        };
+
+
+    return ( $contentSections, $tooltipSections );
+    };
+
+
+#
+#   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.
+#       outputFile - The output <FileName> this is appearing in.
+#       id - 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.
+#
+sub BuildIndexElement #(element, outputFile, id, symbol, package, hasPackage)
+    {
+    my ($self, $element, $outputFile, $id, $symbol, $package, $hasPackage) = @_;
+
+    my $output;
+
+
+    # If we're doing a file sub-index entry...
+
+    if ($hasPackage)
+        {
+        my ($inputDirectory, $relativePath) = NaturalDocs::Settings->SplitFromInputDirectory($element->File());
+
+        $output =
+        $self->BuildIndexLink($self->StringToHTML($relativePath, ADD_HIDDEN_BREAKS), $symbol,
+                                        $package, $element->File(), $element->Type(), $element->Prototype(),
+                                        $element->Summary(), $outputFile, '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())
+            {
+            $output .= $self->BuildIndexLink($text, $symbol, $element->Package(), $element->File(), $element->Type(),
+                                                             $element->Prototype(), $element->Summary(), $outputFile, 'IParent');
+            }
+
+        else
+            {
+            $output .=
+            '<span class=IParent>'
+                . $text
+            . '</span>'
+            . '<div class=ISubIndex>';
+
+            my $fileElements = $element->File();
+            foreach my $fileElement (@$fileElements)
+                {
+                $output .= $self->BuildIndexElement($fileElement, $outputFile, $id, $symbol, $element->Package(), 1);
+                };
+
+            $output .=
+            '</div>';
+            };
+        }
+
+
+    # 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());
+
+        $output .=
+        '<tr>'
+            . '<td class=ISymbolPrefix' . ($id ? ' id=' . $id : '') . '>'
+                . ($symbolPrefix || '&nbsp;')
+            . '</td><td class=IEntry>';
+
+        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())
+                {
+                $output .=
+                    $self->BuildIndexLink($symbolText, $element->Symbol(), $element->Package(), $element->File(),
+                                                     $element->Type(), $element->Prototype(), $element->Summary(), $outputFile, 'ISymbol');
+
+                if (defined $packageText)
+                    {
+                    $output .=
+                    ', <span class=IParent>'
+                        . $packageText
+                    . '</span>';
+                    };
+                }
+            else # hasMultipleFiles but not mulitplePackages
+                {
+                $output .=
+                '<span class=ISymbol>'
+                    . $symbolText
+                . '</span>';
+
+                if (defined $packageText)
+                    {
+                    $output .=
+                    ', <span class=IParent>'
+                        . $packageText
+                    . '</span>';
+                    };
+
+                $output .=
+                '<div class=ISubIndex>';
+
+                my $fileElements = $element->File();
+                foreach my $fileElement (@$fileElements)
+                    {
+                    $output .= $self->BuildIndexElement($fileElement, $outputFile, $id, $element->Symbol(), $element->Package(), 1);
+                    };
+
+                $output .=
+                '</div>';
+                };
+            }
+
+        else # hasMultiplePackages
+            {
+            $output .=
+            '<span class=ISymbol>'
+                . $symbolText
+            . '</span>'
+            . '<div class=ISubIndex>';
+
+            my $packageElements = $element->Package();
+            foreach my $packageElement (@$packageElements)
+                {
+                $output .= $self->BuildIndexElement($packageElement, $outputFile, $id, $element->Symbol());
+                };
+
+            $output .=
+            '</div>';
+            };
+
+        $output .= '</td></tr>';
+        };
+
+
+    return $output;
+    };
+
+
+#
+#   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.
+#       outputFile - The HTML <FileName> this link will appear in.
+#       style - The CSS style to apply to the link.
+#
+sub BuildIndexLink #(text, symbol, package, file, type, prototype, summary, outputFile, style)
+    {
+    my ($self, $text, $symbol, $package, $file, $type, $prototype, $summary, $outputFile, $style) = @_;
+
+    $symbol = NaturalDocs::SymbolString->Join($package, $symbol);
+
+    my $targetTooltipID = $self->BuildToolTip($symbol, $file, $type, $prototype, $summary);
+    my $toolTipProperties = $self->BuildToolTipLinkProperties($targetTooltipID);
+
+    return '<a href="' . $self->MakeRelativeURL( $outputFile, $self->OutputFileOf($file), 1 )
+                         . '#' . $self->SymbolToHTMLSymbol($symbol) . '" ' . $toolTipProperties . ' '
+                . 'class=' . $style . '>' . $text . '</a>';
+    };
+
+
+#
+#   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>.
+#       startingPage - If defined, only pages starting with this number will be removed.  Otherwise all pages will be removed.
+#
+sub PurgeIndexFiles #(type, startingPage)
+    {
+    my ($self, $type, $page) = @_;
+
+    if (!defined $page)
+        {  $page = 1;  };
+
+    for (;;)
+        {
+        my $file = $self->IndexFileOf($type, $page);
+
+        if (-e $file)
+            {
+            unlink($file);
+            $page++;
+            }
+        else
+            {
+            last;
+            };
+        };
+    };
+
+
+#
+#   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: 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: 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' );
+    };
+
+
+
+
+###############################################################################
+# 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: 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.
+#
+#   Returns:
+#
+#       The text in HTML.
+#
+sub NDMarkupToHTML #(sourceFile, text, symbol, package, type, using)
+    {
+    my ($self, $sourceFile, $text, $symbol, $package, $type, $using) = @_;
+
+    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 class=CCode>';
+            $inCode = 1;
+            }
+        elsif ($text eq '</code>')
+            {
+            $output .= '</pre></blockquote>';
+            $inCode = undef;
+            }
+        elsif ($inCode)
+            {
+            $text =~ s/\n/<br>/g;
+            $output .= $text;
+            }
+        else
+            {
+            # Format non-code text.
+
+            # Convert quotes to fancy quotes.
+            $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;
+
+            # 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;
+
+            # Resolve and convert links.
+            $text =~ s/<link>([^<]+)<\/link>/$self->BuildTextLink($1, $package, $using, $sourceFile)/ge;
+            $text =~ s/<url>([^<]+)<\/url>/$self->BuildURLLink($1)/ge;
+            $text =~ s/<email>([^<]+)<\/email>/$self->BuildEMailLink($1)/eg;
+
+            # Add double spaces too.
+            $text = $self->AddDoubleSpaces($text);
+
+            # Paragraphs
+            $text =~ s/<p>/<p class=CParagraph>/g;
+
+            # Bulleted lists
+            $text =~ s/<ul>/<ul class=CBulletList>/g;
+
+            # 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:
+#
+#       text  - The link text
+#       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.
+#
+#   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 #(text, package, using, sourceFile)
+    {
+    my ($self, $text, $package, $using, $sourceFile) = @_;
+
+    my $plainText = $self->RestoreAmpChars($text);
+
+    my $symbol = NaturalDocs::SymbolString->FromText($plainText);
+    my $target = NaturalDocs::SymbolTable->References(::REFERENCE_TEXT(), $symbol, $package, $using, $sourceFile);
+
+    if (defined $target)
+        {
+        my $targetFile;
+
+        if ($target->File() ne $sourceFile)
+            {  $targetFile = $self->MakeRelativeURL( $self->OutputFileOf($sourceFile), $self->OutputFileOf($target->File()), 1 );  };
+        # else leave it undef
+
+        my $targetTooltipID = $self->BuildToolTip($target->Symbol(), $sourceFile, $target->Type(),
+                                                                      $target->Prototype(), $target->Summary());
+
+        my $toolTipProperties = $self->BuildToolTipLinkProperties($targetTooltipID);
+
+        return '<a href="' . $targetFile . '#' . $self->SymbolToHTMLSymbol($target->Symbol()) . '" '
+                    . 'class=L' . NaturalDocs::Topics->NameOfType($target->Type(), 0, 1) . ' ' . $toolTipProperties . '>' . $text . '</a>';
+        }
+    else
+        {
+        return '&lt;' . $text . '&gt;';
+        };
+    };
+
+
+#
+#   Function: BuildURLLink
+#
+#   Creates a HTML link to an external URL.  Long URLs will have hidden breaks to allow them to wrap.
+#
+#   Parameters:
+#
+#       url - The URL to link to.
+#
+#   Returns:
+#
+#       The HTML link, complete with tags.
+#
+sub BuildURLLink #(url)
+    {
+    my ($self, $url) = @_;
+
+    $url = $self->RestoreAmpChars($url);
+
+    if (length $url < 50)
+        {  return '<a href="' . $url . '" class=LURL>' . $self->ConvertAmpChars($url) . '</a>';  };
+
+    my @segments = split(/([\,\&\/])/, $url);
+    my $output = '<a href="' . $url . '" class=LURL>';
+
+    # Get past the first batch of slashes, since we don't want to break on things like http://.
+
+    $output .= $self->ConvertAmpChars($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)
+        {
+        # Spaces don't wrap in IE for some reason.  Need to use dashes as well.
+        if ($segments[$i] eq ',' || $segments[$i] eq '/' || $segments[$i] eq '&')
+            {  $output .= '<span class=HB>- </span>';  };
+
+        $output .= $self->ConvertAmpChars($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:
+#
+#       address  - The e-mail address.
+#
+#   Returns:
+#
+#       The HTML e-mail link, complete with tags.
+#
+sub BuildEMailLink #(address)
+    {
+    my ($self, $address) = @_;
+    my @splitAddress;
+
+
+    # Hack the address up.  We want two user pieces and two host pieces.
+
+    my ($user, $host) = split(/\@/, $address);
+
+    my $userSplit = length($user) / 2;
+
+    push @splitAddress, substr($user, 0, $userSplit);
+    push @splitAddress, substr($user, $userSplit);
+
+    push @splitAddress, '@';
+
+    my $hostSplit = length($host) / 2;
+
+    push @splitAddress, substr($host, 0, $hostSplit);
+    push @splitAddress, 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.
+
+    return
+    "<a href=\"#\" onClick=\"location.href='mai' + 'lto:' + '" . join("' + '", @splitAddress) . "'; return false;\" class=LEMail>"
+        . $splitAddress[0] . '<span style="display: none">.nosp@m.</span>' . $splitAddress[1]
+        . '<span>@</span>'
+        . $splitAddress[3] . '<span style="display: none">.nosp@m.</span>' . $splitAddress[4]
+    . '</a>';
+    };
+
+
+#
+#   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 . '<span class=HB> <\/span>' . $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/doctool/Modules/NaturalDocs/ClassHierarchy.pm b/docs/doctool/Modules/NaturalDocs/ClassHierarchy.pm
new file mode 100644
index 00000000..11eb1e0c
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/ClassHierarchy.pm
@@ -0,0 +1,861 @@
+###############################################################################
+#
+#   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-2005 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 = 1;
+    my $fileName = NaturalDocs::Project->ClassHierarchyFile();
+
+    if (!open(CLASS_HIERARCHY_FILEHANDLE, '<' . $fileName))
+        {  $fileIsOkay = undef;  }
+    else
+        {
+        # 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);
+            $fileIsOkay = undef;
+            }
+        else
+            {
+            my $version = NaturalDocs::Version->FromBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE);
+
+            # Minor bugs were fixed in 1.33 that may affect the stored data.
+
+            if ($version > NaturalDocs::Settings->AppVersion() || $version < NaturalDocs::Version->FromString('1.33'))
+                {
+                close(CLASS_HIERARCHY_FILEHANDLE);
+                $fileIsOkay = undef;
+                };
+            };
+        };
+
+
+    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);
+
+                $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->ClassHierarchyFile())
+        or die "Couldn't save " . NaturalDocs::Project->ClassHierarchyFile() . ".\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) = 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>.
+#
+#   Note:
+#
+#       The file parameter must be defined when using this function externally.  It may be undef for internal use only.
+#
+sub AddClass #(file, class)
+    {
+    my ($self, $file, $class) = @_;
+
+    if (!exists $classes{$class})
+        {
+        $classes{$class} = NaturalDocs::ClassHierarchy::Class->New();
+        NaturalDocs::SymbolTable->AddReference($self->ClassReferenceOf($class), $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, $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) );
+            };
+
+        return $deletedParent;
+        };
+
+    return undef;
+    };
+
+
+#
+#   Function: ClassReferenceOf
+#
+#   Returns the <REFERENCE_CH_CLASS> <ReferenceString> of the passed class <SymbolString>.
+#
+sub ClassReferenceOf #(class)
+    {
+    my ($self, $class) = @_;
+
+    return NaturalDocs::ReferenceString->MakeFrom(::REFERENCE_CH_CLASS(), $class, 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/doctool/Modules/NaturalDocs/ClassHierarchy/Class.pm b/docs/doctool/Modules/NaturalDocs/ClassHierarchy/Class.pm
new file mode 100644
index 00000000..c3ed4aef
--- /dev/null
+++ b/docs/doctool/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-2005 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/doctool/Modules/NaturalDocs/ClassHierarchy/File.pm b/docs/doctool/Modules/NaturalDocs/ClassHierarchy/File.pm
new file mode 100644
index 00000000..19d17229
--- /dev/null
+++ b/docs/doctool/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-2005 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/doctool/Modules/NaturalDocs/ConfigFile.pm b/docs/doctool/Modules/NaturalDocs/ConfigFile.pm
new file mode 100644
index 00000000..9a20fc5e
--- /dev/null
+++ b/docs/doctool/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-2005 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/doctool/Modules/NaturalDocs/Constants.pm b/docs/doctool/Modules/NaturalDocs/Constants.pm
new file mode 100644
index 00000000..91c53556
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Constants.pm
@@ -0,0 +1,229 @@
+###############################################################################
+#
+#   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-2005 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 = ('REFERENCE_TEXT', 'REFERENCE_CH_CLASS', 'REFERENCE_CH_PARENT',
+
+                   'RESOLVE_RELATIVE', 'RESOLVE_ABSOLUTE', 'RESOLVE_NOPLURAL', 'RESOLVE_NOUSING',
+
+                   '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',
+
+                   'BINARY_FORMAT');
+
+#
+#   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: 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:
+#
+#       - <NaturalDocs::ReferenceString->ToBinaryFile()> and <NaturalDocs::ReferenceString->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: 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: 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:
+#
+#       - <NaturalDocs::ReferenceString->ToBinaryFile()> and <NaturalDocs::ReferenceString->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;
+
+
+#
+#   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: Other Constants
+
+
+#
+#   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: 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/doctool/Modules/NaturalDocs/DefineMembers.pm b/docs/doctool/Modules/NaturalDocs/DefineMembers.pm
new file mode 100644
index 00000000..63a7dbfe
--- /dev/null
+++ b/docs/doctool/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-2005 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/doctool/Modules/NaturalDocs/Error.pm b/docs/doctool/Modules/NaturalDocs/Error.pm
new file mode 100644
index 00000000..080de38f
--- /dev/null
+++ b/docs/doctool/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-2005 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/doctool/Modules/NaturalDocs/File.pm b/docs/doctool/Modules/NaturalDocs/File.pm
new file mode 100644
index 00000000..f69f3b18
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/File.pm
@@ -0,0 +1,521 @@
+###############################################################################
+#
+#   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-2005 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)
+                {
+                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>.  Morons.
+#
+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?:
+#
+#       Wow, where to begin?  First of all, there's nothing that gives a relative path between two relative paths.
+#
+#       Second of all, 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 of all, 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 fucking 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.  Morons.
+#
+#       Update: This last one appears to be fixed in File::Spec 0.83, but that version isn't even listed on CPAN.  Lovely.  Apparently
+#       it just comes with ActivePerl.  Somehow I don't think most Linux users are using that.
+#
+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: 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.
+#
+sub Copy #(source, destination)
+    {
+    my ($self, $source, $destination) = @_;
+    File::Copy::copy($source, $destination);
+    };
+
+
+1;
diff --git a/docs/doctool/Modules/NaturalDocs/Languages.pm b/docs/doctool/Modules/NaturalDocs/Languages.pm
new file mode 100644
index 00000000..4f29634c
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Languages.pm
@@ -0,0 +1,1471 @@
+###############################################################################
+#
+#   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-2005 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->MainLanguagesFile());
+        }
+
+
+    $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->UserLanguagesFile());
+        };
+
+
+    # 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->MainLanguagesFile();
+        $status = NaturalDocs::Project->MainLanguagesFileStatus();
+        }
+    else
+        {
+        $file = NaturalDocs::Project->UserLanguagesFile();
+        $status = NaturalDocs::Project->UserLanguagesFileStatus();
+        };
+
+
+    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 $value)
+                    {  $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->MainLanguagesFileStatus() == ::FILE_SAME())
+            {  return;  };
+        $file = NaturalDocs::Project->MainLanguagesFile();
+        }
+    else
+        {
+        # Have to check the main too because this file lists the languages defined there.
+        if (NaturalDocs::Project->UserLanguagesFileStatus() == ::FILE_SAME() &&
+            NaturalDocs::Project->MainLanguagesFileStatus() == ::FILE_SAME())
+            {  return;  };
+        $file = NaturalDocs::Project->UserLanguagesFile();
+        };
+
+
+    # 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;
+
+                open(SOURCEFILEHANDLE, '<' . $sourceFile) or die 'Could not open ' . $sourceFile;
+
+                read(SOURCEFILEHANDLE, $shebangLine, 2);
+                if ($shebangLine eq '#!')
+                    {  $shebangLine = <SOURCEFILEHANDLE>;  }
+                else
+                    {  $shebangLine = undef;  };
+
+                close (SOURCEFILEHANDLE);
+
+                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/doctool/Modules/NaturalDocs/Languages/ActionScript.pm b/docs/doctool/Modules/NaturalDocs/Languages/ActionScript.pm
new file mode 100644
index 00000000..33f3b73d
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Languages/ActionScript.pm
@@ -0,0 +1,885 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Languages::ActionScript
+#
+###############################################################################
+#
+#   A subclass to handle the language variations of Flash ActionScript.
+#
+#
+#   Topic: Language Support
+#
+#       Supported:
+#
+#       Not supported yet:
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2005 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Languages::ActionScript;
+
+use base 'NaturalDocs::Languages::Advanced';
+
+
+################################################################################
+# 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 );
+
+#
+#   hash: memberModifiers
+#   An existence hash of all the acceptable class member modifiers.  The keys are in all lowercase.
+#
+my %memberModifiers = ( 'public' => 1,
+                                        'private' => 1,
+                                        'static' => 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,
+                                        'static' => 1,
+                                        'class' => 1,
+                                        'interface' => 1,
+                                        'var' => 1,
+                                        'function' => 1,
+                                        'import' => 1 );
+
+
+
+################################################################################
+# 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) = @_;
+    return $self->ParsePascalParameterLine($line);
+    };
+
+
+#
+#   Function: TypeBeforeParameter
+#   Returns whether the type appears before the parameter in prototypes.
+#
+sub TypeBeforeParameter
+    {  return 0;  };
+
+
+#
+#   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->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
+#
+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;
+
+    while ($tokens->[$index] =~ /^[a-z]/i &&
+              exists $memberModifiers{lc($tokens->[$index])} )
+        {
+        push @modifiers, lc($tokens->[$index]);
+        $index++;
+
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+        };
+
+    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);
+
+        $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;  };
+
+    $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
+#
+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;
+
+    while ($tokens->[$index] =~ /^[a-z]/i &&
+              exists $memberModifiers{lc($tokens->[$index])} )
+        {
+        push @modifiers, lc($tokens->[$index]);
+        $index++;
+
+        $self->TryToSkipWhitespace(\$index, \$lineNumber);
+        };
+
+    if ($tokens->[$index] ne 'var')
+        {  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);
+
+            $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;  };
+
+        $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New(::TOPIC_VARIABLE(), $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))
+        {
+        }
+
+    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: 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();
+
+    do
+        {
+        $self->GenericSkip($indexRef, $lineNumberRef);
+        }
+    while ( $$indexRef < scalar @$tokens &&
+              !exists $declarationEnders{$tokens->[$$indexRef]} );
+    };
+
+
+#
+#   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) = @_;
+
+    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/doctool/Modules/NaturalDocs/Languages/Ada.pm b/docs/doctool/Modules/NaturalDocs/Languages/Ada.pm
new file mode 100644
index 00000000..b2467799
--- /dev/null
+++ b/docs/doctool/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-2005 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/doctool/Modules/NaturalDocs/Languages/Advanced.pm b/docs/doctool/Modules/NaturalDocs/Languages/Advanced.pm
new file mode 100644
index 00000000..98ea8884
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Languages/Advanced.pm
@@ -0,0 +1,801 @@
+###############################################################################
+#
+#   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-2005 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.
+#
+#   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 #(sourceFile, lineCommentSymbols, blockCommentSymbols)
+    {
+    my ($self, $sourceFile, $lineCommentSymbols, $blockCommentSymbols) = @_;
+
+    open(SOURCEFILEHANDLE, '<' . $sourceFile)
+        or die "Couldn't open input file " . $sourceFile . "\n";
+
+    my $tokens = [ ];
+    $self->SetTokens($tokens);
+
+    # For convenience.
+    $self->ClearAutoTopics();
+    $self->ClearScopeStack();
+
+    my @commentLines;
+
+    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);
+        $self->PreprocessLine(\$line);
+
+        my $originalLine = $line;
+        my $closingSymbol;
+
+
+        # Retrieve single line comments.  This leaves $line at the next line.
+
+        if ($self->StripOpeningSymbols(\$line, $lineCommentSymbols))
+            {
+            do
+                {
+                push @commentLines, $line;
+                push @$tokens, "\n";
+                $line = <SOURCEFILEHANDLE>;
+
+                if (!defined $line)
+                    {  goto EndDo;  };
+
+                ::XChomp(\$line);
+                $self->PreprocessLine(\$line);
+                }
+            while ($self->StripOpeningSymbols(\$line, $lineCommentSymbols));
+
+            EndDo:  # I hate Perl sometimes.
+            }
+
+
+        # Retrieve multiline comments.  This leaves $line at the next line.
+
+        elsif ($closingSymbol = $self->StripOpeningBlockSymbols(\$line, $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 ($symbol, $lineRemainder, $isMultiLine);
+
+            for (;;)
+                {
+                ($symbol, $lineRemainder) = $self->StripClosingSymbol(\$line, $closingSymbol);
+
+                push @commentLines, $line;
+
+                #  If we found an end comment symbol...
+                if (defined $symbol)
+                    {  last;  };
+
+                push @$tokens, "\n";
+                $line = <SOURCEFILEHANDLE>;
+                $isMultiLine = 1;
+
+                if (!defined $line)
+                    {  last;  };
+
+                ::XChomp(\$line);
+                $self->PreprocessLine(\$line);
+                };
+
+            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($originalLine);
+                    };
+
+                $lineNumber += scalar @commentLines;
+                @commentLines = ( );
+                }
+            else
+                {
+                push @$tokens, "\n";
+                };
+
+            $line = <SOURCEFILEHANDLE>;
+            }
+
+
+        # Otherwise just add it to the code.
+
+        else
+            {
+            $self->TokenizeLine($line);
+            $lineNumber++;
+            $line = <SOURCEFILEHANDLE>;
+            };
+
+
+        # If there were comments, send them to Parser->OnComment().
+
+        if (scalar @commentLines)
+            {
+            NaturalDocs::Parser->OnComment(\@commentLines, $lineNumber);
+            $lineNumber += scalar @commentLines;
+            @commentLines = ( );
+            };
+
+        };  # while (defined $line)
+
+
+    close(SOURCEFILEHANDLE);
+    }
+
+
+#
+#   Function: PreprocessLine
+#
+#   An overridable function if you'd like to preprocess a text line before it goes into <ParseForCommentsAndTokens()>.
+#
+#   Parameters:
+#
+#       lineRef - A reference to the line.  Already has the line break stripped off, but is otherwise untouched.
+#
+sub PreprocessLine #(lineRef)
+    {
+    };
+
+
+#
+#   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->[$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/doctool/Modules/NaturalDocs/Languages/Advanced/Scope.pm b/docs/doctool/Modules/NaturalDocs/Languages/Advanced/Scope.pm
new file mode 100644
index 00000000..49defeac
--- /dev/null
+++ b/docs/doctool/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-2005 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/doctool/Modules/NaturalDocs/Languages/Advanced/ScopeChange.pm b/docs/doctool/Modules/NaturalDocs/Languages/Advanced/ScopeChange.pm
new file mode 100644
index 00000000..89b45ff4
--- /dev/null
+++ b/docs/doctool/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-2005 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/doctool/Modules/NaturalDocs/Languages/Base.pm b/docs/doctool/Modules/NaturalDocs/Languages/Base.pm
new file mode 100644
index 00000000..e84ca2fd
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Languages/Base.pm
@@ -0,0 +1,743 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Languages::Base
+#
+###############################################################################
+#
+#   A base class for all programming language parsers.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2005 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) = @_;
+
+    if ($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 '(')
+                {  $parameter .= $token;  }
+            else
+                {  $beforeParameters .= $token;  };
+
+            if ($token eq $symbolStack[-1])
+                {  pop @symbolStack;  };
+            }
+
+        elsif ($token =~ /^[\(\[\{\<\'\"]$/)
+            {
+            if ($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;  };
+                }
+            else
+                {
+                $beforeParameters .= $token;
+                };
+
+            pop @symbolStack;
+            }
+
+        elsif ($token eq ',' || $token eq ';')
+            {
+            if ($symbolStack[0] eq '(')
+                {
+                if (scalar @symbolStack == 1)
+                    {
+                    push @parameterLines, $parameter . $token;
+                    $parameter = undef;
+                    }
+                else
+                    {
+                    $parameter .= $token;
+                    };
+                }
+            else
+                {
+                $beforeParameters .= $token;
+                };
+            }
+
+        else
+            {
+            if ($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: 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: 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/doctool/Modules/NaturalDocs/Languages/CSharp.pm b/docs/doctool/Modules/NaturalDocs/Languages/CSharp.pm
new file mode 100644
index 00000000..72f2b871
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Languages/CSharp.pm
@@ -0,0 +1,1215 @@
+###############################################################################
+#
+#   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
+#
+#       Not supported yet:
+#
+#       - Enums
+#       - Using
+#       - Using alias
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2005 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 );
+
+#
+#   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: 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->TryToGetClass(\$index, \$lineNumber) ||
+            $self->TryToGetFunction(\$index, \$lineNumber) ||
+            $self->TryToGetOverloadedOperator(\$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->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(), undef,
+                                                                                         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;
+
+    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);
+
+    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 (!defined $parentName)
+                {  return undef;  };
+
+            push @parents, NaturalDocs::SymbolString->FromText($parentName);
+
+            $self->TryToSkipWhitespace(\$index, \$lineNumber);
+            }
+        while ($tokens->[$index] eq ',')
+        };
+
+    if ($tokens->[$index] ne '{')
+        {  return undef;  };
+
+    $index++;
+
+
+    # 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 $autoTopic = NaturalDocs::Parser::ParsedTopic->New($topicType, $name,
+                                                                                         undef, undef,
+                                                                                         undef,
+                                                                                         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());
+
+    $$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];
+        $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(), undef,
+                                                                                                  $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(), undef,
+                                                                                                  $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(), undef,
+                                                                                                  $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(), undef,
+                                                                                              $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(), undef,
+                                                                                                  $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;  };
+
+    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/doctool/Modules/NaturalDocs/Languages/PLSQL.pm b/docs/doctool/Modules/NaturalDocs/Languages/PLSQL.pm
new file mode 100644
index 00000000..b713b323
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Languages/PLSQL.pm
@@ -0,0 +1,313 @@
+###############################################################################
+#
+#   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-2005 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.
+#
+#   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 ($ender =~ /^[a-z]+$/i && substr($$prototypeRef, -1) eq '@')
+        {  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/doctool/Modules/NaturalDocs/Languages/Pascal.pm b/docs/doctool/Modules/NaturalDocs/Languages/Pascal.pm
new file mode 100644
index 00000000..b6c4a018
--- /dev/null
+++ b/docs/doctool/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-2005 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/doctool/Modules/NaturalDocs/Languages/Perl.pm b/docs/doctool/Modules/NaturalDocs/Languages/Perl.pm
new file mode 100644
index 00000000..ca9d24fc
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Languages/Perl.pm
@@ -0,0 +1,1338 @@
+###############################################################################
+#
+#   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-2005 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Languages::Perl;
+
+use base 'NaturalDocs::Languages::Advanced';
+
+
+#
+#   bool: inNDPOD
+#   Set whenever we're in ND POD in <PreprocessLine()>.
+#
+my $inNDPOD;
+
+#
+#   bool: mustBreakPOD
+#   Set whenever the next line needs to be prefixed with "(NDPODBREAK)" in <PreprocessLine()>.
+#
+my $mustBreakPOD;
+
+#
+#   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) = @_;
+
+    $inNDPOD = 0;
+    $mustBreakPOD = 0;
+    @hereDocTerminators = ( );
+
+    $self->ParseForCommentsAndTokens($sourceFile, [ '#' ], [ '=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: PreprocessLine
+#
+#   Overridden to support "=begin nd" and similar.
+#
+#   - "=begin [nd|naturaldocs|natural docs]" all translate to "=begin nd".
+#   - "=[nd|naturaldocs|natural docs]" also translate to "=begin nd".
+#   - "=end [nd|naturaldocs|natural docs]" 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.
+#   - "=pod begin nd" and "=pod end nd" are supported for compatibility with ND 1.32 and earlier, even though the syntax is a
+#     mistake.
+#
+sub PreprocessLine #(lineRef)
+    {
+    my ($self, $lineRef) = @_;
+
+    if ($$lineRef =~ /^\=(?:(?:pod[ \t]+)?begin[ \t]+)?(?:nd|naturaldocs|natural[ \t]+docs)[ \t]*$/i)
+        {
+        $$lineRef = '=begin nd';
+        $inNDPOD = 1;
+        $mustBreakPOD = 0;
+        }
+    elsif ($$lineRef =~ /^\=(?:pod[ \t]+)end[ \t]+(?:nd|naturaldocs|natural[ \t]+docs)[ \t]*$/i)
+        {
+        $$lineRef = '=end nd';
+        $inNDPOD = 0;
+        $mustBreakPOD = 0;
+        }
+    elsif ($$lineRef =~ /^\=cut[ \t]*$/i)
+        {
+        if ($inNDPOD)
+            {
+            $$lineRef = '=end nd';
+            $inNDPOD = 0;
+            $mustBreakPOD = 1;
+            };
+        }
+    elsif ($mustBreakPOD)
+        {
+        $$lineRef = '(NDPODBREAK)' . $$lineRef;
+        $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.
+#       allowStringedClosingParens - If set, allows $) to end a parenthesis set.
+#
+sub GenericSkip #(indexRef, lineNumberRef, noRegExps, allowStringedClosingParens)
+    {
+    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))
+        {
+        $$indexRef++;
+
+        do
+            {  $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ')', $noRegExps, $allowStringedClosingParens);  }
+        while ($$indexRef < scalar @$tokens && $self->IsStringed($$indexRef - 1) && !$allowStringedClosingParens);
+        }
+    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.  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;
+        }
+    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] eq 'EOF')
+            {
+            push @hereDocTerminators, [ 'EOF' ];
+            $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.
+#
+sub TryToSkipRegexp #(indexRef, lineNumberRef)
+    {
+    my ($self, $indexRef, $lineNumberRef) = @_;
+    my $tokens = $self->Tokens();
+
+    my $isRegexp;
+
+    if ($tokens->[$$indexRef] =~ /^(?:m|qr|s|tr|y|)$/i &&
+         ($$indexRef == 0 || $tokens->[$$indexRef - 1] !~ /^[\$\%\@\*\-]$/) )
+        {  $isRegexp = 1;  }
+    elsif ( ($tokens->[$$indexRef] eq '/' || $tokens->[$$indexRef] eq '?') && !$self->IsStringed($$indexRef) )
+        {
+        my $index = $$indexRef - 1;
+
+        while ($index >= 0 && $tokens->[$index] =~ /^(?: |\t|\n)/)
+            {  $index--;  };
+
+        if ($index < 0 || $tokens->[$index] !~ /^[a-zA-Z0-9_\)\]\}\'\"\`]/)
+            {  $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;  };
+
+        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.
+#
+#   Parameters:
+#
+#       index - The index of the postition.
+#
+sub IsStringed #(index)
+    {
+    my ($self, $index) = @_;
+    my $tokens = $self->Tokens();
+
+    if ($index > 0 && $tokens->[$index - 1] eq '$')
+        {  return 1;  }
+    else
+        {  return undef;  };
+    };
+
+
+1;
diff --git a/docs/doctool/Modules/NaturalDocs/Languages/Prototype.pm b/docs/doctool/Modules/NaturalDocs/Languages/Prototype.pm
new file mode 100644
index 00000000..e529b89a
--- /dev/null
+++ b/docs/doctool/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-2005 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/doctool/Modules/NaturalDocs/Languages/Prototype/Parameter.pm b/docs/doctool/Modules/NaturalDocs/Languages/Prototype/Parameter.pm
new file mode 100644
index 00000000..f1f65b08
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Languages/Prototype/Parameter.pm
@@ -0,0 +1,74 @@
+###############################################################################
+#
+#   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-2005 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.
+
+
+#
+#   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/doctool/Modules/NaturalDocs/Languages/Simple.pm b/docs/doctool/Modules/NaturalDocs/Languages/Simple.pm
new file mode 100644
index 00000000..8e5762be
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Languages/Simple.pm
@@ -0,0 +1,495 @@
+###############################################################################
+#
+#   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-2005 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 = $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 = index($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/doctool/Modules/NaturalDocs/Languages/Tcl.pm b/docs/doctool/Modules/NaturalDocs/Languages/Tcl.pm
new file mode 100644
index 00000000..846937bd
--- /dev/null
+++ b/docs/doctool/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-2005 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/doctool/Modules/NaturalDocs/Menu.pm b/docs/doctool/Modules/NaturalDocs/Menu.pm
new file mode 100644
index 00000000..e2cb709c
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Menu.pm
@@ -0,0 +1,3168 @@
+###############################################################################
+#
+#   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-2005 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;
+
+#
+#   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.
+#
+#       > # [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]
+#
+#       The file format version, menu title, subtitle, and footer 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.
+#
+#       > 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.
+#
+#
+#   Revisions:
+#
+#       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->MenuFile());
+        };
+
+    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->MenuFileStatus() == ::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->MenuFileStatus() != ::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: Indexes
+#
+#   Returns an existence hashref of all the index <TopicTypes> appearing in the menu.  Do not change the arrayref.
+#
+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
+#   arrayref.
+#
+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>, <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->MenuFile(), 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);
+
+                    # 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(), $title, $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(), $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 = $value;  }
+                else
+                    {  NaturalDocs::ConfigFile->AddError('Title can only be defined once.');  };
+                }
+
+
+            elsif ($keyword eq 'subtitle')
+                {
+                if (defined $title)
+                    {
+                    if (!defined $subTitle)
+                        {  $subTitle = $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 = $value;  }
+                else
+                    {  NaturalDocs::ConfigFile->AddError('Footer can only be defined once.');  };
+                }
+
+
+            elsif ($keyword eq 'text')
+                {
+                $currentGroup->PushToGroup( NaturalDocs::Menu::Entry->New(::MENU_TEXT(), $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(), $title, $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($index);
+
+                    if (defined $indexType)
+                        {  $bannedIndexes{$indexType} = 1;  };
+                    };
+                }
+
+            elsif ($keyword eq 'index')
+                {
+                my $entry = NaturalDocs::Menu::Entry->New(::MENU_INDEX(), $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($index);
+
+                if (defined $indexType)
+                    {
+                    if ($indexInfo->Index())
+                        {
+                        $indexes{$indexType} = 1;
+                        $currentGroup->PushToGroup(
+                            NaturalDocs::Menu::Entry->New(::MENU_INDEX(), $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->MenuFile())
+        or die "Couldn't save menu file " . NaturalDocs::Project->MenuFile() . "\n";
+
+
+    print MENUFILEHANDLE
+    "Format: " . NaturalDocs::Settings->TextAppVersion() . "\n\n\n";
+
+    my $inputDirs = NaturalDocs::Settings->InputDirectories();
+
+
+    if (defined $title)
+        {
+        print MENUFILEHANDLE 'Title: ' . $title . "\n";
+
+        if (defined $subTitle)
+            {
+            print MENUFILEHANDLE 'SubTitle: ' . $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: ' . $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";
+        };
+
+    print MENUFILEHANDLE "\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 NaturalDocs::Topics->NameOfType($index, 1);
+            };
+
+        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: ' . $entry->Title()
+                                  . '  (' . ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE() ? 'no auto-title, ' : '') . $fileName . ")\n";
+            }
+        elsif ($entry->Type() == ::MENU_GROUP())
+            {
+            if (defined $lastEntryType && $lastEntryType != ::MENU_GROUP())
+                {  print $fileHandle "\n";  };
+
+            print $fileHandle $indentChars . 'Group: ' . $entry->Title() . "  {\n\n";
+            $self->WriteMenuEntries($entry->GroupContent(), $fileHandle, '   ' . $indentChars, $relativeFiles);
+            print $fileHandle '   ' . $indentChars . '}  # Group: ' . $entry->Title() . "\n\n";
+            }
+        elsif ($entry->Type() == ::MENU_TEXT())
+            {
+            print $fileHandle $indentChars . 'Text: ' . $entry->Title() . "\n";
+            }
+        elsif ($entry->Type() == ::MENU_LINK())
+            {
+            print $fileHandle $indentChars . 'Link: ' . $entry->Title() . '  (' . $entry->Target() . ')' . "\n";
+            }
+        elsif ($entry->Type() == ::MENU_INDEX())
+            {
+            my $type;
+            if ($entry->Target() ne ::TOPIC_GENERAL())
+                {
+                $type = NaturalDocs::Topics->NameOfType($entry->Target()) . ' ';
+                };
+
+            print $fileHandle $indentChars . $type . 'Index: ' . $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->PreviousMenuStateFile();
+
+    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 ($version <= NaturalDocs::Settings->AppVersion())
+                {  $fileIsOkay = 1;  }
+            else
+                {  close(PREVIOUSSTATEFILEHANDLE);  };
+            }
+
+        else # it's not in binary
+            {  close(PREVIOUSSTATEFILEHANDLE);  };
+        };
+
+    if ($fileIsOkay)
+        {
+        if (NaturalDocs::Project->MenuFileStatus() == ::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->PreviousMenuStateFile())
+        or die "Couldn't save " . NaturalDocs::Project->PreviousMenuStateFile() . ".\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->MenuBackupFile();
+
+        NaturalDocs::File->Copy( NaturalDocs::Project->MenuFile(), $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;
+    };
+
+
+###############################################################################
+# 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;
+            my $noSharedDirectories;
+
+            my @sharedPrefixes;
+            my $noSharedPrefixes;
+
+            foreach my $entry (@{$groupEntry->GroupContent()})
+                {
+                if ($entry->Type() == ::MENU_FILE())
+                    {
+                    # Find the common path among all file entries in this group.
+
+                    if (!$noSharedDirectories)
+                        {
+                        my ($volume, $directoryString, $file) = NaturalDocs::File->SplitPath($entry->Target());
+                        my @entryDirectories = NaturalDocs::File->SplitDirectories($directoryString);
+
+                        if (!scalar @entryDirectories)
+                            {  $noSharedDirectories = 1;  }
+                        elsif (!scalar @sharedDirectories)
+                            {  @sharedDirectories = @entryDirectories;  }
+                        elsif ($entryDirectories[0] ne $sharedDirectories[0])
+                            {  $noSharedDirectories = 1;  }
+
+                        # If both arrays have entries, and the first is shared...
+                        else
+                            {
+                            my $index = 1;
+
+                            while ($index < scalar @sharedDirectories && $index < scalar @entryDirectories &&
+                                     $entryDirectories[$index] eq $sharedDirectories[$index])
+                                {  $index++;  };
+
+                            if ($index < scalar @sharedDirectories)
+                                {  splice(@sharedDirectories, $index);  };
+                            };
+                        };
+
+
+                    # Find the common prefixes among all file entries that are unlocked and don't use the file name as their default title.
+
+                    if (!$noSharedPrefixes && ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0 &&
+                        NaturalDocs::Project->DefaultMenuTitleOf($entry->Target()) ne $entry->Target())
+                        {
+                        my @entryPrefixes = split(/(\.|::|->)/, NaturalDocs::Project->DefaultMenuTitleOf($entry->Target()));
+
+                        # Remove potential leading undef/empty string.
+                        if (!length $entryPrefixes[0])
+                            {  shift @entryPrefixes;  };
+
+                        # Remove last entry.  Something has to exist for the title.
+                        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 @sharedDirectories)
+                {  $noSharedDirectories = 1;  };
+            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($title);
+                        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 @segments = split(/(::|\.|->)/, $title);
+                        if (!length $segments[0])
+                            {  shift @segments;  };
+
+                        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/doctool/Modules/NaturalDocs/Menu/Entry.pm b/docs/doctool/Modules/NaturalDocs/Menu/Entry.pm
new file mode 100644
index 00000000..af443f5d
--- /dev/null
+++ b/docs/doctool/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-2005 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/doctool/Modules/NaturalDocs/NDMarkup.pm b/docs/doctool/Modules/NaturalDocs/NDMarkup.pm
new file mode 100644
index 00000000..bc4dde66
--- /dev/null
+++ b/docs/doctool/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-2005 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/doctool/Modules/NaturalDocs/Parser.pm b/docs/doctool/Modules/NaturalDocs/Parser.pm
new file mode 100644
index 00000000..87671817
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Parser.pm
@@ -0,0 +1,1209 @@
+###############################################################################
+#
+#   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-2005 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use NaturalDocs::Parser::ParsedTopic;
+use NaturalDocs::Parser::Native;
+
+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);
+
+    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>([^<]+)<\/link>/g)
+            {
+            my $linkText = NaturalDocs::NDMarkup->RestoreAmpChars($1);
+            my $linkSymbol = NaturalDocs::SymbolString->FromText($linkText);
+
+            NaturalDocs::SymbolTable->AddReference(::REFERENCE_TEXT(), $linkSymbol,
+                                                                           $topic->Package(), $topic->Using(), $sourceFile);
+            };
+        };
+
+    # Handle any changes to the file.
+    NaturalDocs::ClassHierarchy->AnalyzeChanges();
+    NaturalDocs::SymbolTable->AnalyzeChanges();
+
+    # 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.
+#
+#   Returns:
+#
+#       The number of topics created by this comment, or zero if none.
+#
+sub OnComment #(commentLines, lineNumber)
+    {
+    my ($self, $commentLines, $lineNumber) = @_;
+
+    $self->CleanComment($commentLines);
+
+    return NaturalDocs::Parser::Native->ParseComment($commentLines, $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);
+        };
+
+    # 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)
+        {
+        # If there's only one topic, it's title overrides the file name.  Certain topic types override the file name as well.
+        if (scalar @parsedFile == 1 || NaturalDocs::Topics->TypeInfo( $parsedFile[0]->Type() )->PageTitleIfFirst() )
+            {
+            $defaultMenuTitle = $parsedFile[0]->Title();
+            }
+        else
+            {
+            # If the title ended up being the file name, add a leading section for it.
+            my $name;
+
+            my ($inputDirectory, $relativePath) = NaturalDocs::Settings->SplitFromInputDirectory($sourceFile);
+
+            my ($volume, $dirString, $file) = NaturalDocs::File->SplitPath($relativePath);
+            my @directories = NaturalDocs::File->SplitDirectories($dirString);
+
+            if (scalar @directories > 2)
+                {
+                $dirString = NaturalDocs::File->JoinDirectories('...', $directories[-2], $directories[-1]);
+                $name = NaturalDocs::File->JoinPath(undef, $dirString, $file);
+                }
+            else
+                {
+                $name = $relativePath;
+                }
+
+            unshift @parsedFile,
+                       NaturalDocs::Parser::ParsedTopic->New(::TOPIC_FILE(), $name, 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, leading
+#   and trailing line breaks, trailing whitespace from lines, and expands all tab characters.  It keeps leading whitespace, though,
+#   since it may be needed for example code, and multiple 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.
+        elsif ($line =~ /^([^a-zA-Z0-9 ])\1{3,}$/ ||
+                $line =~ /^([^a-zA-Z0-9 ])\1*([^a-zA-Z0-9 ])\2{3,}([^a-zA-Z0-9 ])\3*$/)
+            {
+            # Convert the original to a blank line.
+            $commentLines->[$index] = '';
+
+            # This has no effect on the vertical line detection.
+            }
+
+        # 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;
+
+    while ($index < scalar @$commentLines)
+        {
+        # 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.  We couldn't do this in the first loop because that would make the regexes over-tolerant.
+
+        if ($leftSide == IS_UNIFORM || $rightSide == IS_UNIFORM)
+            {
+            $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*$//;
+            };
+
+
+        $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.
+        else
+            {
+            my $scope = NaturalDocs::Topics->TypeInfo($topic->Type())->Scope();
+
+            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.
+#
+#   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++;
+            }
+
+        # Transfer information if we have a match.
+        elsif ($topic->Type() == $autoTopic->Type() && index($autoTopic->Title(), $cleanTitle) != -1)
+            {
+            $topic->SetType($autoTopic->Type());
+            $topic->SetPrototype($autoTopic->Prototype());
+
+            if (NaturalDocs::Topics->TypeInfo($topic->Type())->Scope() != ::SCOPE_START())
+                {  $topic->SetPackage($autoTopic->Package());  };
+
+            $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 ($autoTopicIndex < scalar @$autoTopics && !NaturalDocs::Settings->DocumentedOnly())
+        {
+        push @parsedFile, @$autoTopics[$autoTopicIndex..scalar @$autoTopics-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 (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/doctool/Modules/NaturalDocs/Parser/Native.pm b/docs/doctool/Modules/NaturalDocs/Parser/Native.pm
new file mode 100644
index 00000000..b88c7bac
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Parser/Native.pm
@@ -0,0 +1,926 @@
+###############################################################################
+#
+#   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-2005 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: ParseComment
+#
+#   This will be called whenever a comment capable of containing Natural Docs content is found.
+#
+#   Parameters:
+#
+#       commentLines - An arrayref of the comment's lines.  All tabs must be expanded into spaces, and all comment symbols,
+#                               boxes, lines, and trailing whitespace should be removed.  Leading whitespace and multiple blank lines
+#                               should be preserved.
+#       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, lineNumber, parsedTopics)
+    {
+    my ($self, $commentLines, $lineNumber, $parsedTopics) = @_;
+
+    my $topicCount = 0;
+    my $prevLineBlank = 1;
+    my $inCodeSection;
+
+    my $type;
+    my $typeInfo;
+    my $isPlural;
+    my $title;
+    my $symbol;
+    #my $package;  # package variable.
+
+    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;
+            $bodyEnd++;
+            }
+
+        # If the line has a recognized header and the previous line is blank...
+        elsif ($prevLineBlank &&
+                $commentLines->[$index] =~ /^ *([a-z0-9 ]*[a-z0-9]): +(.*)$/i &&
+                (my ($newType, $newTypeInfo, $newIsPlural) = NaturalDocs::Topics->KeywordInfo($1)) )
+            {
+            my $newTitle = $2;
+
+            # Process the previous one, if any.
+
+            if (defined $type)
+                {
+                if ($typeInfo->Scope() == ::SCOPE_START() || $typeInfo->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();
+                };
+
+            ($type, $typeInfo, $isPlural, $title) = ($newType, $newTypeInfo, $newIsPlural, $newTitle);
+
+            $bodyStart = $index + 1;
+            $bodyEnd = $index + 1;
+
+            $prevLineBlank = 0;
+            }
+
+        # Line without recognized header
+        else
+            {
+            $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 (defined $type)
+        {
+        if ($typeInfo->Scope() == ::SCOPE_START() || $typeInfo->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;
+    };
+
+
+
+###############################################################################
+# Group: Support Functions
+
+
+#
+#   Function: MakeParsedTopic
+#
+#   Creates a <NaturalDocs::Parser::ParsedTopic> object for the passed parameters.  Scope is gotten from
+#   the package variable <scope> instead of from the parameters.  The summary is generated from the body.
+#
+#   Parameters:
+#
+#       type         - The <TopicType>.
+#       title          - The title of the topic.
+#       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.
+#       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+] +([^ ].*)$/)
+                {
+                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 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;
+
+
+    # Split the text from the potential tags.
+
+    my @tempTextBlocks = split(/([\*_<>])/, $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 '<' && $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.
+
+                if ($linkText =~ /^(?:mailto\:)?((?:[a-z0-9\-_]+\.)*[a-z0-9\-_]+@(?:[a-z0-9\-]+\.)+[a-z]{2,4})$/i)
+                    {  $output .= '<email>' . NaturalDocs::NDMarkup->ConvertAmpChars($1) . '</email>';  }
+                elsif ($linkText =~ /^(?:http|https|ftp|news|file)\:/i)
+                    {  $output .= '<url>' . NaturalDocs::NDMarkup->ConvertAmpChars($linkText) . '</url>';  }
+                else
+                    {  $output .= '<link>' . NaturalDocs::NDMarkup->ConvertAmpChars($linkText) . '</link>';  };
+                }
+
+            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++;
+        };
+
+
+    # Pull out the e-mail addresses and URLs that aren't in angle brackets.  Preventing > from being the leading character will
+    # prevent it from duplicating <url> and <email> tags, although I don't know if it may be falsely triggered in other situations
+    # as well.
+
+    $output =~ s{
+                        # The previous character can't be an alphanumeric.
+                        (?<!  [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.
+                        (?!  [a-z0-9]  )
+
+                        }
+
+                   {<email>$1<\/email>}igx;
+
+    $output =~ s{
+                        # The previous character can't be an alphanumeric.
+                        (?<!  [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.  This will prevent the URL from ending early just
+                        # to get a match.
+                        (?!  [a-z0-9\-\=\~\@\#\%\&\_\+\/\;\:\?]  )
+
+                        }
+                       {<url>$1<\/url>}igx;
+
+    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\(\{\[\"\'\-\/]$/ )&&
+
+        # 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] !~ /^\=/ ) )
+        {
+        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|\')/ ) ) &&
+
+            # 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] !~ /[>=-]$/ ) )
+        {
+        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/doctool/Modules/NaturalDocs/Parser/ParsedTopic.pm b/docs/doctool/Modules/NaturalDocs/Parser/ParsedTopic.pm
new file mode 100644
index 00000000..e978b985
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Parser/ParsedTopic.pm
@@ -0,0 +1,210 @@
+###############################################################################
+#
+#   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-2005 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.
+
+
+###############################################################################
+# 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: 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) = @_;
+
+    if (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/doctool/Modules/NaturalDocs/Project.pm b/docs/doctool/Modules/NaturalDocs/Project.pm
new file mode 100644
index 00000000..2575fa4c
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Project.pm
@@ -0,0 +1,966 @@
+###############################################################################
+#
+#   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 <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 <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-2005 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use NaturalDocs::Project::File;
+
+use strict;
+use integer;
+
+package NaturalDocs::Project;
+
+
+###############################################################################
+# Group: Variables
+
+#
+#   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>.
+#
+
+
+#
+#   hash: supportedFiles
+#
+#   A hash of all the supported files in the input directory.  The keys are the <FileNames>, and the values are
+#   <NaturalDocs::Project::File> 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;
+
+
+# var: menuFileStatus
+# The <FileStatus> of the project's menu file.
+my $menuFileStatus;
+
+# var: mainTopicsFileStatus
+# The <FileStatus> of the project's main topics file.
+my $mainTopicsFileStatus;
+
+# var: userTopicsFileStatus
+# The <FileStatus> of the project's user topics file.
+my $userTopicsFileStatus;
+
+# var: mainLanguagesFileStatus
+# The <FileStatus> of the project's main languages file.
+my $mainLanguagesFileStatus;
+
+# var: userLanguagesFileStatus
+# The <FileStatus> of the project's user languages file.
+my $userLanguagesFileStatus;
+
+# 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: 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.
+#
+
+
+
+###############################################################################
+# 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->OnMostUsedLanguageChange()> 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->FileInfoFile()))
+        {
+        # 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.35.
+        # We'll tolerate the difference between 1.16 and 1.3 in the loader.
+
+        if ($version >= NaturalDocs::Version->FromString('1.16') && $version <= NaturalDocs::Settings->AppVersion())
+            {
+            $fileIsOkay = 1;
+
+            if ($version < NaturalDocs::Version->FromString('1.35'))
+                {
+                $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->FileInfoFile())
+        or die "Couldn't save project file " . $self->FileInfoFile() . "\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 to disk.
+#
+sub LoadConfigFileInfo
+    {
+    my ($self) = @_;
+
+    my $fileIsOkay;
+    my $version;
+    my $fileName = NaturalDocs::Project->ConfigFileInfoFile();
+
+    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 ($version <= NaturalDocs::Settings->AppVersion())
+                {  $fileIsOkay = 1;  }
+            else
+                {  close(FH_CONFIGFILEINFO);  };
+            }
+
+        else # it's not in binary
+            {  close(FH_CONFIGFILEINFO);  };
+        };
+
+    my @configFiles = ( $self->MenuFile(), \$menuFileStatus,
+                                 $self->MainTopicsFile(), \$mainTopicsFileStatus,
+                                 $self->UserTopicsFile(), \$userTopicsFileStatus,
+                                 $self->MainLanguagesFile(), \$mainLanguagesFileStatus,
+                                 $self->UserLanguagesFile(), \$userLanguagesFileStatus );
+
+    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
+        {
+        while (scalar @configFiles)
+            {
+            my $file = shift @configFiles;
+            my $fileStatus = shift @configFiles;
+
+            if (-e $file)
+                {  $$fileStatus = ::FILE_CHANGED();  }
+            else
+                {  $$fileStatus = ::FILE_DOESNTEXIST();  };
+            };
+        };
+
+    if ($menuFileStatus == ::FILE_SAME() && $rebuildEverything)
+        {  $menuFileStatus = ::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->ConfigFileInfoFile())
+        or die "Couldn't save " . NaturalDocs::Project->ConfigFileInfoFile() . ".\n";
+
+    binmode(FH_CONFIGFILEINFO);
+
+    print FH_CONFIGFILEINFO '' . ::BINARY_FORMAT();
+
+    NaturalDocs::Version->ToBinaryFile(\*FH_CONFIGFILEINFO, NaturalDocs::Settings->AppVersion());
+
+    print FH_CONFIGFILEINFO pack('NNNNN', (stat($self->MenuFile()))[9],
+                                                                (stat($self->MainTopicsFile()))[9],
+                                                                (stat($self->UserTopicsFile()))[9],
+                                                                (stat($self->MainLanguagesFile()))[9],
+                                                                (stat($self->UserLanguagesFile()))[9] );
+
+    close(FH_CONFIGFILEINFO);
+    };
+
+
+#
+#   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->MenuFile() );
+
+        if (-e NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.sym'))
+            {  rename( NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.sym'), $self->SymbolTableFile() );  };
+
+        if (-e NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.files'))
+            {  rename( NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.files'), $self->FileInfoFile() );  };
+
+        if (-e NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.m'))
+            {  rename( NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.m'), $self->PreviousMenuStateFile() );  };
+        };
+    };
+
+
+
+###############################################################################
+# Group: Data File Functions
+
+
+# Function: FileInfoFile
+# Returns the full path to the file information file.
+sub FileInfoFile
+    {  return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDataDirectory(), 'FileInfo.nd' );  };
+
+# Function: ConfigFileInfoFile
+# Returns the full path to the config file information file.
+sub ConfigFileInfoFile
+    {  return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDataDirectory(), 'ConfigFileInfo.nd' );  };
+
+# Function: SymbolTableFile
+# Returns the full path to the symbol table's data file.
+sub SymbolTableFile
+    {  return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDataDirectory(), 'SymbolTable.nd' );  };
+
+# Function: ClassHierarchyFile
+# Returns the full path to the class hierarchy's data file.
+sub ClassHierarchyFile
+    {  return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDataDirectory(), 'ClassHierarchy.nd' );  };
+
+# Function: MenuFile
+# Returns the full path to the project's menu file.
+sub MenuFile
+    {  return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDirectory(), 'Menu.txt' );  };
+
+# Function: MenuFileStatus
+# Returns the <FileStatus> of the project's menu file.
+sub MenuFileStatus
+    {  return $menuFileStatus;  };
+
+# Function: MainTopicsFile
+# Returns the full path to the main topics file.
+sub MainTopicsFile
+    {  return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ConfigDirectory(), 'Topics.txt' );  };
+
+# Function: MainTopicsFileStatus
+# Returns the <FileStatus> of the project's main topics file.
+sub MainTopicsFileStatus
+    {  return $mainTopicsFileStatus;  };
+
+# Function: UserTopicsFile
+# Returns the full path to the user's topics file.
+sub UserTopicsFile
+    {  return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDirectory(), 'Topics.txt' );  };
+
+# Function: UserTopicsFileStatus
+# Returns the <FileStatus> of the project's user topics file.
+sub UserTopicsFileStatus
+    {  return $userTopicsFileStatus;  };
+
+# Function: MainLanguagesFile
+# Returns the full path to the main languages file.
+sub MainLanguagesFile
+    {  return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ConfigDirectory(), 'Languages.txt' );  };
+
+# Function: MainLanguagesFileStatus
+# Returns the <FileStatus> of the project's main languages file.
+sub MainLanguagesFileStatus
+    {  return $mainLanguagesFileStatus;  };
+
+# Function: UserLanguagesFile
+# Returns the full path to the user's languages file.
+sub UserLanguagesFile
+    {  return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDirectory(), 'Languages.txt' );  };
+
+# Function: UserLanguagesFileStatus
+# Returns the <FileStatus> of the project's user languages file.
+sub UserLanguagesFileStatus
+    {  return $userLanguagesFileStatus;  };
+
+# Function: SettingsFile
+# Returns the full path to the project's settings file.
+sub SettingsFile
+    {  return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDirectory(), 'Settings.txt' );  };
+
+# Function: PreviousSettingsFile
+# Returns the full path to the project's previous settings file.
+sub PreviousSettingsFile
+    {  return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDataDirectory(), 'PreviousSettings.nd' );  };
+
+# Function: PreviousMenuStateFile
+# Returns the full path to the project's previous menu state file.
+sub PreviousMenuStateFile
+    {  return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDataDirectory(), 'PreviousMenuState.nd' );  };
+
+# Function: MenuBackupFile
+# Returns the full path to the project's menu backup file, which is used to save the original menu in some situations.
+sub MenuBackupFile
+    {  return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDirectory(), 'Menu_Backup.txt' );  };
+
+
+
+###############################################################################
+# 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 ($menuFileStatus == ::FILE_SAME())
+        {  $menuFileStatus = ::FILE_CHANGED();  };
+    };
+
+
+# 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: 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::File->LastModified()>.  Also sets <mostUsedLanguage>.
+#
+sub GetAllSupportedFiles
+    {
+    my ($self) = @_;
+
+    my @directories = @{NaturalDocs::Settings->InputDirectories()};
+
+    # 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 (NaturalDocs::File->IsCaseSensitive())
+            {  $excludedDirectories{$excludedDirectory} = 1;  }
+        else
+            {  $excludedDirectories{lc($excludedDirectory)} = 1;  };
+        };
+
+
+    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 (NaturalDocs::File->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
+                {
+                if (my $language = NaturalDocs::Languages->LanguageOf($fullEntry))
+                    {
+                    $supportedFiles{$fullEntry} = NaturalDocs::Project::File->New(undef, (stat($fullEntry))[9], undef, undef);
+                    $languageCounts{$language->Name()}++;
+                    };
+                };
+            };
+        };
+
+
+    my $topCount = 0;
+
+    while (my ($language, $count) = each %languageCounts)
+        {
+        if ($count > $topCount && $language ne 'Text File')
+            {
+            $topCount = $count;
+            $mostUsedLanguage = $language;
+            };
+        };
+    };
+
+
+1;
diff --git a/docs/doctool/Modules/NaturalDocs/Project/File.pm b/docs/doctool/Modules/NaturalDocs/Project/File.pm
new file mode 100644
index 00000000..e901d837
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Project/File.pm
@@ -0,0 +1,113 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Project::File
+#
+###############################################################################
+#
+#   A simple information class about project files.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2005 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Project::File;
+
+
+
+###############################################################################
+# 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/doctool/Modules/NaturalDocs/ReferenceString.pm b/docs/doctool/Modules/NaturalDocs/ReferenceString.pm
new file mode 100644
index 00000000..c9bca75a
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/ReferenceString.pm
@@ -0,0 +1,301 @@
+###############################################################################
+#
+#   Package: NaturalDocs::ReferenceString
+#
+###############################################################################
+#
+#   A package to manage <ReferenceString> handling throughout the program.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2005 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' );
+
+
+#
+#   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;
+
+
+#
+#
+#   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.
+#       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 #(type, symbol, scope, using, resolvingFlags)
+    {
+    my ($self, $type, $symbol, $scope, $using, $resolvingFlags) = @_;
+
+    if ($type == ::REFERENCE_TEXT() || $resolvingFlags == 0)
+       {  $resolvingFlags = undef;  };
+
+    # The format is [type] 0x1E [resolving flags] \x1E [symbol] 0x1E [scope] ( 0x1E [using] )*
+    # The format of the symbol, scope, and usings are the identifiers separated by 0x1F characters.
+    # If scope is undef but using isn't, there will be two 0x1E's to signify the missing scope.
+
+    my @identifiers = NaturalDocs::SymbolString->IdentifiersOf($symbol);
+
+    my $string = $type . "\x1E" . $resolvingFlags . "\x1E" . join("\x1F", @identifiers);
+
+    if (defined $scope)
+        {
+        @identifiers = NaturalDocs::SymbolString->IdentifiersOf($scope);
+        $string .= "\x1E" . join("\x1F", @identifiers);
+        };
+
+    if (defined $using)
+        {
+        my @usingStrings;
+
+        foreach my $using (@$using)
+            {
+            @identifiers = NaturalDocs::SymbolString->IdentifiersOf($using);
+            push @usingStrings, join("\x1F", @identifiers);
+            };
+
+        if (!defined $scope)
+            {  $string .= "\x1E";  };
+
+        $string .= "\x1E" . join("\x1E", @usingStrings);
+        };
+
+    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]
+#       > [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, referenceString, binaryFormatFlags)
+    {
+    my ($self, $fileHandle, $referenceString, $binaryFormatFlags) = @_;
+
+    my ($type, $symbol, $scope, $using, $resolvingFlags) = $self->InformationOf($referenceString);
+
+    # [SymbolString: Symbol or undef for an undef reference]
+
+    NaturalDocs::SymbolString->ToBinaryFile($fileHandle, $symbol);
+
+    # [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, binaryFormatFlags, type, resolvingFlags)
+    {
+    my ($self, $fileHandle, $binaryFormatFlags, $type, $resolvingFlags) = @_;
+
+    # [SymbolString: Symbol or undef for an undef reference]
+
+    my $symbol = NaturalDocs::SymbolString->FromBinaryFile($fileHandle);
+
+    if (!defined $symbol)
+        {  return undef;  };
+
+    # [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, $scope, $usingSymbol, $resolvingFlags);
+    };
+
+
+#
+#   Function: InformationOf
+#
+#   Returns the information encoded in a <ReferenceString>.
+#
+#   Parameters:
+#
+#       referenceString - The <ReferenceString> to decode.
+#
+#   Returns:
+#
+#       The array ( type, symbol, scope, using, resolvingFlags ).
+#
+#       type - The <ReferenceType>.
+#       symbol - The <SymbolString>.
+#       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)
+    {
+    my ($self, $referenceString) = @_;
+
+    my ($type, $resolvingFlags, $symbolString, $scopeString, @usingStrings) = split(/\x1E/, $referenceString);
+
+    if (!length $resolvingFlags)
+        {  $resolvingFlags = undef;  };
+
+    my @identifiers = split(/\x1F/, $symbolString);
+    my $symbol = NaturalDocs::SymbolString->Join(@identifiers);
+
+    my $scope;
+    if (defined $scopeString && length($scopeString))
+        {
+        @identifiers = split(/\x1F/, $scopeString);
+        $scope = NaturalDocs::SymbolString->Join(@identifiers);
+        };
+
+    my $using;
+    if (scalar @usingStrings)
+        {
+        $using = [ ];
+        foreach my $usingString (@usingStrings)
+            {
+            @identifiers = split(/\x1F/, $usingString);
+            push @$using, NaturalDocs::SymbolString->Join(@identifiers);
+            };
+        };
+
+    return ( $type, $symbol, $scope, $using, $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)
+    {
+    my ($self, $referenceString) = @_;
+
+    $referenceString =~ /^([^\x1E]+)/;
+    return $1;
+    };
+
+
+1;
diff --git a/docs/doctool/Modules/NaturalDocs/Settings.pm b/docs/doctool/Modules/NaturalDocs/Settings.pm
new file mode 100644
index 00000000..084c77bc
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Settings.pm
@@ -0,0 +1,1258 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Settings
+#
+###############################################################################
+#
+#   A package to handle the command line and various other program settings.
+#
+#   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()>.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2005 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use Cwd ();
+
+use NaturalDocs::Settings::BuildTarget;
+
+use strict;
+use integer;
+
+package NaturalDocs::Settings;
+
+
+
+###############################################################################
+# Group: Variables
+
+
+# handle: SETTINGSFILEHANDLE
+# The file handle used with <Settings.txt>.
+
+# 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: 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;
+
+# 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: isQuiet
+# Whether the script should be run in quiet mode or not.
+my $isQuiet;
+
+# 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: Settings.txt
+#
+#   The file that stores the Natural Docs build targets.
+#
+#   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.
+#
+#       > # [comment]
+#
+#       The file supports single-line comments via #.  They can appear alone on a line or after content.
+#
+#       > Format: [version]
+#       > TabLength: [length]
+#       > Style: [style]
+#
+#       The file format version, tab length, and default style are specified as above.  Each can only be specified once, with
+#       subsequent ones being ignored.  Notice that the tags correspond to the long forms of the command line options.
+#
+#       > Source: [directory]
+#       > Input: [directory]
+#
+#       The input directory is specified as above.  As in the command line, either "Source" or "Input" can be used.
+#
+#       > [Extension Option]: [opton]
+#
+#       Options for extensions can be specified as well.  The long form is used as the tag.
+#
+#       > Option: [HeadersOnly], [Quiet], [Extension Option]
+#
+#       Options that don't have parameters can be specified in an Option line.  The commas are not required.
+#
+#       > Output: [name]
+#
+#       Specifies an output target with a user defined name.  The name is what will be referenced from the command line, and the
+#       name "All" is reserved.
+#
+#       *The options below can only be specified after an output tag.*  Everything that follows an output tag is part of that target's
+#       options until the next output tag.
+#
+#       > Format: [format]
+#
+#       The output format of the target.
+#
+#       > Directory: [directory]
+#       > Location: [directory]
+#       > Folder: [directory]
+#
+#       The output directory of the target.  All are synonyms.
+#
+#       > Style: [style]
+#
+#       The style of the output target.  This overrides the default and is optional.
+#
+
+
+#
+#   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)]
+#       > [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.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 directories, which can later be retrieved with <InputDirectoryNameOf()>.
+#
+#   Parameters:
+#
+#       hints - A hashref of suggested 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.
+#
+sub GenerateDirectoryNames #(hints)
+    {
+    my ($self, $hints) = @_;
+
+    my %usedNames;
+
+
+    if (defined $hints)
+        {
+        # 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 %$hints;
+        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 %$hints)
+                {
+                $hints->{$directory} = $conversion{ $hints->{$directory} };
+                };
+            };
+
+
+        # Now we apply all the names from the hints.
+
+        for (my $i = 0; $i < scalar @inputDirectories; $i++)
+            {
+            if (exists $hints->{$inputDirectories[$i]})
+                {
+                $inputDirectoryNames[$i] = $hints->{$inputDirectories[$i]};
+                $usedNames{ $hints->{$inputDirectories[$i]} } = 1;
+                delete $hints->{$inputDirectories[$i]};
+                };
+            };
+
+
+        # Any remaining hints are saved as removed directories.
+
+        while (my ($directory, $name) = each %$hints)
+            {
+            push @removedInputDirectories, $directory;
+            push @removedInputDirectoryNames, $name;
+            };
+        };
+
+
+    # Now we generate names for anything remaining.
+
+    my $nameCounter = 1;
+
+    for (my $i = 0; $i < scalar @inputDirectories; $i++)
+        {
+        if (!defined $inputDirectoryNames[$i])
+            {
+            while (exists $usedNames{$nameCounter})
+                {  $nameCounter++;  };
+
+            $inputDirectoryNames[$i] = $nameCounter;
+            $usedNames{$nameCounter} = 1;
+
+            $nameCounter++;
+            };
+        };
+    };
+
+
+
+###############################################################################
+# 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) = @_;
+
+    my $name;
+
+    for (my $i = 0; $i < scalar @inputDirectories && !defined $name; $i++)
+        {
+        if ($directory eq $inputDirectories[$i])
+            {  $name = $inputDirectoryNames[$i];  };
+        };
+
+    for (my $i = 0; $i < scalar @removedInputDirectories && !defined $name; $i++)
+        {
+        if ($directory eq $removedInputDirectories[$i])
+            {  $name = $removedInputDirectoryNames[$i];  };
+        };
+
+    return $name;
+    };
+
+
+#
+#   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: 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: IsQuiet
+# Returns whether the script should be run in quiet mode or not.
+sub IsQuiet
+    {  return $isQuiet;  };
+
+# 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.35';  };
+
+#
+#   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',
+                                  'output'  => '-o',
+                                  'project' => '-p',
+                                  'documentedonly' => '-do',
+                                  'style'    => '-s',
+                                  'rebuild' => '-r',
+                                  'rebuildoutput' => '-ro',
+                                  'tablength' => '-t',
+                                  'quiet'    => '-q',
+                                  'headersonly' => '-ho',
+                                  'help'     => '-h',
+                                  'autogroup' => '-ag',
+                                  'noautogroup' => '-nag',
+                                  'charset' => '-cs',
+                                  'characterset' => '-cs' );
+
+
+    my @errorMessages;
+
+    my $valueRef;
+    my $option;
+
+    my @outputStrings;
+
+
+    # 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 '-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();
+                    }
+                elsif ($option eq '-ro')
+                    {
+                    NaturalDocs::Project->RebuildEverything();
+                    }
+                elsif ($option eq '-do')
+                    {  $documentedOnly = 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(undef, $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.';  };
+
+
+    # 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 the input (source) directory.  Required.\n"
+    . "     Can be specified multiple times.\n"
+    . "\n"
+    . " -o [fmt] [dir]\n--output [fmt] [dir]\n"
+    . "    Specifies the 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"
+    . " -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"
+    . " -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 = 1;
+    my $fileName = NaturalDocs::Project->PreviousSettingsFile();
+    my $version;
+
+    if (!open(PREVIOUS_SETTINGS_FILEHANDLE, '<' . $fileName))
+        {  $fileIsOkay = undef;  }
+    else
+        {
+        # See if it's binary.
+        binmode(PREVIOUS_SETTINGS_FILEHANDLE);
+
+        my $firstChar;
+        read(PREVIOUS_SETTINGS_FILEHANDLE, $firstChar, 1);
+
+        if ($firstChar != ::BINARY_FORMAT())
+            {
+            close(PREVIOUS_SETTINGS_FILEHANDLE);
+            $fileIsOkay = undef;
+            }
+        else
+            {
+            $version = NaturalDocs::Version->FromBinaryFile(\*PREVIOUS_SETTINGS_FILEHANDLE);
+
+            # The file format changed in 1.33.
+
+            if ($version > NaturalDocs::Settings->AppVersion() || $version < NaturalDocs::Version->FromString('1.33'))
+                {
+                close(PREVIOUS_SETTINGS_FILEHANDLE);
+                $fileIsOkay = undef;
+                };
+            };
+        };
+
+
+    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)]
+        # [AString16: charset]
+
+        read(PREVIOUS_SETTINGS_FILEHANDLE, $raw, 5);
+        my ($prevTabLength, $prevDocumentedOnly, $prevNoAutoGroup, $prevCharsetLength)
+            = unpack('CCCn', $raw);
+
+        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 ($prevDocumentedOnly != $self->DocumentedOnly() ||
+            $prevNoAutoGroup != $self->NoAutoGroup())
+            {
+            NaturalDocs::Project->ReparseEverything();
+            };
+
+        my $prevCharset;
+        read(PREVIOUS_SETTINGS_FILEHANDLE, $prevCharset, $prevCharsetLength);
+
+        if ($prevCharset ne $charset)
+            {  NaturalDocs::Project->RebuildEverything();  };
+
+
+        # [UInt8: number of input directories]
+
+        read(PREVIOUS_SETTINGS_FILEHANDLE, $raw, 1);
+        my $inputDirectoryCount = unpack('C', $raw);
+
+        while ($inputDirectoryCount)
+            {
+            # [AString16: input directory] [AString16: input directory name] ...
+
+            read(PREVIOUS_SETTINGS_FILEHANDLE, $raw, 2);
+            my $inputDirectoryLength = unpack('n', $raw);
+
+            my $inputDirectory;
+            read(PREVIOUS_SETTINGS_FILEHANDLE, $inputDirectory, $inputDirectoryLength);
+
+            read (PREVIOUS_SETTINGS_FILEHANDLE, $raw, 2);
+            my $inputDirectoryNameLength = unpack('n', $raw);
+
+            my $inputDirectoryName;
+            read(PREVIOUS_SETTINGS_FILEHANDLE, $inputDirectoryName, $inputDirectoryNameLength);
+
+            # Not doing anything with this for now.
+
+            $inputDirectoryCount--;
+            };
+
+
+        # [UInt8: number of output targets]
+
+        read(PREVIOUS_SETTINGS_FILEHANDLE, $raw, 1);
+        my $outputTargetCount = unpack('C', $raw);
+
+        # Keys are the directories, values are the command line options.
+        my %previousOutputDirectories;
+
+        while ($outputTargetCount)
+            {
+            # [AString16: output directory] [AString16: output format command line option] ...
+
+            read(PREVIOUS_SETTINGS_FILEHANDLE, $raw, 2);
+            my $outputDirectoryLength = unpack('n', $raw);
+
+            my $outputDirectory;
+            read(PREVIOUS_SETTINGS_FILEHANDLE, $outputDirectory, $outputDirectoryLength);
+
+            read (PREVIOUS_SETTINGS_FILEHANDLE, $raw, 2);
+            my $outputCommandLength = unpack('n', $raw);
+
+            my $outputCommand;
+            read(PREVIOUS_SETTINGS_FILEHANDLE, $outputCommand, $outputCommandLength);
+
+            $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;
+                };
+            };
+
+        close(PREVIOUSSTATEFILEHANDLE);
+        };
+    };
+
+
+#
+#   Function: SavePreviousSettings
+#
+#   Saves the settings into <PreviousSettings.nd>.
+#
+sub SavePreviousSettings
+    {
+    my ($self) = @_;
+
+    open (PREVIOUS_SETTINGS_FILEHANDLE, '>' . NaturalDocs::Project->PreviousSettingsFile())
+        or die "Couldn't save " . NaturalDocs::Project->PreviousSettingsFile() . ".\n";
+
+    binmode(PREVIOUS_SETTINGS_FILEHANDLE);
+
+    print PREVIOUS_SETTINGS_FILEHANDLE '' . ::BINARY_FORMAT();
+    NaturalDocs::Version->ToBinaryFile(\*PREVIOUS_SETTINGS_FILEHANDLE, NaturalDocs::Settings->AppVersion());
+
+    # [UInt8: tab length]
+    # [UInt8: documented only (0 or 1)]
+    # [UInt8: no auto-group (0 or 1)]
+    # [AString16: charset]
+    # [UInt8: number of input directories]
+
+    my $inputDirectories = $self->InputDirectories();
+
+    print PREVIOUS_SETTINGS_FILEHANDLE pack('CCCnA*C', $self->TabLength(), ($self->DocumentedOnly() ? 1 : 0),
+                                                                                        ($self->NoAutoGroup() ? 1 : 0), length($charset), $charset,
+                                                                                         scalar @$inputDirectories);
+
+    foreach my $inputDirectory (@$inputDirectories)
+        {
+        my $inputDirectoryName = $self->InputDirectoryNameOf($inputDirectory);
+
+        # [AString16: input directory] [AString16: input directory name] ...
+        print PREVIOUS_SETTINGS_FILEHANDLE pack('nA*nA*', length($inputDirectory), $inputDirectory,
+                                                                                          length($inputDirectoryName), $inputDirectoryName);
+        };
+
+    # [UInt8: number of output targets]
+
+    my $buildTargets = $self->BuildTargets();
+    print PREVIOUS_SETTINGS_FILEHANDLE pack('C', scalar @$buildTargets);
+
+    foreach my $buildTarget (@$buildTargets)
+        {
+        my $buildTargetDirectory = $buildTarget->Directory();
+        my $buildTargetCommand = $buildTarget->Builder()->CommandLineOption();
+
+        # [AString16: output directory] [AString16: output format command line option] ...
+        print PREVIOUS_SETTINGS_FILEHANDLE pack('nA*nA*', length($buildTargetDirectory), $buildTargetDirectory,
+                                                                                          length($buildTargetCommand), $buildTargetCommand);
+        };
+
+    close(PREVIOUS_SETTINGS_FILEHANDLE);
+    };
+
+
+1;
diff --git a/docs/doctool/Modules/NaturalDocs/Settings/BuildTarget.pm b/docs/doctool/Modules/NaturalDocs/Settings/BuildTarget.pm
new file mode 100644
index 00000000..8494a902
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Settings/BuildTarget.pm
@@ -0,0 +1,91 @@
+###############################################################################
+#
+#   Class: NaturalDocs::Settings::BuildTarget
+#
+###############################################################################
+#
+#   A class that stores information about a build target.
+#
+#   Notes:
+#
+#       <Name()> is not used yet.  It's present because targets can be named in the settings file ("api", "apipdf", etc.) but the
+#       settings file isn't implemented yet, so just set it to undef.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2005 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Settings::BuildTarget;
+
+
+###############################################################################
+# Group: Implementation
+
+
+#
+#   Constants: Members
+#
+#   The class is implemented as a blessed arrayref with the members below.
+#
+#       NAME           - The name of the target.
+#       BUILDER      - The <NaturalDocs::Builder::Base>-derived object for the target's output format.
+#       DIRECTORY - The output directory of the target.
+#
+use constant NAME => 0;
+use constant BUILDER => 1;
+use constant DIRECTORY => 2;
+# New depends on the order of these constants.
+
+
+###############################################################################
+# Group: Functions
+
+#
+#   Function: New
+#
+#   Creates and returns a new object.
+#
+#   Parameters:
+#
+#       name - The name of the target.
+#       builder - The <NaturalDocs::Builder::Base>-derived object for the target's output format.
+#       directory - The directory to place the output files in.
+#
+sub New #(name, builder, directory, style)
+    {
+    my $package = shift;
+
+    # This depends on the order of the parameters matching the order of the constants.
+    my $object = [ @_ ];
+    bless $object, $package;
+
+    return $object;
+    };
+
+
+# Function: Name
+# Returns the target's name.
+sub Name
+    {  return $_[0]->[NAME];  };
+
+# Function: SetName
+# Changes the target's name.
+sub SetName #(name)
+    {  $_[0]->[NAME] = $_[1];  };
+
+# Function: Builder
+# Returns the <NaturalDocs::Builder::Base>-derived object for the target's output format.
+sub Builder
+    {  return $_[0]->[BUILDER];  };
+
+# Function: Directory
+# Returns the directory for the traget's output files.
+sub Directory
+    {  return $_[0]->[DIRECTORY];  };
+
+
+1;
diff --git a/docs/doctool/Modules/NaturalDocs/StatusMessage.pm b/docs/doctool/Modules/NaturalDocs/StatusMessage.pm
new file mode 100644
index 00000000..5edb91c2
--- /dev/null
+++ b/docs/doctool/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-2005 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)
+            {
+            print $message . ' (' . ($completed / $total) . '%)' . "\n";
+            $lastMessageTime = time();
+            };
+        };
+    };
+
+1;
diff --git a/docs/doctool/Modules/NaturalDocs/SymbolString.pm b/docs/doctool/Modules/NaturalDocs/SymbolString.pm
new file mode 100644
index 00000000..f573b188
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/SymbolString.pm
@@ -0,0 +1,208 @@
+###############################################################################
+#
+#   Package: NaturalDocs::SymbolString
+#
+###############################################################################
+#
+#   A package to manage <SymbolString> handling throughout the program.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2005 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 #(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;
+
+    # 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;
+                };
+            }
+        else
+            {
+            $symbolString .= $piece;
+            $lastWasSeparator = 0;
+            };
+        };
+
+    $symbolString =~ s/\x1F$//;
+
+    return $symbolString;
+    };
+
+
+#
+#   Function: ToText
+#
+#   Converts a <SymbolString> to text, using the passed separator.
+#
+sub ToText #(symbolString, 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, 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)
+    {
+    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 #(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 #(identifier/symbol, 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/doctool/Modules/NaturalDocs/SymbolTable.pm b/docs/doctool/Modules/NaturalDocs/SymbolTable.pm
new file mode 100644
index 00000000..f3a5465d
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/SymbolTable.pm
@@ -0,0 +1,1810 @@
+###############################################################################
+#
+#   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-2005 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;
+
+
+#
+#   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.
+#
+
+
+###############################################################################
+# Group: File Functions
+
+
+#
+#   Function: Load
+#
+#   Loads the symbol table from disk.
+#
+sub Load
+    {
+    my ($self) = @_;
+
+    my $fileIsOkay;
+
+    if (open(SYMBOLTABLE_FILEHANDLE, '<' . NaturalDocs::Project->SymbolTableFile()))
+        {
+        # 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 ($version >= NaturalDocs::Version->FromString('1.3') && $version <= NaturalDocs::Settings->AppVersion())
+                {  $fileIsOkay = 1;  }
+            else
+                {  close(PREVIOUSSTATEFILEHANDLE);  };
+            }
+
+        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: 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 the symbol table to disk.  It is written to <SymbolTable.nd>.
+#
+sub Save
+    {
+    my ($self) = @_;
+
+    open (SYMBOLTABLE_FILEHANDLE, '>' . NaturalDocs::Project->SymbolTableFile())
+        or die "Couldn't save " . NaturalDocs::Project->SymbolTableFile() . ".\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);
+    };
+
+
+
+###############################################################################
+# 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, $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, $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 #(type)
+    {
+    my ($self, $type) = @_;
+    return ($rebuildIndexes || defined $indexChanges{$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 #(type)
+    {
+    my ($self, $type) = @_;
+
+    $indexChanges{$type} = 1;
+    $indexChanges{::TOPIC_GENERAL()} = 1;
+    };
+
+
+#
+#   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, $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/doctool/Modules/NaturalDocs/SymbolTable/File.pm b/docs/doctool/Modules/NaturalDocs/SymbolTable/File.pm
new file mode 100644
index 00000000..712a9322
--- /dev/null
+++ b/docs/doctool/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-2005 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/doctool/Modules/NaturalDocs/SymbolTable/IndexElement.pm b/docs/doctool/Modules/NaturalDocs/SymbolTable/IndexElement.pm
new file mode 100644
index 00000000..da08cf4f
--- /dev/null
+++ b/docs/doctool/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-2005 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/doctool/Modules/NaturalDocs/SymbolTable/Reference.pm b/docs/doctool/Modules/NaturalDocs/SymbolTable/Reference.pm
new file mode 100644
index 00000000..830bf60b
--- /dev/null
+++ b/docs/doctool/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-2005 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/doctool/Modules/NaturalDocs/SymbolTable/ReferenceTarget.pm b/docs/doctool/Modules/NaturalDocs/SymbolTable/ReferenceTarget.pm
new file mode 100644
index 00000000..bff8fb23
--- /dev/null
+++ b/docs/doctool/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-2005 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/doctool/Modules/NaturalDocs/SymbolTable/Symbol.pm b/docs/doctool/Modules/NaturalDocs/SymbolTable/Symbol.pm
new file mode 100644
index 00000000..b607269a
--- /dev/null
+++ b/docs/doctool/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-2005 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/doctool/Modules/NaturalDocs/SymbolTable/SymbolDefinition.pm b/docs/doctool/Modules/NaturalDocs/SymbolTable/SymbolDefinition.pm
new file mode 100644
index 00000000..fddff164
--- /dev/null
+++ b/docs/doctool/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-2005 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/doctool/Modules/NaturalDocs/Topics.pm b/docs/doctool/Modules/NaturalDocs/Topics.pm
new file mode 100644
index 00000000..24418bdd
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Topics.pm
@@ -0,0 +1,1351 @@
+###############################################################################
+#
+#   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-2005 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.
+#
+#       > Variable Type: [yes|no]
+#
+#       Whether the topic can be used as a variable type.  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.4:
+#
+#           Added Variable Type.
+#
+#       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->MainTopicsFile());
+        }
+
+
+    $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->UserTopicsFile());
+        }
+    };
+
+
+#
+#   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->MainTopicsFile();
+        $status = NaturalDocs::Project->MainTopicsFileStatus();
+        }
+    else
+        {
+        $file = NaturalDocs::Project->UserTopicsFile();
+        $status = NaturalDocs::Project->UserTopicsFileStatus();
+        };
+
+    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 'variable type')
+                {
+                $value = lc($value);
+
+                if ($value eq 'yes')
+                    {
+                    if (defined $topicTypeObject)
+                        {  $topicTypeObject->SetVariableType(1);  };
+                    }
+                elsif ($value eq 'no')
+                    {
+                    if (defined $topicTypeObject)
+                        {  $topicTypeObject->SetVariableType(0);  };
+                    }
+                else
+                    {
+                    NaturalDocs::ConfigFile->AddError('Variable Type 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->MainTopicsFileStatus() == ::FILE_SAME())
+            {  return;  };
+        $file = NaturalDocs::Project->MainTopicsFile();
+        }
+    else
+        {
+        # We have to check the main one two because this lists the topics defined in it.
+        if (NaturalDocs::Project->UserTopicsFileStatus() == ::FILE_SAME() &&
+            NaturalDocs::Project->MainTopicsFileStatus() == ::FILE_SAME())
+            {  return;  };
+        $file = NaturalDocs::Project->UserTopicsFile();
+        };
+
+
+    # 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 'variable type' ||
+                    $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"
+    . "# Variable Type: [yes|no]\n"
+    . "#    Whether the topics can be a variable type.  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', 'Variable Type', '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/doctool/Modules/NaturalDocs/Topics/Type.pm b/docs/doctool/Modules/NaturalDocs/Topics/Type.pm
new file mode 100644
index 00000000..c5558acb
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Topics/Type.pm
@@ -0,0 +1,155 @@
+###############################################################################
+#
+#   Package: NaturalDocs::Topics::Type
+#
+###############################################################################
+#
+#   A class storing information about a <TopicType>.
+#
+###############################################################################
+
+# This file is part of Natural Docs, which is Copyright (C) 2003-2005 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()',
+                                                 'VARIABLE_TYPE',          'VariableType()',    'SetVariableType()',
+                                                 '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.
+#   VARIABLE_TYPE - Whether the topic can be a variable type.
+#   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.
+#   VariableType - Returns whether the topic can be a variable type.
+#   SetVariableType - Sets whether the topic can be a variable type.
+#
+
+
+#
+#   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/doctool/Modules/NaturalDocs/Version.pm b/docs/doctool/Modules/NaturalDocs/Version.pm
new file mode 100644
index 00000000..67f1a331
--- /dev/null
+++ b/docs/doctool/Modules/NaturalDocs/Version.pm
@@ -0,0 +1,201 @@
+###############################################################################
+#
+#   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-2005 Greg Valure
+# Natural Docs is licensed under the GPL
+
+use strict;
+use integer;
+
+package NaturalDocs::Version;
+
+#
+#   About: Format
+#
+#   Version numbers are represented as major.minor.  Major is from 0 to 255, and minor can have one or two digits.  Minor is
+#   interpreted as a decimal, so 1.25 is less than 1.3.
+#
+
+# Group: Functions
+
+
+#
+#   Function: FromString
+#
+#   Converts a version string to a <VersionInt>.
+#
+sub FromString #(string)
+    {
+    my ($self, $string) = @_;
+
+    if ($string eq '1')
+        {
+        return 91;  # 0.91
+        }
+    else
+        {
+        $string =~ /^(\d+)\.(\d+)$/;
+        my ($major, $minor) = ($1, $2);
+
+        if (length $minor == 1)
+            {  $minor *= 10;  };
+
+        return ($major << 8) | $minor;
+        };
+    };
+
+#
+#   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 #(fileHandle)
+    {
+    my ($self, $fileHandle) = @_;
+
+    my $version;
+    read($fileHandle, $version, 2);
+
+    # A big-endian UInt16 as a shortcut to the integer format.
+    return unpack('n', $version);
+    };
+
+#
+#   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 #(fileHandle)
+    {
+    my ($self, $fileHandle) = @_;
+
+    my $version = <$fileHandle>;
+    ::XChomp(\$version);
+
+    return $self->FromString($version);
+    };
+
+
+#
+#   Function: ToString
+#
+#   Converts a <VersionInt> to a string.
+#
+sub ToString #(integer)
+    {
+    my ($self, $integer) = @_;
+
+    my $major = $integer >> 8;
+    my $minor = $integer & 0x00FF;
+
+    if ($minor % 10 == 0)
+        {  $minor /= 10;  }
+    elsif ($minor < 10)
+        {  $minor = '0' . $minor;  };
+
+    return $major . '.' . $minor;
+    };
+
+
+#
+#   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 #(fileHandle, version)
+    {
+    my ($self, $fileHandle, $version) = @_;
+
+    print $fileHandle $self->ToString($version) . "\n";
+    };
+
+
+#
+#   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 #(fileHandle, version)
+    {
+    my ($self, $fileHandle, $version) = @_;
+
+    # Big-endian UInt16 as a shortcut to the binary format.
+    print $fileHandle pack('n', $version);
+    };
+
+
+
+###############################################################################
+# Group: Implementation
+
+#
+#   About: String Format
+#
+#   String versions are normally in the common major.minor format, with the exception of "1".
+#
+#   If the string is "1" and not "1.0", it's represents releases 0.85 through 0.91, since those had a separate version number for data
+#   files.  We switched to using the app version number in 0.95.  This issue does not apply to binary data files since they came
+#   after 0.95.
+#
+#   Text files with "1" as the version will be interpreted as 0.91, since this should not cause compatibility problems.  The only
+#   file format changes between 0.85 and 0.91 were to <PreviousMenuState.nd>, which didn't exist in 0.85 and didn't change
+#   between 0.9 and 0.91, and <Menu.txt>, which only changed in 0.9 to add index entries.
+#
+
+#
+#   About: Integer Format
+#
+#   <VersionInts> are 16-bit unsigned values.  The major version is the high-order byte, and the minor the low-order byte.
+#   The minor is always stored with two decimals, so 0.9 would be stored as 0 and 90.
+#
+
+#
+#   About: Binary File Format
+#
+#   In binary files, versions are two 8-bit unsigned values, appearing major then minor.  The minor is always stored with two
+#   decimals, so 0.9 would be stored as 0 and 90.  It's in the <Integer Format> if interpreted as a _big-endian_ 16-bit value.
+#
+
+#
+#   About: Text File Format
+#
+#   In text files, versions are the <String Format> followed by a native line break.
+#
+
+
+1;