<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">###############################################################################
#
#   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 &lt;Config and Data File Functions&gt; are available immediately, except for the status functions.
#
#       - &lt;ReparseEverything()&gt; and &lt;RebuildEverything()&gt; are available immediately, because they may need to be called
#         after &lt;LoadConfigFileInfo()&gt; but before &lt;LoadSourceFileInfo()&gt;.
#
#       - Prior to &lt;LoadConfigFileInfo()&gt;, &lt;NaturalDocs::Settings&gt; must be initialized.
#
#       - After &lt;LoadConfigFileInfo()&gt;, the status &lt;Config and Data File Functions&gt; are available as well.
#
#       - Prior to &lt;LoadSourceFileInfo()&gt;, &lt;NaturalDocs::Settings&gt; and &lt;NaturalDocs::Languages&gt; must be initialized.
#
#       - After &lt;LoadSourceFileInfo()&gt;, the rest of the &lt;Source File Functions&gt; are available.
#
###############################################################################

# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
# Natural Docs is licensed under the GPL

use NaturalDocs::Project::SourceFile;
use NaturalDocs::Project::ImageFile;

use strict;
use integer;

package NaturalDocs::Project;


###############################################################################
# Group: File Handles

#
#   handle: FH_FILEINFO
#
#   The file handle for the file information file, &lt;FileInfo.nd&gt;.
#

#
#   handle: FH_CONFIGFILEINFO
#
#   The file handle for the config file information file, &lt;ConfigFileInfo.nd&gt;.
#

#
#   handle: FH_IMAGEFILE
#
#   The file handle for determining the dimensions of image files.
#



###############################################################################
# Group: Source File Variables


#
#   hash: supportedFiles
#
#   A hash of all the supported files in the input directory.  The keys are the &lt;FileNames&gt;, and the values are
#   &lt;NaturalDocs::Project::SourceFile&gt; objects.
#
my %supportedFiles;

#
#   hash: filesToParse
#
#   An existence hash of all the &lt;FileNames&gt; that need to be parsed.
#
my %filesToParse;

#
#   hash: filesToBuild
#
#   An existence hash of all the &lt;FileNames&gt; that need to be built.
#
my %filesToBuild;

#
#   hash: filesToPurge
#
#   An existence hash of the &lt;FileNames&gt; 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 &lt;FileNames&gt; that have Natural Docs content but are not part of &lt;filesToBuild&gt;.
#
my %unbuiltFilesWithContent;


# bool: reparseEverything
# Whether all the source files need to be reparsed.
my $reparseEverything;

# bool: rebuildEverything
# Whether all the source files need to be rebuilt.
my $rebuildEverything;

# hash: mostUsedLanguage
# The name of the most used language.  Doesn't include text files.
my $mostUsedLanguage;



###############################################################################
# Group: Configuration File Variables


#
#   hash: mainConfigFile
#
#   A hash mapping all the main configuration file names without paths to their &lt;FileStatus&gt;.  Prior to &lt;LoadConfigFileInfo()&gt;,
#   it serves as an existence hashref of the file names.
#
my %mainConfigFiles = ( 'Topics.txt' =&gt; 1, 'Languages.txt' =&gt; 1 );

#
#   hash: userConfigFiles
#
#   A hash mapping all the user configuration file names without paths to their &lt;FileStatus&gt;.  Prior to &lt;LoadConfigFileInfo()&gt;,
#   it serves as an existence hashref of the file names.
#
my %userConfigFiles = ( 'Topics.txt' =&gt; 1, 'Languages.txt' =&gt; 1, 'Menu.txt' =&gt; 1 );




###############################################################################
# Group: Image File Variables


#
#   hash: imageFileExtensions
#
#   An existence hash of all the file extensions for images.  Extensions are in all lowercase.
#
my %imageFileExtensions = ( 'jpg' =&gt; 1, 'jpeg' =&gt; 1, 'gif' =&gt; 1, 'png' =&gt; 1, 'bmp' =&gt; 1 );


#
#   hash: imageFiles
#
#   A hash of all the image files in the project.  The keys are the &lt;FileNames&gt; and the values are
#   &lt;NaturalDocs::Project::ImageFiles&gt;.
#
my %imageFiles;


#
#   hash: imageFilesToUpdate
#
#   An existence hash of all the image &lt;FileNames&gt; that need to be updated, either because they changed or they're new to the
#   project.
#
my %imageFilesToUpdate;


#
#   hash: imageFilesToPurge
#
#   An existence hash of all the image &lt;FileNames&gt; that need to be purged, either because the files no longer exist or because
#   they are no longer used.
#
my %imageFilesToPurge;


#
#   hash: insensitiveImageFiles
#
#   A hash that maps all lowercase image &lt;FileNames&gt; to their proper case as it would appear in &lt;imageFiles&gt;.  Used for
#   case insensitivity, obviously.
#
#   You can't just use all lowercase in &lt;imageFiles&gt; because both Linux and HTTP are case sensitive, so the original case must
#   be preserved.  We also want to allow separate entries for files that differ based only on case, so it goes to &lt;imageFiles&gt; first
#   where they can be distinguished and here only if there's no match.  Ties are broken by whichever is lower with cmp, because
#   it has to resolve consistently on all runs of the program.
#
my %insensitiveImageFiles;



###############################################################################
# Group: Files


#
#   File: FileInfo.nd
#
#   An index of the state of the files as of the last parse.  Used to determine if files were added, deleted, or changed.
#
#   Format:
#
#       The format is a text file.
#
#       &gt; [VersionInt: app version]
#
#       The beginning of the file is the &lt;VersionInt&gt; it was generated with.
#
#       &gt; [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
#
#       &gt; [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 &lt;VersionInt&gt;, which was previously the last modification time of &lt;Menu.txt&gt;, 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:
#
#       &gt; [BINARY_FORMAT]
#       &gt; [VersionInt: app version]
#
#       First is the standard &lt;BINARY_FORMAT&gt; &lt;VersionInt&gt; header.
#
#       &gt; [UInt32: last modification time of menu]
#       &gt; [UInt32: last modification of main topics file]
#       &gt; [UInt32: last modification of user topics file]
#       &gt; [UInt32: last modification of main languages file]
#       &gt; [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 &lt;Menu.txt&gt; was stored in &lt;FileInfo.nd&gt;, and
#             &lt;Topics.txt&gt; and &lt;Languages.txt&gt; didn't exist.
#


#
#   File: ImageFileInfo.nd
#
#   An index of the state of the image files as of the last parse.
#
#   Format:
#
#       &gt; [Standard Binary Header]
#
#       First is the standard binary file header as defined by &lt;NaturalDocs::BinaryFile&gt;.
#
#       &gt; [AString16: file name or undef]
#       &gt; [UInt32: last modification time]
#       &gt; [UInt8: was used]
#
#       This section is repeated until the file name is null.  The last modification times are UInt32s in the standard Unix format.
#
#
#   Revisions:
#
#       1.4:
#
#           - The file was added to Natural Docs.
#



###############################################################################
# Group: File Functions

#
#   Function: LoadSourceFileInfo
#
#   Loads the project file from disk and compares it against the files in the input directory.  Project is loaded from
#   &lt;FileInfo.nd&gt;.  New and changed files will be added to &lt;FilesToParse()&gt;, and if they have content,
#   &lt;FilesToBuild()&gt;.
#
#   Will call &lt;NaturalDocs::Languages-&gt;OnMostUsedLanguageKnown()&gt; if &lt;MostUsedLanguage()&gt; changes.
#
#   Returns:
#
#       Returns whether the project was changed in any way.
#
sub LoadSourceFileInfo
    {
    my ($self) = @_;

    $self-&gt;GetAllSupportedFiles();
    NaturalDocs::Languages-&gt;OnMostUsedLanguageKnown();

    my $fileIsOkay;
    my $version;
    my $hasChanged;

    if (open(FH_FILEINFO, '&lt;' . $self-&gt;DataFile('FileInfo.nd')))
        {
        # Check if the file is in the right format.
        $version = NaturalDocs::Version-&gt;FromTextFile(\*FH_FILEINFO);

        # The project file need to be rebuilt for 1.16.  The source files need to be reparsed and the output files rebuilt for 1.4.
        # We'll tolerate the difference between 1.16 and 1.3 in the loader.

        if (NaturalDocs::Version-&gt;CheckFileFormat( $version, NaturalDocs::Version-&gt;FromString('1.16') ))
            {
            $fileIsOkay = 1;

            if (!NaturalDocs::Version-&gt;CheckFileFormat( $version, NaturalDocs::Version-&gt;FromString('1.4') ))
                {
                $reparseEverything = 1;
                $rebuildEverything = 1;
                $hasChanged = 1;
                };
            }
        else
            {
            close(FH_FILEINFO);
            $hasChanged = 1;
            };
        };


    if ($fileIsOkay)
        {
        my %indexedFiles;


        my $line = &lt;FH_FILEINFO&gt;;
        ::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 &lt; NaturalDocs::Version-&gt;FromString('1.32') || lc($mostUsedLanguage) ne lc($line))
            {
            $reparseEverything = 1;
            NaturalDocs::SymbolTable-&gt;RebuildAllIndexes();
            };


        # Parse the rest of the file.

        while ($line = &lt;FH_FILEINFO&gt;)
            {
            ::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}-&gt;LastModified() != $modification)
                    {
                    $supportedFiles{$file}-&gt;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 &amp;&amp; $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}-&gt;SetStatus($status);
                    };

                $supportedFiles{$file}-&gt;SetHasContent($hasContent);
                $supportedFiles{$file}-&gt;SetDefaultMenuTitle($menuTitle);
                };
            };

        close(FH_FILEINFO);


        # Check for added files.

        if (scalar keys %supportedFiles &gt; scalar keys %indexedFiles)
            {
            foreach my $file (keys %supportedFiles)
                {
                if (!exists $indexedFiles{$file})
                    {
                    $supportedFiles{$file}-&gt;SetStatus(::FILE_NEW());
                    $supportedFiles{$file}-&gt;SetDefaultMenuTitle($file);
                    $supportedFiles{$file}-&gt;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}-&gt;SetStatus(::FILE_NEW());
            $supportedFiles{$file}-&gt;SetDefaultMenuTitle($file);
            $supportedFiles{$file}-&gt;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-&gt;RebuildEverything();  };


    return $hasChanged;
    };


#
#   Function: SaveSourceFileInfo
#
#   Saves the source file info to disk.  Everything is saved in &lt;FileInfo.nd&gt;.
#
sub SaveSourceFileInfo
    {
    my ($self) = @_;

    open(FH_FILEINFO, '&gt;' . $self-&gt;DataFile('FileInfo.nd'))
        or die "Couldn't save project file " . $self-&gt;DataFile('FileInfo.nd') . "\n";

    NaturalDocs::Version-&gt;ToTextFile(\*FH_FILEINFO, NaturalDocs::Settings-&gt;AppVersion());

    print FH_FILEINFO $mostUsedLanguage . "\n";

    while (my ($fileName, $file) = each %supportedFiles)
        {
        print FH_FILEINFO $fileName . "\t"
                              . $file-&gt;LastModified() . "\t"
                              . ($file-&gt;HasContent() || '0') . "\t"
                              . $file-&gt;DefaultMenuTitle() . "\n";
        };

    close(FH_FILEINFO);
    };


#
#   Function: LoadConfigFileInfo
#
#   Loads the config file info from disk.
#
sub LoadConfigFileInfo
    {
    my ($self) = @_;

    my $fileIsOkay;
    my $version;
    my $fileName = NaturalDocs::Project-&gt;DataFile('ConfigFileInfo.nd');

    if (open(FH_CONFIGFILEINFO, '&lt;' . $fileName))
        {
        # See if it's binary.
        binmode(FH_CONFIGFILEINFO);

        my $firstChar;
        read(FH_CONFIGFILEINFO, $firstChar, 1);

        if ($firstChar == ::BINARY_FORMAT())
            {
            $version = NaturalDocs::Version-&gt;FromBinaryFile(\*FH_CONFIGFILEINFO);

            # It hasn't changed since being introduced.

            if (NaturalDocs::Version-&gt;CheckFileFormat($version))
                {  $fileIsOkay = 1;  }
            else
                {  close(FH_CONFIGFILEINFO);  };
            }

        else # it's not in binary
            {  close(FH_CONFIGFILEINFO);  };
        };

    my @configFiles = ( $self-&gt;UserConfigFile('Menu.txt'), \$userConfigFiles{'Menu.txt'},
                                 $self-&gt;MainConfigFile('Topics.txt'), \$mainConfigFiles{'Topics.txt'},
                                 $self-&gt;UserConfigFile('Topics.txt'), \$userConfigFiles{'Topics.txt'},
                                 $self-&gt;MainConfigFile('Languages.txt'), \$mainConfigFiles{'Languages.txt'},
                                 $self-&gt;UserConfigFile('Languages.txt'), \$userConfigFiles{'Languages.txt'} );

    if ($fileIsOkay)
        {
        my $raw;

        read(FH_CONFIGFILEINFO, $raw, 20);
        my @configFileDates = unpack('NNNNN', $raw);

        while (scalar @configFiles)
            {
            my $file = shift @configFiles;
            my $fileStatus = shift @configFiles;
            my $fileDate = shift @configFileDates;

            if (-e $file)
                {
                if ($fileDate == (stat($file))[9])
                    {  $$fileStatus = ::FILE_SAME();  }
                else
                    {  $$fileStatus = ::FILE_CHANGED();  };
                }
            else
                {  $$fileStatus = ::FILE_DOESNTEXIST();  };
            };

        close(FH_CONFIGFILEINFO);
        }
    else # !$fileIsOkay
        {
        while (scalar @configFiles)
            {
            my $file = shift @configFiles;
            my $fileStatus = shift @configFiles;

            if (-e $file)
                {  $$fileStatus = ::FILE_CHANGED();  }
            else
                {  $$fileStatus = ::FILE_DOESNTEXIST();  };
            };
        };

    if ($userConfigFiles{'Menu.txt'} == ::FILE_SAME() &amp;&amp; $rebuildEverything)
        {  $userConfigFiles{'Menu.txt'} = ::FILE_CHANGED();  };
    };


#
#   Function: SaveConfigFileInfo
#
#   Saves the config file info to disk.  You *must* save all other config files first, such as &lt;Menu.txt&gt; and &lt;Topics.txt&gt;.
#
sub SaveConfigFileInfo
    {
    my ($self) = @_;

    open (FH_CONFIGFILEINFO, '&gt;' . NaturalDocs::Project-&gt;DataFile('ConfigFileInfo.nd'))
        or die "Couldn't save " . NaturalDocs::Project-&gt;DataFile('ConfigFileInfo.nd') . ".\n";

    binmode(FH_CONFIGFILEINFO);

    print FH_CONFIGFILEINFO '' . ::BINARY_FORMAT();

    NaturalDocs::Version-&gt;ToBinaryFile(\*FH_CONFIGFILEINFO, NaturalDocs::Settings-&gt;AppVersion());

    print FH_CONFIGFILEINFO pack('NNNNN', (stat($self-&gt;UserConfigFile('Menu.txt')))[9],
                                                                (stat($self-&gt;MainConfigFile('Topics.txt')))[9],
                                                                (stat($self-&gt;UserConfigFile('Topics.txt')))[9],
                                                                (stat($self-&gt;MainConfigFile('Languages.txt')))[9],
                                                                (stat($self-&gt;UserConfigFile('Languages.txt')))[9] );

    close(FH_CONFIGFILEINFO);
    };


#
#   Function: LoadImageFileInfo
#
#   Loads the image file info from disk.
#
sub LoadImageFileInfo
    {
    my ($self) = @_;

    my $version = NaturalDocs::BinaryFile-&gt;OpenForReading( NaturalDocs::Project-&gt;DataFile('ImageFileInfo.nd') );
    my $fileIsOkay;

    if (defined $version)
        {
        # It hasn't changed since being introduced.

        if (NaturalDocs::Version-&gt;CheckFileFormat($version))
            {  $fileIsOkay = 1;  }
        else
            {  NaturalDocs::BinaryFile-&gt;Close();  };
        };

    if ($fileIsOkay)
        {
        # [AString16: file name or undef]

        while (my $imageFile = NaturalDocs::BinaryFile-&gt;GetAString16())
            {
            # [UInt32: last modified]
            # [UInt8: was used]

            my $lastModified = NaturalDocs::BinaryFile-&gt;GetUInt32();
            my $wasUsed = NaturalDocs::BinaryFile-&gt;GetUInt8();

            my $imageFileObject = $imageFiles{$imageFile};

            # If there's an image file in ImageFileInfo.nd that no longer exists...
            if (!$imageFileObject)
                {
                $imageFileObject = NaturalDocs::Project::ImageFile-&gt;New($lastModified, ::FILE_DOESNTEXIST(), $wasUsed);
                $imageFiles{$imageFile} = $imageFileObject;

                if ($wasUsed)
                    {  $imageFilesToPurge{$imageFile} = 1;  };
                }
            else
                {
                $imageFileObject-&gt;SetWasUsed($wasUsed);

                # This will be removed if it gets any references.
                if ($wasUsed)
                    {  $imageFilesToPurge{$imageFile} = 1;  };

                if ($imageFileObject-&gt;LastModified() == $lastModified &amp;&amp; !$rebuildEverything)
                    {  $imageFileObject-&gt;SetStatus(::FILE_SAME());  }
                else
                    {  $imageFileObject-&gt;SetStatus(::FILE_CHANGED());  };
                };
            };

        NaturalDocs::BinaryFile-&gt;Close();
        }

    else # !$fileIsOkay
        {
        $self-&gt;RebuildEverything();
        };
    };


#
#   Function: SaveImageFileInfo
#
#   Saves the image file info to disk.
#
sub SaveImageFileInfo
    {
    my $self = shift;

    NaturalDocs::BinaryFile-&gt;OpenForWriting( NaturalDocs::Project-&gt;DataFile('ImageFileInfo.nd') );

    while (my ($imageFile, $imageFileInfo) = each %imageFiles)
        {
        if ($imageFileInfo-&gt;Status() != ::FILE_DOESNTEXIST())
            {
            # [AString16: file name or undef]
            # [UInt32: last modification time]
            # [UInt8: was used]

            NaturalDocs::BinaryFile-&gt;WriteAString16($imageFile);
            NaturalDocs::BinaryFile-&gt;WriteUInt32($imageFileInfo-&gt;LastModified());
            NaturalDocs::BinaryFile-&gt;WriteUInt8( ($imageFileInfo-&gt;ReferenceCount() &gt; 0 ? 1 : 0) );
            };
        };

    NaturalDocs::BinaryFile-&gt;WriteAString16(undef);
    NaturalDocs::BinaryFile-&gt;Close();
    };


#
#   Function: MigrateOldFiles
#
#   If the project uses the old file names used prior to 1.14, it converts them to the new file names.
#
sub MigrateOldFiles
    {
    my ($self) = @_;

    my $projectDirectory = NaturalDocs::Settings-&gt;ProjectDirectory();

    # We use the menu file as a test to see if we're using the new format.
    if (-e NaturalDocs::File-&gt;JoinPaths($projectDirectory, 'NaturalDocs_Menu.txt'))
        {
        # The Data subdirectory would have been created by NaturalDocs::Settings.

        rename( NaturalDocs::File-&gt;JoinPaths($projectDirectory, 'NaturalDocs_Menu.txt'), $self-&gt;UserConfigFile('Menu.txt') );

        if (-e NaturalDocs::File-&gt;JoinPaths($projectDirectory, 'NaturalDocs.sym'))
            {  rename( NaturalDocs::File-&gt;JoinPaths($projectDirectory, 'NaturalDocs.sym'), $self-&gt;DataFile('SymbolTable.nd') );  };

        if (-e NaturalDocs::File-&gt;JoinPaths($projectDirectory, 'NaturalDocs.files'))
            {  rename( NaturalDocs::File-&gt;JoinPaths($projectDirectory, 'NaturalDocs.files'), $self-&gt;DataFile('FileInfo.nd') );  };

        if (-e NaturalDocs::File-&gt;JoinPaths($projectDirectory, 'NaturalDocs.m'))
            {  rename( NaturalDocs::File-&gt;JoinPaths($projectDirectory, 'NaturalDocs.m'), $self-&gt;DataFile('PreviousMenuState.nd') );  };
        };
    };



###############################################################################
# Group: Config and Data File Functions


#
#   Function: MainConfigFile
#
#   Returns the full path to the passed main configuration file.  Pass the file name only.
#
sub MainConfigFile #(string file)
    {
    my ($self, $file) = @_;
    return NaturalDocs::File-&gt;JoinPaths( NaturalDocs::Settings-&gt;ConfigDirectory(), $file );
    };

#
#   Function: MainConfigFileStatus
#
#   Returns the &lt;FileStatus&gt; of the passed main configuration file.  Pass the file name only.
#
sub MainConfigFileStatus #(string file)
    {
    my ($self, $file) = @_;
    return $mainConfigFiles{$file};
    };

#
#   Function: UserConfigFile
#
#   Returns the full path to the passed user configuration file.  Pass the file name only.
#
sub UserConfigFile #(string file)
    {
    my ($self, $file) = @_;
    return NaturalDocs::File-&gt;JoinPaths( NaturalDocs::Settings-&gt;ProjectDirectory(), $file );
    };

#
#   Function: UserConfigFileStatus
#
#   Returns the &lt;FileStatus&gt; of the passed user configuration file.  Pass the file name only.
#
sub UserConfigFileStatus #(string file)
    {
    my ($self, $file) = @_;
    return $userConfigFiles{$file};
    };

#
#   Function: DataFile
#
#   Returns the full path to the passed data file.  Pass the file name only.
#
sub DataFile #(string file)
    {
    my ($self, $file) = @_;
    return NaturalDocs::File-&gt;JoinPaths( NaturalDocs::Settings-&gt;ProjectDataDirectory(), $file );
    };




###############################################################################
# Group: Source File Functions


# Function: FilesToParse
# Returns an existence hashref of the &lt;FileNames&gt; 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 &lt;FileNames&gt; 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 &lt;FileNames&gt; 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 &lt;FilesToPurge()&gt;.  If this gets called on a file and that file later gets Natural Docs content, it will be added.
#
#   Parameters:
#
#       file - The &lt;FileName&gt; 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} &amp;&amp; !exists $filesToPurge{$file} &amp;&amp; $supportedFiles{$file}-&gt;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-&gt;RebuildAllIndexes();

    if ($userConfigFiles{'Menu.txt'} == ::FILE_SAME())
        {  $userConfigFiles{'Menu.txt'} = ::FILE_CHANGED();  };

    while (my ($imageFile, $imageObject) = each %imageFiles)
        {
        if ($imageObject-&gt;ReferenceCount())
            {  $imageFilesToUpdate{$imageFile} = 1;  };
        };
    };


# Function: UnbuiltFilesWithContent
# Returns an existence hashref of the &lt;FileNames&gt; that have Natural Docs content but are not part of &lt;FilesToBuild()&gt;.  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 &lt;FileNames&gt; 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 &lt;FileName&gt; contains Natural Docs content.
#
sub HasContent #(file)
    {
    my ($self, $file) = @_;

    if (exists $supportedFiles{$file})
        {  return $supportedFiles{$file}-&gt;HasContent();  }
    else
        {  return undef;  };
    };


#
#   Function: SetHasContent
#
#   Sets whether the &lt;FileName&gt; has Natural Docs content or not.
#
sub SetHasContent #(file, hasContent)
    {
    my ($self, $file, $hasContent) = @_;

    if (exists $supportedFiles{$file} &amp;&amp; $supportedFiles{$file}-&gt;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}-&gt;SetHasContent($hasContent);
        };
    };


#
#   Function: StatusOf
#
#   Returns the &lt;FileStatus&gt; of the passed &lt;FileName&gt;.
#
sub StatusOf #(file)
    {
    my ($self, $file) = @_;

    if (exists $supportedFiles{$file})
        {  return $supportedFiles{$file}-&gt;Status();  }
    else
        {  return ::FILE_DOESNTEXIST();  };
    };


#
#   Function: DefaultMenuTitleOf
#
#   Returns the default menu title of the &lt;FileName&gt;.  If one isn't specified, it returns the &lt;FileName&gt;.
#
sub DefaultMenuTitleOf #(file)
    {
    my ($self, $file) = @_;

    if (exists $supportedFiles{$file})
        {  return $supportedFiles{$file}-&gt;DefaultMenuTitle();  }
    else
        {  return $file;  };
    };


#
#   Function: SetDefaultMenuTitle
#
#   Sets the &lt;FileName's&gt; default menu title.
#
sub SetDefaultMenuTitle #(file, menuTitle)
    {
    my ($self, $file, $menuTitle) = @_;

    if (exists $supportedFiles{$file} &amp;&amp; $supportedFiles{$file}-&gt;DefaultMenuTitle() ne $menuTitle)
        {
        $supportedFiles{$file}-&gt;SetDefaultMenuTitle($menuTitle);
        NaturalDocs::Menu-&gt;OnDefaultTitleChange($file);
        };
    };


#
#   Function: MostUsedLanguage
#
#   Returns the name of the most used language in the source trees.  Does not include text files.
#
sub MostUsedLanguage
    {  return $mostUsedLanguage;  };




###############################################################################
# Group: Image File Functions


#
#   Function: ImageFileExists
#   Returns whether the passed image file exists.
#
sub ImageFileExists #(FileName file) =&gt; bool
    {
    my ($self, $file) = @_;

    if (!exists $imageFiles{$file})
        {  $file = $insensitiveImageFiles{lc($file)};  };

    return (exists $imageFiles{$file} &amp;&amp; $imageFiles{$file}-&gt;Status() != ::FILE_DOESNTEXIST());
    };


#
#   Function: ImageFileDimensions
#   Returns the dimensions of the passed image file as the array ( width, height ).  Returns them both as undef if it cannot be
#   determined.
#
sub ImageFileDimensions #(FileName file) =&gt; (int, int)
    {
    my ($self, $file) = @_;

    if (!exists $imageFiles{$file})
        {  $file = $insensitiveImageFiles{lc($file)};  };

    my $object = $imageFiles{$file};
    if (!$object)
        {  die "Tried to get the dimensions of an image that doesn't exist.";  };

    if ($object-&gt;Width() == -1)
        {  $self-&gt;DetermineImageDimensions($file);  };

    return ($object-&gt;Width(), $object-&gt;Height());
    };


#
#   Function: ImageFileCapitalization
#   Returns the properly capitalized version of the passed image &lt;FileName&gt;.  Image file paths are treated as case insensitive
#   regardless of whether the underlying operating system is or not, so we have to make sure the final version matches the
#   capitalization of the actual file.
#
sub ImageFileCapitalization #(FileName file) =&gt; FileName
    {
    my ($self, $file) = @_;

    if (exists $imageFiles{$file})
        {  return $file;  }
    elsif (exists $insensitiveImageFiles{lc($file)})
        {  return $insensitiveImageFiles{lc($file)};  }
    else
        {  die "Tried to get the capitalization of an image file that doesn't exist.";  };
    };


#
#   Function: AddImageFileReference
#   Adds a reference to the passed image &lt;FileName&gt;.
#
sub AddImageFileReference #(FileName imageFile)
    {
    my ($self, $imageFile) = @_;

    if (!exists $imageFiles{$imageFile})
        {  $imageFile = $insensitiveImageFiles{lc($imageFile)};  };

    my $imageFileInfo = $imageFiles{$imageFile};

    if ($imageFileInfo == undef || $imageFileInfo-&gt;Status() == ::FILE_DOESNTEXIST())
        {  die "Tried to add a reference to a non-existant image file.";  };

    if ($imageFileInfo-&gt;AddReference() == 1)
        {
        delete $imageFilesToPurge{$imageFile};

        if (!$imageFileInfo-&gt;WasUsed() ||
            $imageFileInfo-&gt;Status() == ::FILE_NEW() ||
            $imageFileInfo-&gt;Status() == ::FILE_CHANGED())
            {  $imageFilesToUpdate{$imageFile} = 1;  };
        };
    };


#
#   Function: DeleteImageFileReference
#   Deletes a reference from the passed image &lt;FileName&gt;.
#
sub DeleteImageFileReference #(FileName imageFile)
    {
    my ($self, $imageFile) = @_;

    if (!exists $imageFiles{$imageFile})
        {  $imageFile = $insensitiveImageFiles{lc($imageFile)};  };

    if (!exists $imageFiles{$imageFile})
        {  die "Tried to delete a reference to a non-existant image file.";  };

    if ($imageFiles{$imageFile}-&gt;DeleteReference() == 0)
        {
        delete $imageFilesToUpdate{$imageFile};

        if ($imageFiles{$imageFile}-&gt;WasUsed())
            {  $imageFilesToPurge{$imageFile} = 1;  };
        };
    };


#
#   Function: ImageFilesToUpdate
#   Returns an existence hashref of image &lt;FileNames&gt; that need to be updated.  *Do not change.*
#
sub ImageFilesToUpdate
    {  return \%imageFilesToUpdate;  };


#
#   Function: ImageFilesToPurge
#   Returns an existence hashref of image &lt;FileNames&gt; that need to be updated.  *Do not change.*
#
sub ImageFilesToPurge
    {  return \%imageFilesToPurge;  };



###############################################################################
# Group: Support Functions

#
#   Function: GetAllSupportedFiles
#
#   Gets all the supported files in the passed directory and its subdirectories and puts them into &lt;supportedFiles&gt;.  The only
#   attribute that will be set is &lt;NaturalDocs::Project::SourceFile-&gt;LastModified()&gt;.  Also sets &lt;mostUsedLanguage&gt;.
#
sub GetAllSupportedFiles
    {
    my ($self) = @_;

    my @directories = @{NaturalDocs::Settings-&gt;InputDirectories()};
    my $isCaseSensitive = NaturalDocs::File-&gt;IsCaseSensitive();

    # Keys are language names, values are counts.
    my %languageCounts;


    # Make an existence hash of excluded directories.

    my %excludedDirectories;
    my $excludedDirectoryArrayRef = NaturalDocs::Settings-&gt;ExcludedInputDirectories();

    foreach my $excludedDirectory (@$excludedDirectoryArrayRef)
        {
        if ($isCaseSensitive)
            {  $excludedDirectories{$excludedDirectory} = 1;  }
        else
            {  $excludedDirectories{lc($excludedDirectory)} = 1;  };
        };


    my $imagesOnly;
    my $language;

    while (scalar @directories)
        {
        my $directory = pop @directories;

        opendir DIRECTORYHANDLE, $directory;
        my @entries = readdir DIRECTORYHANDLE;
        closedir DIRECTORYHANDLE;

        @entries = NaturalDocs::File-&gt;NoUpwards(@entries);

        foreach my $entry (@entries)
            {
            my $fullEntry = NaturalDocs::File-&gt;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-&gt;JoinPaths($directory, $entry, 1);

                if ($isCaseSensitive)
                    {
                    if (!exists $excludedDirectories{$fullEntry})
                        {  push @directories, $fullEntry;  };
                    }
                else
                    {
                    if (!exists $excludedDirectories{lc($fullEntry)})
                        {  push @directories, $fullEntry;  };
                    };
                }

            # Otherwise add it if it's a supported extension.
            else
                {
                my $extension = NaturalDocs::File-&gt;ExtensionOf($entry);

                if (exists $imageFileExtensions{lc($extension)})
                    {
                    my $fileObject = NaturalDocs::Project::ImageFile-&gt;New( (stat($fullEntry))[9], ::FILE_NEW(), 0 );
                    $imageFiles{$fullEntry} = $fileObject;

                    my $lcFullEntry = lc($fullEntry);

                    if (!exists $insensitiveImageFiles{$lcFullEntry} ||
                        ($fullEntry cmp $insensitiveImageFiles{$lcFullEntry}) &lt; 0)
                        {
                        $insensitiveImageFiles{$lcFullEntry} = $fullEntry;
                        };
                    }
                elsif (!$imagesOnly &amp;&amp; ($language = NaturalDocs::Languages-&gt;LanguageOf($fullEntry)) )
                    {
                    my $fileObject = NaturalDocs::Project::SourceFile-&gt;New();
                    $fileObject-&gt;SetLastModified(( stat($fullEntry))[9] );
                    $supportedFiles{$fullEntry} = $fileObject;

                    $languageCounts{$language-&gt;Name()}++;
                    };
                };
            };


        # After we run out of source directories, add the image directories.

        if (scalar @directories == 0 &amp;&amp; !$imagesOnly)
            {
            $imagesOnly = 1;
            @directories = @{NaturalDocs::Settings-&gt;ImageDirectories()};
            };
        };


    my $topCount = 0;

    while (my ($language, $count) = each %languageCounts)
        {
        if ($count &gt; $topCount &amp;&amp; $language ne 'Text File')
            {
            $topCount = $count;
            $mostUsedLanguage = $language;
            };
        };
    };


#
#   Function: DetermineImageDimensions
#
#   Attempts to determine the dimensions of the passed image and apply them to their object in &lt;imageFiles&gt;.  Will set them to
#   undef if they can't be determined.
#
sub DetermineImageDimensions #(FileName imageFile)
    {
    my ($self, $imageFile) = @_;

    my $imageFileObject = $imageFiles{$imageFile};
    if (!defined $imageFileObject)
        {  die "Tried to determine image dimensions of a file with no object.";  };

    my $extension = lc( NaturalDocs::File-&gt;ExtensionOf($imageFile) );
    my ($width, $height);

    if ($imageFileExtensions{$extension})
        {
        open(FH_IMAGEFILE, '&lt;' . $imageFile)
            or die 'Could not open ' . $imageFile . "\n";
        binmode(FH_IMAGEFILE);

        my $raw;

        if ($extension eq 'gif')
            {
            read(FH_IMAGEFILE, $raw, 6);

            if ($raw eq 'GIF87a' || $raw eq 'GIF89a')
                {
                read(FH_IMAGEFILE, $raw, 4);
                ($width, $height) = unpack('vv', $raw);
                };
            }

        elsif ($extension eq 'png')
            {
            read(FH_IMAGEFILE, $raw, 8);

            if ($raw eq "\x89PNG\x0D\x0A\x1A\x0A")
                {
                seek(FH_IMAGEFILE, 4, 1);
                read(FH_IMAGEFILE, $raw, 4);

                if ($raw eq 'IHDR')
                    {
                    read(FH_IMAGEFILE, $raw, 8);
                    ($width, $height) = unpack('NN', $raw);
                    };
                };
            }

        elsif ($extension eq 'bmp')
            {
            read(FH_IMAGEFILE, $raw, 2);

            if ($raw eq 'BM')
                {
                seek(FH_IMAGEFILE, 16, 1);
                read(FH_IMAGEFILE, $raw, 8);

                ($width, $height) = unpack('VV', $raw);
                };
            }

        elsif ($extension eq 'jpg' || $extension eq 'jpeg')
            {
            read(FH_IMAGEFILE, $raw, 2);
            my $isOkay = ($raw eq "\xFF\xD8");

            while ($isOkay)
                {
                read(FH_IMAGEFILE, $raw, 4);
                my ($marker, $code, $length) = unpack('CCn', $raw);

                $isOkay = ($marker eq 0xFF);

                if ($isOkay)
                    {
                    if ($code &gt;= 0xC0 &amp;&amp; $code &lt;= 0xC3)
                        {
                        read(FH_IMAGEFILE, $raw, 5);
                        ($height, $width) = unpack('xnn', $raw);
                        last;
                        }

                    else
                        {
                        $isOkay = seek(FH_IMAGEFILE, $length - 2, 1);
                        };
                    };
                };
            };

        close(FH_IMAGEFILE);
        };


    # Sanity check the values.  Although images can theoretically be bigger than 5000, most won't.  The worst that happens in this
    # case is just that they don't get length and width values in the output anyway.
    if ($width &gt; 0 &amp;&amp; $width &lt; 5000 &amp;&amp; $height &gt; 0 &amp;&amp; $height &lt; 5000)
        {  $imageFileObject-&gt;SetDimensions($width, $height);  }
    else
        {  $imageFileObject-&gt;SetDimensions(undef, undef);  };
    };


1;
</pre></body></html>