diff options
Diffstat (limited to 'docs/tool/Modules/NaturalDocs')
63 files changed, 35293 insertions, 0 deletions
diff --git a/docs/tool/Modules/NaturalDocs/BinaryFile.pm b/docs/tool/Modules/NaturalDocs/BinaryFile.pm new file mode 100644 index 00000000..5c1adb8d --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/BinaryFile.pm @@ -0,0 +1,294 @@ +############################################################################### +# +# Package: NaturalDocs::BinaryFile +# +############################################################################### +# +# A package to manage Natural Docs' binary data files. +# +# Usage: +# +# - Only one data file can be managed with this package at a time. You must close the file before opening another +# one. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::BinaryFile; + +use vars qw(@EXPORT @ISA); +require Exporter; +@ISA = qw(Exporter); + +@EXPORT = ('BINARY_FORMAT'); + + +############################################################################### +# Group: Format + +# +# Topic: Standard Header +# +# > [UInt8: BINARY_FORMAT] +# > [VersionInt: app version] +# +# The first byte is <BINARY_FORMAT>, which distinguishes binary configuration files from text ones, since Natural Docs +# used to use text data files with the same name. +# +# The next section is the version of Natural Docs that wrote the file, as defined by <NaturalDocs::Settings->AppVersion> +# and written by <NaturalDocs::Version->ToBinaryFile()>. +# + +# +# Topic: Data Types +# +# All the integer data types are written most significant byte first, aka big endian. +# +# An AString16 is a UInt16 followed by that many 8-bit ASCII characters. It doesn't include a null character at the end. Undef +# strings are represented by a zero for the UInt16 and nothing following it. +# + +# +# Constant: BINARY_FORMAT +# +# An 8-bit constant that's used as the first byte of binary data files. This is used so that you can easily distinguish between +# binary and old-style text data files. It's not a character that would appear in plain text files. +# +use constant BINARY_FORMAT => pack('C', 0x06); +# Which is ACK or acknowledge in ASCII. Is the cool spade character in DOS displays. + + +############################################################################### +# Group: Variables + +# +# handle: FH_BINARYDATAFILE +# +# The file handle used for the data file. +# + + +# +# string: currentFile +# +# The <FileName> for the current configuration file being parsed. +# +my $currentFile; + + + +############################################################################### +# Group: File Functions + + +# +# Function: OpenForReading +# +# Opens a binary file for reading. +# +# Parameters: +# +# minimumVersion - The minimum version of the file format that is acceptible. May be undef. +# +# Returns: +# +# The format <VersionInt> or undef if it failed. It could fail for any of the following reasons. +# +# - The file doesn't exist. +# - The file couldn't be opened. +# - The file didn't have the proper header. +# - Either the application or the file was from a development release, and they're not the exact same development release. +# - The file's format was less than the minimum version, if one was defined. +# - The file was from a later application version than the current. +# +sub OpenForReading #(FileName file, optional VersionInt minimumVersion) => VersionInt + { + my ($self, $file, $minimumVersion) = @_; + + if (defined $currentFile) + { die "Tried to open binary file " . $file . " for reading when " . $currentFile . " was already open."; }; + + $currentFile = $file; + + if (open(FH_BINARYDATAFILE, '<' . $currentFile)) + { + # See if it's binary. + binmode(FH_BINARYDATAFILE); + + my $firstChar; + read(FH_BINARYDATAFILE, $firstChar, 1); + + if ($firstChar == ::BINARY_FORMAT()) + { + my $version = NaturalDocs::Version->FromBinaryFile(\*FH_BINARYDATAFILE); + + if (NaturalDocs::Version->CheckFileFormat($version, $minimumVersion)) + { return $version; }; + }; + + close(FH_BINARYDATAFILE); + }; + + $currentFile = undef; + return undef; + }; + + +# +# Function: OpenForWriting +# +# Opens a binary file for writing and writes the standard header. Dies if the file cannot be opened. +# +sub OpenForWriting #(FileName file) + { + my ($self, $file) = @_; + + if (defined $currentFile) + { die "Tried to open binary file " . $file . " for writing when " . $currentFile . " was already open."; }; + + $currentFile = $file; + + open (FH_BINARYDATAFILE, '>' . $currentFile) + or die "Couldn't save " . $file . ".\n"; + + binmode(FH_BINARYDATAFILE); + + print FH_BINARYDATAFILE '' . ::BINARY_FORMAT(); + NaturalDocs::Version->ToBinaryFile(\*FH_BINARYDATAFILE, NaturalDocs::Settings->AppVersion()); + }; + + +# +# Function: Close +# +# Closes the current configuration file. +# +sub Close + { + my $self = shift; + + if (!$currentFile) + { die "Tried to close a binary file when one wasn't open."; }; + + close(FH_BINARYDATAFILE); + $currentFile = undef; + }; + + + +############################################################################### +# Group: Reading Functions + + +# +# Function: GetUInt8 +# Reads and returns a UInt8 from the open file. +# +sub GetUInt8 # => UInt8 + { + my $raw; + read(FH_BINARYDATAFILE, $raw, 1); + + return unpack('C', $raw); + }; + +# +# Function: GetUInt16 +# Reads and returns a UInt16 from the open file. +# +sub GetUInt16 # => UInt16 + { + my $raw; + read(FH_BINARYDATAFILE, $raw, 2); + + return unpack('n', $raw); + }; + +# +# Function: GetUInt32 +# Reads and returns a UInt32 from the open file. +# +sub GetUInt32 # => UInt32 + { + my $raw; + read(FH_BINARYDATAFILE, $raw, 4); + + return unpack('N', $raw); + }; + +# +# Function: GetAString16 +# Reads and returns an AString16 from the open file. Supports undef strings. +# +sub GetAString16 # => string + { + my $rawLength; + read(FH_BINARYDATAFILE, $rawLength, 2); + my $length = unpack('n', $rawLength); + + if (!$length) + { return undef; }; + + my $string; + read(FH_BINARYDATAFILE, $string, $length); + + return $string; + }; + + + +############################################################################### +# Group: Writing Functions + + +# +# Function: WriteUInt8 +# Writes a UInt8 to the open file. +# +sub WriteUInt8 #(UInt8 value) + { + my ($self, $value) = @_; + print FH_BINARYDATAFILE pack('C', $value); + }; + +# +# Function: WriteUInt16 +# Writes a UInt32 to the open file. +# +sub WriteUInt16 #(UInt16 value) + { + my ($self, $value) = @_; + print FH_BINARYDATAFILE pack('n', $value); + }; + +# +# Function: WriteUInt32 +# Writes a UInt32 to the open file. +# +sub WriteUInt32 #(UInt32 value) + { + my ($self, $value) = @_; + print FH_BINARYDATAFILE pack('N', $value); + }; + +# +# Function: WriteAString16 +# Writes an AString16 to the open file. Supports undef strings. +# +sub WriteAString16 #(string value) + { + my ($self, $string) = @_; + + if (length($string)) + { print FH_BINARYDATAFILE pack('nA*', length($string), $string); } + else + { print FH_BINARYDATAFILE pack('n', 0); }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Builder.pm b/docs/tool/Modules/NaturalDocs/Builder.pm new file mode 100644 index 00000000..cdcdff72 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Builder.pm @@ -0,0 +1,280 @@ +############################################################################### +# +# Package: NaturalDocs::Builder +# +############################################################################### +# +# A package that takes parsed source file and builds the output for it. +# +# Usage and Dependencies: +# +# - <Add()> can be called immediately. +# - <OutputPackages()> and <OutputPackageOf()> can be called once all sub-packages have been registered via <Add()>. +# Since this is normally done in their INIT functions, they should be available to all normal functions immediately. +# +# - Prior to calling <Run()>, <NaturalDocs::Settings>, <NaturalDocs::Project>, <NaturalDocs::Menu>, and +# <NaturalDocs::Parser> must be initialized. <NaturalDocs::Settings->GenerateDirectoryNames()> must be called. +# <NaturalDocs::SymbolTable> and <NaturalDocs::ClassHierarchy> must be initialized and fully resolved. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + + +use strict; +use integer; + +use NaturalDocs::Builder::Base; +use NaturalDocs::Builder::HTML; +use NaturalDocs::Builder::FramedHTML; + +package NaturalDocs::Builder; + + +############################################################################### +# Group: Variables + +# +# Array: outputPackages +# +# An array of the output packages available for use. +# +my @outputPackages; + + +############################################################################### +# Group: Functions + + +# +# Function: OutputPackages +# +# Returns an arrayref of the output packages available for use. The arrayref is not a copy of the data, so don't change it. +# +# Add output packages to this list with the <Add()> function. +# +sub OutputPackages + { return \@outputPackages; }; + + +# +# Function: OutputPackageOf +# +# Returns the output package corresponding to the passed command line option, or undef if none. +# +sub OutputPackageOf #(commandLineOption) + { + my ($self, $commandLineOption) = @_; + + $commandLineOption = lc($commandLineOption); + + foreach my $package (@outputPackages) + { + if (lc($package->CommandLineOption()) eq $commandLineOption) + { return $package; }; + }; + + return undef; + }; + + + +# +# Function: Add +# +# Adds an output package to those available for use. All output packages must call this function in order to be recognized. +# +# Parameters: +# +# package - The package name. +# +sub Add #(package) + { + my ($self, $package) = @_; + + # Output packages shouldn't register themselves more than once, so we don't need to check for it. + push @outputPackages, $package; + }; + + +# +# Function: Run +# +# Runs the build process. This must be called *every time* Natural Docs is run, regardless of whether any source files changed +# or not. Some output packages have dependencies on files outside of the source tree that need to be checked. +# +# Since there are multiple stages to the build process, this function will handle its own status messages. There's no need to print +# "Building files..." or something similar beforehand. +# +sub Run + { + my ($self) = @_; + + + # Determine what we're doing. + + my $buildTargets = NaturalDocs::Settings->BuildTargets(); + + my $filesToBuild = NaturalDocs::Project->FilesToBuild(); + my $numberOfFilesToBuild = (scalar keys %$filesToBuild) * (scalar @$buildTargets); + + my $filesToPurge = NaturalDocs::Project->FilesToPurge(); + my $numberOfFilesToPurge = (scalar keys %$filesToPurge) * (scalar @$buildTargets); + + my $imagesToUpdate = NaturalDocs::Project->ImageFilesToUpdate(); + my $numberOfImagesToUpdate = (scalar keys %$imagesToUpdate) * (scalar @$buildTargets); + + my $imagesToPurge = NaturalDocs::Project->ImageFilesToPurge(); + my $numberOfImagesToPurge = (scalar keys %$imagesToPurge) * (scalar @$buildTargets); + + my %indexesToBuild; + my %indexesToPurge; + + my $currentIndexes = NaturalDocs::Menu->Indexes(); + my $previousIndexes = NaturalDocs::Menu->PreviousIndexes(); + + foreach my $index (keys %$currentIndexes) + { + if (NaturalDocs::SymbolTable->IndexChanged($index) || !exists $previousIndexes->{$index}) + { + $indexesToBuild{$index} = 1; + }; + }; + + # All indexes that still exist should have been deleted. + foreach my $index (keys %$previousIndexes) + { + if (!exists $currentIndexes->{$index}) + { + $indexesToPurge{$index} = 1; + }; + }; + + my $numberOfIndexesToBuild = (scalar keys %indexesToBuild) * (scalar @$buildTargets); + my $numberOfIndexesToPurge = (scalar keys %indexesToPurge) * (scalar @$buildTargets); + + + # Start the build process + + foreach my $buildTarget (@$buildTargets) + { + $buildTarget->Builder()->BeginBuild( $numberOfFilesToBuild || $numberOfFilesToPurge || + $numberOfImagesToUpdate || $numberOfImagesToPurge || + $numberOfIndexesToBuild || $numberOfIndexesToPurge || + NaturalDocs::Menu->HasChanged() ); + }; + + if ($numberOfFilesToPurge) + { + NaturalDocs::StatusMessage->Start('Purging ' . $numberOfFilesToPurge + . ' file' . ($numberOfFilesToPurge > 1 ? 's' : '') . '...', + scalar @$buildTargets); + + foreach my $buildTarget (@$buildTargets) + { + $buildTarget->Builder()->PurgeFiles($filesToPurge); + NaturalDocs::StatusMessage->CompletedItem(); + }; + }; + + if ($numberOfIndexesToPurge) + { + NaturalDocs::StatusMessage->Start('Purging ' . $numberOfIndexesToPurge + . ' index' . ($numberOfIndexesToPurge > 1 ? 'es' : '') . '...', + scalar @$buildTargets); + + foreach my $buildTarget (@$buildTargets) + { + $buildTarget->Builder()->PurgeIndexes(\%indexesToPurge); + NaturalDocs::StatusMessage->CompletedItem(); + }; + }; + + if ($numberOfImagesToPurge) + { + NaturalDocs::StatusMessage->Start('Purging ' . $numberOfImagesToPurge + . ' image' . ($numberOfImagesToPurge > 1 ? 's' : '') . '...', + scalar @$buildTargets); + + foreach my $buildTarget (@$buildTargets) + { + $buildTarget->Builder()->PurgeImages($imagesToPurge); + NaturalDocs::StatusMessage->CompletedItem(); + }; + }; + + if ($numberOfFilesToBuild) + { + NaturalDocs::StatusMessage->Start('Building ' . $numberOfFilesToBuild + . ' file' . ($numberOfFilesToBuild > 1 ? 's' : '') . '...', + $numberOfFilesToBuild); + + foreach my $file (keys %$filesToBuild) + { + my $parsedFile = NaturalDocs::Parser->ParseForBuild($file); + + NaturalDocs::Error->OnStartBuilding($file); + + foreach my $buildTarget (@$buildTargets) + { + $buildTarget->Builder()->BuildFile($file, $parsedFile); + NaturalDocs::StatusMessage->CompletedItem(); + }; + + NaturalDocs::Error->OnEndBuilding($file); + }; + }; + + if ($numberOfIndexesToBuild) + { + NaturalDocs::StatusMessage->Start('Building ' . $numberOfIndexesToBuild + . ' index' . ($numberOfIndexesToBuild > 1 ? 'es' : '') . '...', + $numberOfIndexesToBuild); + + foreach my $index (keys %indexesToBuild) + { + foreach my $buildTarget (@$buildTargets) + { + $buildTarget->Builder()->BuildIndex($index); + NaturalDocs::StatusMessage->CompletedItem(); + }; + }; + }; + + if ($numberOfImagesToUpdate) + { + NaturalDocs::StatusMessage->Start('Updating ' . $numberOfImagesToUpdate + . ' image' . ($numberOfImagesToUpdate > 1 ? 's' : '') . '...', + $numberOfImagesToUpdate); + + foreach my $image (keys %$imagesToUpdate) + { + foreach my $buildTarget (@$buildTargets) + { + $buildTarget->Builder()->UpdateImage($image); + NaturalDocs::StatusMessage->CompletedItem(); + }; + }; + }; + + if (NaturalDocs::Menu->HasChanged()) + { + if (!NaturalDocs::Settings->IsQuiet()) + { print "Updating menu...\n"; }; + + foreach my $buildTarget (@$buildTargets) + { $buildTarget->Builder()->UpdateMenu(); }; + }; + + foreach my $buildTarget (@$buildTargets) + { + $buildTarget->Builder()->EndBuild($numberOfFilesToBuild || $numberOfFilesToPurge || + $numberOfIndexesToBuild || $numberOfIndexesToPurge || + $numberOfImagesToUpdate || $numberOfImagesToPurge || + NaturalDocs::Menu->HasChanged()); + }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Builder/Base.pm b/docs/tool/Modules/NaturalDocs/Builder/Base.pm new file mode 100644 index 00000000..0298264d --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Builder/Base.pm @@ -0,0 +1,348 @@ +############################################################################### +# +# Class: NaturalDocs::Builder::Base +# +############################################################################### +# +# A base class for all Builder output formats. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Builder::Base; + + +############################################################################### +# Group: Notes + + +# +# Topic: Implementation +# +# Builder packages are implemented as blessed arrayrefs, not hashrefs. This is done for all objects in Natural Docs for +# efficiency reasons. You create members by defining constants via <NaturalDocs::DefineMembers> and using them as +# indexes into the array. +# + +# +# Topic: Function Order +# +# The functions in the build process will always be called in the following order. +# +# - <BeginBuild()> will always be called. +# - <PurgeFiles()> will be called next only if there's files that need to be purged. +# - <PurgeIndexes()> will be called next only if there's indexes that need to be purged. +# - <PurgeImages()> will e called next only if there's images that need to be purged. +# - <BuildFile()> will be called once for each file that needs to be built, if any. +# - <BuildIndex()> will be called once for each index that changed and is part of the menu, if any. +# - <UpdateImage()> will be called once for each image that needs to be updated, if any. +# - <UpdateMenu()> will be called next only if the menu changed. +# - <EndBuild()> will always be called. +# + +# +# Topic: How to Approach +# +# Here's an idea of how to approach making packages for different output types. +# +# +# Multiple Output Files, Embedded Menu: +# +# This example is for when you want to build one output file per source file, each with its own copy of the menu within it. +# This is how <NaturalDocs::Builder::HTML> works. +# +# Make sure you create a function that generates just the menu for a particular source file. We'll need to generate menus for +# both building a file from scratch and for updating the menu on an existing output file, so it's better to give it its own function. +# You may want to surround it with something that can be easily detected in the output file to make replacing easier. +# +# <BeginBuild()> isn't important. You don't need to implement it. +# +# Implement <PurgeFiles()> to delete the output files associated with the purged files. +# +# Implement <PurgeIndexes()> to delete the output files associated with the purged indexes. +# +# Implement <BuildFile()> to create an output file for the parsed source file. Use the menu function described earlier. +# +# Implement <BuildIndex()> to create an output file for each index. Use the menu function described earlier for each page. +# +# Implement <UpdateMenu()> to go through the list of unbuilt files and update their menus. You can get the list from +# <NaturalDocs::Project->UnbuiltFilesWithContent()>. You need to open their output files, replace the menu, and save it back +# to disk. Yes, it would be simpler from a programmer's point of view to just rebuild the file completely, but that would be +# _very_ inefficient since there could potentially be a _lot_ of files in this group. +# +# Also make sure <UpdateMenu()> goes through the unchanged indexes and updates them as well. +# +# <EndBuild()> isn't important. You don't need to implement it. +# +# +# Multiple Output Files, Menu in File: +# +# This example is for when you want to build one output file per source file, but keep the menu in its own separate file. This +# is how <NaturalDocs::Builder::FramedHTML> works. +# +# <BeginBuild()> isn't important. You don't need to implement it. +# +# Implement <PurgeFiles()> to delete the output files associated with the purged files. +# +# Implement <PurgeIndexes()> to delete the output files associated with the purged indexes. +# +# Implement <BuildFile()> to generate an output file from the parsed source file. +# +# Implement <BuildIndex()> to generate an output file for each index. +# +# Implement <UpdateMenu()> to rebuild the menu file. +# +# <EndBuild()> isn't important. You don't need to implement it. +# +# +# Single Output File using Intermediate Files: +# +# This example is for when you want to build one output file, such as a PDF file, but use intermediate files to handle differential +# building. This would be much like how a compiler compiles each source file into a object file, and then a linker stitches them +# all together into the final executable file. +# +# <BeginBuild()> isn't important. You don't need to implement it. +# +# Implement <PurgeFiles()> to delete the intermediate files associated with the purged files. +# +# Implement <PurgeIndexes()> to delete the intermediate files associated with the purged indexes. +# +# Implement <BuildFile()> to generate an intermediate file from the parsed source file. +# +# Implement <BuildIndex()> to generate an intermediate file for the specified index. +# +# Implement <UpdateMenu()> to generate the intermediate file for the menu. +# +# Implement <EndBuild()> so that if the project changed, it stitches the intermediate files together into the final +# output file. Make sure you check the parameter because the function will be called when nothing changes too. +# +# +# Single Output File using Direct Changes: +# +# This example is for when you want to build one output file, such as a PDF file, but engineering it in such a way that you don't +# need to use intermediate files. In other words, you're able to add, delete, and modify entries directly in the output file. +# +# Implement <BeginBuild()> so that if the project changed, it opens the output file and does anything it needs to do +# to get ready for editing. +# +# Implement <PurgeFiles()> to remove the entries associated with the purged files. +# +# Implement <PurgeIndexes()> to remove the entries associated with the purged indexes. +# +# Implement <BuildFile()> to add or replace a section of the output file with a new one generated from the parsed file. +# +# Implement <BuildIndex()> to add or replace an index in the output file with a new one generated from the specified index. +# +# Implement <EndBuild()> so that if the project changed, it saves the output file to disk. +# +# How you handle the menu depends on how the output file references other sections of itself. If it can do so by name, then +# you can implement <UpdateMenu()> to update the menu section of the file and you're done. If it has to reference itself +# by address or offset, it gets trickier. You should skip <UpdateMenu()> and instead rebuild the menu in <EndBuild()> if +# the parameter is true. This lets you do it whenever anything changes in a file, rather than just when the menu +# visibly changes. How you keep track of the locations and how they change is your problem. +# + + +############################################################################### +# +# Group: Required Interface Functions +# +# All Builder classes *must* define these functions. +# + + +# +# Function: INIT +# +# Define this function to call <NaturalDocs::Builder->Add()> so that <NaturalDocs::Builder> knows about this package. +# Packages are defined this way so that new ones can be added without messing around in other code. +# + + +# +# Function: CommandLineOption +# +# Define this function to return the text that should be put in the command line after -o to use this package. It cannot have +# spaces and is not case sensitive. +# +# For example, <NaturalDocs::Builder::HTML> returns 'html' so someone could use -o html [directory] to use that package. +# +sub CommandLineOption + { + NaturalDocs::Error->SoftDeath($_[0] . " didn't define CommandLineOption()."); + }; + + +# +# Function: BuildFile +# +# Define this function to convert a parsed file to this package's output format. This function will be called once for every source +# file that needs to be rebuilt. However, if a file hasn't changed since the last time Natural Docs was run, it will not be sent to +# this function. All packages must support differential build. +# +# Parameters: +# +# sourceFile - The name of the source file. +# parsedFile - The parsed source file, as an arrayref of <NaturalDocs::Parser::ParsedTopic> objects. +# +sub BuildFile #(sourceFile, parsedFile) + { + NaturalDocs::Error->SoftDeath($_[0] . " didn't define BuildFile()."); + }; + + +############################################################################### +# +# Group: Optional Interface Functions +# +# These functions can be implemented but packages are not required to do so. +# + + +# +# Function: New +# +# Creates and returns a new object. +# +# Note that this is the only function where the first parameter will be the package name, not the object itself. +# +sub New + { + my $package = shift; + + my $object = [ ]; + bless $object, $package; + + return $object; + }; + + +# +# Function: BeginBuild +# +# Define this function if the package needs to do anything at the beginning of the build process. This function will be called +# every time Natural Docs is run, even if the project hasn't changed. This allows you to manage dependencies specific +# to the output format that may change independently from the source tree and menu. For example, +# <NaturalDocs::Builder::HTML> needs to keep the CSS files in sync regardless of whether the source tree changed or not. +# +# Parameters: +# +# hasChanged - Whether the project has changed, such as source files or the menu file. If false, nothing else is going to be +# called except <EndBuild()>. +# +sub BeginBuild #(hasChanged) + { + }; + + +# +# Function: EndBuild +# +# Define this function if the package needs to do anything at the end of the build process. This function will be called every time +# Natural Docs is run, even if the project hasn't changed. This allows you to manage dependencies specific to the output +# format that may change independently from the source tree. For example, <NaturalDocs::Builder::HTML> needs to keep the +# CSS files in sync regardless of whether the source tree changed or not. +# +# Parameters: +# +# hasChanged - Whether the project has changed, such as source files or the menu file. If false, the only other function that +# was called was <BeginBuild()>. +# +sub EndBuild #(hasChanged) + { + }; + + +# +# Function: BuildIndex +# +# Define this function to create an index for the passed topic. You can get the index from +# <NaturalDocs::SymbolTable->Index()>. +# +# The reason it's not passed directly to this function is because indexes may be time-consuming to create. As such, they're +# generated on demand because some output packages may choose not to implement them. +# +# Parameters: +# +# topic - The <TopicType> to limit the index by. +# +sub BuildIndex #(topic) + { + }; + + +# +# Function: UpdateImage +# +# Define this function to add or update the passed image in the output. +# +# Parameters: +# +# file - The image <FileName> +# +sub UpdateImage #(file) + { + }; + + +# +# Function: PurgeFiles +# +# Define this function to make the package remove all output related to the passed files. These files no longer have Natural Docs +# content. +# +# Parameters: +# +# files - An existence hashref of the files to purge. +# +sub PurgeFiles #(files) + { + }; + + +# +# Function: PurgeIndexes +# +# Define this function to make the package remove all output related to the passed indexes. These indexes are no longer part +# of the menu. +# +# Parameters: +# +# indexes - An existence hashref of the <TopicTypes> of the indexes to purge. +# +sub PurgeIndexes #(indexes) + { + }; + + +# +# Function: PurgeImages +# +# Define this function to make the package remove all output related to the passed image files. These files are no longer used +# by the documentation. +# +# Parameters: +# +# files - An existence hashref of the image <FileNames> to purge. +# +sub PurgeImages #(files) + { + }; + + +# +# Function: UpdateMenu +# +# Define this function to make the package update the menu. It will only be called if the menu changed. +# +sub UpdateMenu + { + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Builder/FramedHTML.pm b/docs/tool/Modules/NaturalDocs/Builder/FramedHTML.pm new file mode 100644 index 00000000..ab020aa6 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Builder/FramedHTML.pm @@ -0,0 +1,345 @@ +############################################################################### +# +# Package: NaturalDocs::Builder::FramedHTML +# +############################################################################### +# +# A package that generates output in HTML with frames. +# +# All functions are called with Package->Function() notation. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + + +use strict; +use integer; + +package NaturalDocs::Builder::FramedHTML; + +use base 'NaturalDocs::Builder::HTMLBase'; + + +############################################################################### +# Group: Implemented Interface Functions + + +# +# Function: INIT +# +# Registers the package with <NaturalDocs::Builder>. +# +sub INIT + { + NaturalDocs::Builder->Add(__PACKAGE__); + }; + + +# +# Function: CommandLineOption +# +# Returns the option to follow -o to use this package. In this case, "html". +# +sub CommandLineOption + { + return 'FramedHTML'; + }; + + +# +# Function: BuildFile +# +# Builds the output file from the parsed source file. +# +# Parameters: +# +# sourcefile - The <FileName> of the source file. +# parsedFile - An arrayref of the source file as <NaturalDocs::Parser::ParsedTopic> objects. +# +sub BuildFile #(sourceFile, parsedFile) + { + my ($self, $sourceFile, $parsedFile) = @_; + + my $outputFile = $self->OutputFileOf($sourceFile); + + + # 99.99% of the time the output directory will already exist, so this will actually be more efficient. It only won't exist + # if a new file was added in a new subdirectory and this is the first time that file was ever parsed. + if (!open(OUTPUTFILEHANDLE, '>' . $outputFile)) + { + NaturalDocs::File->CreatePath( NaturalDocs::File->NoFileName($outputFile) ); + + open(OUTPUTFILEHANDLE, '>' . $outputFile) + or die "Couldn't create output file " . $outputFile . "\n"; + }; + + print OUTPUTFILEHANDLE + + + + # IE 6 doesn't like any doctype here at all. Add one (strict or transitional doesn't matter) and it makes the page slightly too + # wide for the frame. Mozilla and Opera handle it like champs either way because they Don't Suck(tm). + + # '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" ' + # . '"http://www.w3.org/TR/REC-html40/loose.dtd">' . "\n\n" + + '<html><head>' + + . (NaturalDocs::Settings->CharSet() ? + '<meta http-equiv="Content-Type" content="text/html; charset=' . NaturalDocs::Settings->CharSet() . '">' : '') + + . '<title>' + . $self->BuildTitle($sourceFile) + . '</title>' + + . '<link rel="stylesheet" type="text/css" href="' . $self->MakeRelativeURL($outputFile, $self->MainCSSFile(), 1) . '">' + + . '<script language=JavaScript src="' . $self->MakeRelativeURL($outputFile, $self->MainJavaScriptFile(), 1) . '"></script>' + + . '</head><body class="FramedContentPage" onLoad="NDOnLoad()">' + . $self->OpeningBrowserStyles() + + . $self->StandardComments() + + . "\n\n\n" + . $self->BuildContent($sourceFile, $parsedFile) + . "\n\n\n" + + . $self->BuildToolTips() + + . $self->ClosingBrowserStyles() + . '</body></html>'; + + + close(OUTPUTFILEHANDLE); + }; + + +# +# Function: BuildIndex +# +# Builds an index for the passed type. +# +# Parameters: +# +# type - The <TopicType> to limit the index to, or undef if none. +# +sub BuildIndex #(type) + { + my ($self, $type) = @_; + + my $indexTitle = $self->IndexTitleOf($type); + my $indexFile = $self->IndexFileOf($type); + + my $startIndexPage = + + '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" ' + . '"http://www.w3.org/TR/REC-html40/loose.dtd">' . "\n\n" + + . '<html><head>' + + . (NaturalDocs::Settings->CharSet() ? + '<meta http-equiv="Content-Type" content="text/html; charset=' . NaturalDocs::Settings->CharSet() . '">' : '') + + . '<title>'; + + if (defined NaturalDocs::Menu->Title()) + { $startIndexPage .= $self->StringToHTML(NaturalDocs::Menu->Title()) . ' - '; }; + + $startIndexPage .= + $indexTitle + . '</title>' + + . '<link rel="stylesheet" type="text/css" href="' . $self->MakeRelativeURL($indexFile, $self->MainCSSFile(), 1) . '">' + + . '<script language=JavaScript src="' . $self->MakeRelativeURL($indexFile, $self->MainJavaScriptFile(), 1) . '"></script>' + + . '</head><body class="FramedIndexPage" onLoad="NDOnLoad()">' + . $self->OpeningBrowserStyles() + + . "\n\n\n" + . $self->StandardComments() + . "\n\n\n" + . '<div id=Index>' + . '<div class=IPageTitle>' + . $indexTitle + . '</div>'; + + + my $endIndexPage = + + '</div><!--Index-->' + . "\n\n\n" + + . $self->ClosingBrowserStyles() + + . '</body></html>'; + + my $startSearchResultsPage = + + '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" ' + . '"http://www.w3.org/TR/REC-html40/loose.dtd">' . "\n\n" + + . '<html><head>' + + . (NaturalDocs::Settings->CharSet() ? + '<meta http-equiv="Content-Type" content="text/html; charset=' . NaturalDocs::Settings->CharSet() . '">' : '') + + . '<link rel="stylesheet" type="text/css" href="' . $self->MakeRelativeURL($indexFile, $self->MainCSSFile(), 1) . '">' + + . '<script language=JavaScript src="' . $self->MakeRelativeURL($indexFile, $self->MainJavaScriptFile(), 1) . '"></script>' + . '<script language=JavaScript src="' . $self->MakeRelativeURL($indexFile, $self->SearchDataJavaScriptFile(), 1) . '">' + . '</script>' + + . '</head><body class="FramedSearchResultsPage" onLoad="NDOnLoad()">' + . $self->OpeningBrowserStyles() + + . "\n\n\n" + . $self->StandardComments() + . "\n\n\n" + + . '<div id=Index>' + . '<div class=IPageTitle>' + . 'Search Results' + . '</div>'; + + my $endSearchResultsPage = + + '</div><!--Index-->' + . "\n\n\n" + + . $self->ClosingBrowserStyles() + + . '</body></html>'; + + my $indexContent = NaturalDocs::SymbolTable->Index($type); + my $pageCount = $self->BuildIndexPages($type, $indexContent, $startIndexPage, $endIndexPage, + $startSearchResultsPage, $endSearchResultsPage); + $self->PurgeIndexFiles($type, $indexContent, $pageCount + 1); + }; + + +# +# Function: UpdateMenu +# +# Builds the menu file. Also generates index.html. +# +sub UpdateMenu + { + my $self = shift; + + my $outputDirectory = NaturalDocs::Settings->OutputDirectoryOf($self); + my $outputFile = NaturalDocs::File->JoinPaths($outputDirectory, 'menu.html'); + + + open(OUTPUTFILEHANDLE, '>' . $outputFile) + or die "Couldn't create output file " . $outputFile . "\n"; + + my $title = 'Menu'; + if (defined $title) + { $title .= ' - ' . NaturalDocs::Menu->Title(); }; + + $title = $self->StringToHTML($title); + + + print OUTPUTFILEHANDLE + + + '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" ' + . '"http://www.w3.org/TR/REC-html40/loose.dtd">' . "\n\n" + + . '<html><head>' + + . (NaturalDocs::Settings->CharSet() ? + '<meta http-equiv="Content-Type" content="text/html; charset=' . NaturalDocs::Settings->CharSet() . '">' : '') + + . '<title>' + . $title + . '</title>' + + . '<base target="Content">' + + . '<link rel="stylesheet" type="text/css" href="' . $self->MakeRelativeURL($outputFile, $self->MainCSSFile(), 1) . '">' + + . '<script language=JavaScript src="' . $self->MakeRelativeURL($outputFile, $self->MainJavaScriptFile(), 1) . '"></script>' + . '<script language=JavaScript src="' . $self->MakeRelativeURL($outputFile, $self->SearchDataJavaScriptFile(), 1) . '">' + . '</script>' + + . '</head><body class="FramedMenuPage" onLoad="NDOnLoad()">' + . $self->OpeningBrowserStyles() + + . $self->StandardComments() + + . "\n\n\n" + . $self->BuildMenu(undef, undef) + . "\n\n\n" + . $self->BuildFooter(1) + . "\n\n\n" + + . $self->ClosingBrowserStyles() + . '</body></html>'; + + + close(OUTPUTFILEHANDLE); + + + # Update index.html + + my $firstMenuEntry = $self->FindFirstFile(); + my $indexFile = NaturalDocs::File->JoinPaths( NaturalDocs::Settings->OutputDirectoryOf($self), 'index.html' ); + + # We have to check because it's possible that there may be no files with Natural Docs content and thus no files on the menu. + if (defined $firstMenuEntry) + { + open(INDEXFILEHANDLE, '>' . $indexFile) + or die "Couldn't create output file " . $indexFile . ".\n"; + + print INDEXFILEHANDLE + + '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN" ' + . '"http://www.w3.org/TR/REC-html40/frameset.dtd">' + + . '<html>' + + . '<head>' + + . (NaturalDocs::Settings->CharSet() ? + '<meta http-equiv="Content-Type" content="text/html; charset=' . NaturalDocs::Settings->CharSet() . '">' : '') + + . '<title>' + . $self->StringToHTML(NaturalDocs::Menu->Title()) + . '</title>' + + . '</head>' + + . $self->StandardComments() + + . '<frameset cols="185,*">' + . '<frame name=Menu src="menu.html">' + . '<frame name=Content src="' + . $self->MakeRelativeURL($indexFile, $self->OutputFileOf($firstMenuEntry->Target()), 1) . '">' + . '</frameset>' + + . '<noframes>' + . 'This documentation was designed for use with frames. However, you can still use it by ' + . '<a href="menu.html">starting from the menu page</a>.' + . "<script language=JavaScript><!--\n" + . 'location.href="menu.html";' + . "\n// --></script>" + . '</noframes>' + + . '</html>'; + + close INDEXFILEHANDLE; + } + + elsif (-e $indexFile) + { + unlink($indexFile); + }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Builder/HTML.pm b/docs/tool/Modules/NaturalDocs/Builder/HTML.pm new file mode 100644 index 00000000..95f31b5a --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Builder/HTML.pm @@ -0,0 +1,398 @@ +############################################################################### +# +# Package: NaturalDocs::Builder::HTML +# +############################################################################### +# +# A package that generates output in HTML. +# +# All functions are called with Package->Function() notation. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + + +use strict; +use integer; + +package NaturalDocs::Builder::HTML; + +use base 'NaturalDocs::Builder::HTMLBase'; + + +############################################################################### +# Group: Implemented Interface Functions + + +# +# Function: INIT +# +# Registers the package with <NaturalDocs::Builder>. +# +sub INIT + { + NaturalDocs::Builder->Add(__PACKAGE__); + }; + + +# +# Function: CommandLineOption +# +# Returns the option to follow -o to use this package. In this case, "html". +# +sub CommandLineOption + { + return 'HTML'; + }; + + +# +# Function: BuildFile +# +# Builds the output file from the parsed source file. +# +# Parameters: +# +# sourcefile - The <FileName> of the source file. +# parsedFile - An arrayref of the source file as <NaturalDocs::Parser::ParsedTopic> objects. +# +sub BuildFile #(sourceFile, parsedFile) + { + my ($self, $sourceFile, $parsedFile) = @_; + + my $outputFile = $self->OutputFileOf($sourceFile); + + + # 99.99% of the time the output directory will already exist, so this will actually be more efficient. It only won't exist + # if a new file was added in a new subdirectory and this is the first time that file was ever parsed. + if (!open(OUTPUTFILEHANDLE, '>' . $outputFile)) + { + NaturalDocs::File->CreatePath( NaturalDocs::File->NoFileName($outputFile) ); + + open(OUTPUTFILEHANDLE, '>' . $outputFile) + or die "Couldn't create output file " . $outputFile . "\n"; + }; + + print OUTPUTFILEHANDLE + + + '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" ' + . '"http://www.w3.org/TR/REC-html40/strict.dtd">' . "\n\n" + + . '<html><head>' + + . (NaturalDocs::Settings->CharSet() ? + '<meta http-equiv="Content-Type" content="text/html; charset=' . NaturalDocs::Settings->CharSet() . '">' : '') + + . '<title>' + . $self->BuildTitle($sourceFile) + . '</title>' + + . '<link rel="stylesheet" type="text/css" href="' . $self->MakeRelativeURL($outputFile, $self->MainCSSFile(), 1) . '">' + + . '<script language=JavaScript src="' . $self->MakeRelativeURL($outputFile, $self->MainJavaScriptFile(), 1) . '">' + . '</script>' + . '<script language=JavaScript src="' . $self->MakeRelativeURL($outputFile, $self->SearchDataJavaScriptFile(), 1) . '">' + . '</script>' + + . '</head><body class="ContentPage" onLoad="NDOnLoad()">' + . $self->OpeningBrowserStyles() + + . $self->StandardComments() + + . "\n\n\n" + . $self->BuildContent($sourceFile, $parsedFile) + . "\n\n\n" + . $self->BuildFooter() + . "\n\n\n" + . $self->BuildMenu($sourceFile, undef) + . "\n\n\n" + . $self->BuildToolTips() + . "\n\n\n" + . '<div id=MSearchResultsWindow>' + . '<iframe src="" frameborder=0 name=MSearchResults id=MSearchResults></iframe>' + . '<a href="javascript:searchPanel.CloseResultsWindow()" id=MSearchResultsWindowClose>Close</a>' + . '</div>' + . "\n\n\n" + + . $self->ClosingBrowserStyles() + . '</body></html>'; + + + close(OUTPUTFILEHANDLE); + }; + + +# +# Function: BuildIndex +# +# Builds an index for the passed type. +# +# Parameters: +# +# type - The <TopicType> to limit the index to, or undef if none. +# +sub BuildIndex #(type) + { + my ($self, $type) = @_; + + my $indexTitle = $self->IndexTitleOf($type); + + my $startIndexPage = + + '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" ' + . '"http://www.w3.org/TR/REC-html40/strict.dtd">' . "\n\n" + + . '<html><head>' + + . (NaturalDocs::Settings->CharSet() ? + '<meta http-equiv="Content-Type" content="text/html; charset=' . NaturalDocs::Settings->CharSet() . '">' : '') + + . '<title>' + . $indexTitle; + + if (defined NaturalDocs::Menu->Title()) + { $startIndexPage .= ' - ' . $self->StringToHTML(NaturalDocs::Menu->Title()); }; + + $startIndexPage .= + '</title>' + + . '<link rel="stylesheet" type="text/css" href="' . $self->MakeRelativeURL($self->IndexDirectory(), + $self->MainCSSFile()) . '">' + + . '<script language=JavaScript src="' . $self->MakeRelativeURL($self->IndexDirectory(), + $self->MainJavaScriptFile()) . '"></script>' + . '<script language=JavaScript src="' . $self->MakeRelativeURL($self->IndexDirectory(), + $self->SearchDataJavaScriptFile()) . '">' + . '</script>' + + . '</head><body class="IndexPage" onLoad="NDOnLoad()">' + . $self->OpeningBrowserStyles() + + . $self->StandardComments() + + . "\n\n\n" + + . '<div id=Index>' + . '<div class=IPageTitle>' + . $indexTitle + . '</div>'; + + my $endIndexPage = + '</div><!--Index-->' + + . "\n\n\n" + . $self->BuildFooter() + . "\n\n\n" + . $self->BuildMenu(undef, $type) + . "\n\n\n" + . '<div id=MSearchResultsWindow>' + . '<iframe src="" frameborder=0 name=MSearchResults id=MSearchResults></iframe>' + . '<a href="javascript:searchPanel.CloseResultsWindow()" id=MSearchResultsWindowClose>Close</a>' + . '</div>' + . "\n\n\n" + + . $self->ClosingBrowserStyles() + . '</body></html>'; + + + my $startSearchResultsPage = + + '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" ' + . '"http://www.w3.org/TR/REC-html40/strict.dtd">' . "\n\n" + + . '<html><head>' + + . (NaturalDocs::Settings->CharSet() ? + '<meta http-equiv="Content-Type" content="text/html; charset=' . NaturalDocs::Settings->CharSet() . '">' : '') + + . '<link rel="stylesheet" type="text/css" href="' . $self->MakeRelativeURL($self->SearchResultsDirectory(), + $self->MainCSSFile()) . '">' + + . '<script language=JavaScript src="' . $self->MakeRelativeURL($self->SearchResultsDirectory(), + $self->MainJavaScriptFile()) . '"></script>' + + . '</head><body class="PopupSearchResultsPage" onLoad="NDOnLoad()">' + . $self->OpeningBrowserStyles() + + . $self->StandardComments() + + . "\n\n\n" + + . '<div id=Index>'; + + + my $endSearchResultsPage = + '</div>' + . $self->ClosingBrowserStyles() + . '</body></html>'; + + my $indexContent = NaturalDocs::SymbolTable->Index($type); + my $pageCount = $self->BuildIndexPages($type, $indexContent, $startIndexPage, $endIndexPage, + $startSearchResultsPage, $endSearchResultsPage); + $self->PurgeIndexFiles($type, $indexContent, $pageCount + 1); + }; + + +# +# Function: UpdateMenu +# +# Updates the menu in all the output files that weren't rebuilt. Also generates index.html. +# +sub UpdateMenu + { + my $self = shift; + + + # Update the menu on unbuilt files. + + my $filesToUpdate = NaturalDocs::Project->UnbuiltFilesWithContent(); + + foreach my $sourceFile (keys %$filesToUpdate) + { + $self->UpdateFile($sourceFile); + }; + + + # Update the menu on unchanged index files. + + my $indexes = NaturalDocs::Menu->Indexes(); + + foreach my $index (keys %$indexes) + { + if (!NaturalDocs::SymbolTable->IndexChanged($index)) + { + $self->UpdateIndex($index); + }; + }; + + + # Update index.html + + my $firstMenuEntry = $self->FindFirstFile(); + my $indexFile = NaturalDocs::File->JoinPaths( NaturalDocs::Settings->OutputDirectoryOf($self), 'index.html' ); + + # We have to check because it's possible that there may be no files with Natural Docs content and thus no files on the menu. + if (defined $firstMenuEntry) + { + open(INDEXFILEHANDLE, '>' . $indexFile) + or die "Couldn't create output file " . $indexFile . ".\n"; + + print INDEXFILEHANDLE + '<html><head>' + . '<meta http-equiv="Refresh" CONTENT="0; URL=' + . $self->MakeRelativeURL( NaturalDocs::File->JoinPaths( NaturalDocs::Settings->OutputDirectoryOf($self), 'index.html'), + $self->OutputFileOf($firstMenuEntry->Target()), 1 ) . '">' + . '</head></html>'; + + close INDEXFILEHANDLE; + } + + elsif (-e $indexFile) + { + unlink($indexFile); + }; + }; + + + +############################################################################### +# Group: Support Functions + + +# +# Function: UpdateFile +# +# Updates an output file. Replaces the menu, HTML title, and footer. It opens the output file, makes the changes, and saves it +# back to disk, which is much quicker than rebuilding the file from scratch if these were the only things that changed. +# +# Parameters: +# +# sourceFile - The source <FileName>. +# +# Dependencies: +# +# - Requires <Builder::BuildMenu()> to surround its content with the exact strings "<div id=Menu>" and "</div><!--Menu-->". +# - Requires <Builder::BuildFooter()> to surround its content with the exact strings "<div id=Footer>" and +# "</div><!--Footer-->". +# +sub UpdateFile #(sourceFile) + { + my ($self, $sourceFile) = @_; + + my $outputFile = $self->OutputFileOf($sourceFile); + + if (open(OUTPUTFILEHANDLE, '<' . $outputFile)) + { + my $content; + + read(OUTPUTFILEHANDLE, $content, -s OUTPUTFILEHANDLE); + close(OUTPUTFILEHANDLE); + + + $content =~ s{<title>[^<]*<\/title>}{'<title>' . $self->BuildTitle($sourceFile) . '</title>'}e; + + $content =~ s/<div id=Menu>.*?<\/div><!--Menu-->/$self->BuildMenu($sourceFile, undef)/es; + + $content =~ s/<div id=Footer>.*?<\/div><!--Footer-->/$self->BuildFooter()/e; + + + open(OUTPUTFILEHANDLE, '>' . $outputFile); + print OUTPUTFILEHANDLE $content; + close(OUTPUTFILEHANDLE); + }; + }; + + +# +# Function: UpdateIndex +# +# Updates an index's output file. Replaces the menu and footer. It opens the output file, makes the changes, and saves it +# back to disk, which is much quicker than rebuilding the file from scratch if these were the only things that changed. +# +# Parameters: +# +# type - The index <TopicType>, or undef if none. +# +sub UpdateIndex #(type) + { + my ($self, $type) = @_; + + my $page = 1; + + my $outputFile = $self->IndexFileOf($type, $page); + + my $newMenu = $self->BuildMenu(undef, $type); + my $newFooter = $self->BuildFooter(); + + while (-e $outputFile) + { + open(OUTPUTFILEHANDLE, '<' . $outputFile) + or die "Couldn't open output file " . $outputFile . ".\n"; + + my $content; + + read(OUTPUTFILEHANDLE, $content, -s OUTPUTFILEHANDLE); + close(OUTPUTFILEHANDLE); + + + $content =~ s/<div id=Menu>.*?<\/div><!--Menu-->/$newMenu/es; + + $content =~ s/<div id=Footer>.*<\/div><!--Footer-->/$newFooter/e; + + + open(OUTPUTFILEHANDLE, '>' . $outputFile) + or die "Couldn't save output file " . $outputFile . ".\n"; + + print OUTPUTFILEHANDLE $content; + close(OUTPUTFILEHANDLE); + + $page++; + $outputFile = $self->IndexFileOf($type, $page); + }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Builder/HTMLBase.pm b/docs/tool/Modules/NaturalDocs/Builder/HTMLBase.pm new file mode 100644 index 00000000..d943622b --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Builder/HTMLBase.pm @@ -0,0 +1,3693 @@ +############################################################################### +# +# Package: NaturalDocs::Builder::HTMLBase +# +############################################################################### +# +# A base package for all the shared functionality in <NaturalDocs::Builder::HTML> and +# <NaturalDocs::Builder::FramedHTML>. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + + +use Tie::RefHash; + +use strict; +use integer; + +package NaturalDocs::Builder::HTMLBase; + +use base 'NaturalDocs::Builder::Base'; + +use NaturalDocs::DefineMembers 'MADE_EMPTY_SEARCH_RESULTS_PAGE', 'MadeEmptySearchResultsPage()', + 'SetMadeEmptySearchResultsPage()'; + + + +############################################################################### +# Group: Object Variables + + +# +# Constants: Members +# +# The object is implemented as a blessed arrayref, with the follow constants as indexes. +# +# MADE_EMPTY_SEARCH_RESULTS_PAGE - Whether the search results page for searches with no results was generated. +# + +# +# Constants: NDMarkupToHTML Styles +# +# These are the styles used with <NDMarkupToHTML()>. +# +# NDMARKUPTOHTML_GENERAL - General style. +# NDMARKUPTOHTML_SUMMARY - For summaries. +# NDMARKUPTOHTML_TOOLTIP - For tooltips. +# +use constant NDMARKUPTOHTML_GENERAL => undef; +use constant NDMARKUPTOHTML_SUMMARY => 1; +use constant NDMARKUPTOHTML_TOOLTIP => 2; + + + +############################################################################### +# Group: Package Variables +# These variables are shared by all instances of the package so don't change them. + + +# +# handle: FH_CSS_FILE +# +# The file handle to use when updating CSS files. +# + + +# +# Hash: abbreviations +# +# An existence hash of acceptable abbreviations. These are words that <AddDoubleSpaces()> won't put a second space +# after when followed by period-whitespace-capital letter. Yes, this is seriously over-engineered. +# +my %abbreviations = ( mr => 1, mrs => 1, ms => 1, dr => 1, + rev => 1, fr => 1, 'i.e' => 1, + maj => 1, gen => 1, pres => 1, sen => 1, rep => 1, + n => 1, s => 1, e => 1, w => 1, ne => 1, se => 1, nw => 1, sw => 1 ); + +# +# array: indexHeadings +# +# An array of the headings of all the index sections. First is for symbols, second for numbers, and the rest for each letter. +# +my @indexHeadings = ( '$#!', '0-9', 'A' .. 'Z' ); + +# +# array: indexAnchors +# +# An array of the HTML anchors of all the index sections. First is for symbols, second for numbers, and the rest for each letter. +# +my @indexAnchors = ( 'Symbols', 'Numbers', 'A' .. 'Z' ); + +# +# array: searchExtensions +# +# An array of the search file name extensions for all the index sections. First is for symbols, second for numbers, and the rest +# for each letter. +# +my @searchExtensions = ( 'Symbols', 'Numbers', 'A' .. 'Z' ); + +# +# bool: saidUpdatingCSSFile +# +# Whether the status message "Updating CSS file..." has been displayed. We only want to print it once, no matter how many +# HTML-based targets we are building. +# +my $saidUpdatingCSSFile; + +# +# constant: ADD_HIDDEN_BREAKS +# +# Just a synonym for "1" so that setting the flag on <StringToHTML()> is clearer in the calling code. +# +use constant ADD_HIDDEN_BREAKS => 1; + + +############################################################################### +# Group: ToolTip Package Variables +# +# These variables are for the tooltip generation functions only. Since they're reset on every call to <BuildContent()> and +# <BuildIndexSections()>, and are only used by them and their support functions, they can be shared by all instances of the +# package. + +# +# int: tooltipLinkNumber +# +# A number used as part of the ID for each link that has a tooltip. Should be incremented whenever one is made. +# +my $tooltipLinkNumber; + +# +# int: tooltipNumber +# +# A number used as part of the ID for each tooltip. Should be incremented whenever one is made. +# +my $tooltipNumber; + +# +# hash: tooltipSymbolsToNumbers +# +# A hash that maps the tooltip symbols to their assigned numbers. +# +my %tooltipSymbolsToNumbers; + +# +# string: tooltipHTML +# +# The generated tooltip HTML. +# +my $tooltipHTML; + + +############################################################################### +# Group: Menu Package Variables +# +# These variables are for the menu generation functions only. Since they're reset on every call to <BuildMenu()> and are +# only used by it and its support functions, they can be shared by all instances of the package. +# + + +# +# hash: prebuiltMenus +# +# A hash that maps output directonies to menu HTML already built for it. There will be no selection or JavaScript in the menus. +# +my %prebuiltMenus; + + +# +# bool: menuNumbersAndLengthsDone +# +# Set when the variables that only need to be calculated for the menu once are done. This includes <menuGroupNumber>, +# <menuLength>, <menuGroupLengths>, and <menuGroupNumbers>, and <menuRootLength>. +# +my $menuNumbersAndLengthsDone; + + +# +# int: menuGroupNumber +# +# The current menu group number. Each time a group is created, this is incremented so that each one will be unique. +# +my $menuGroupNumber; + + +# +# int: menuLength +# +# The length of the entire menu, fully expanded. The value is computed from the <Menu Length Constants>. +# +my $menuLength; + + +# +# hash: menuGroupLengths +# +# A hash of the length of each group, *not* including any subgroup contents. The keys are references to each groups' +# <NaturalDocs::Menu::Entry> object, and the values are their lengths computed from the <Menu Length Constants>. +# +my %menuGroupLengths; +tie %menuGroupLengths, 'Tie::RefHash'; + + +# +# hash: menuGroupNumbers +# +# A hash of the number of each group, as managed by <menuGroupNumber>. The keys are references to each groups' +# <NaturalDocs::Menu::Entry> object, and the values are the number. +# +my %menuGroupNumbers; +tie %menuGroupNumbers, 'Tie::RefHash'; + + +# +# int: menuRootLength +# +# The length of the top-level menu entries without expansion. The value is computed from the <Menu Length Constants>. +# +my $menuRootLength; + + +# +# constants: Menu Length Constants +# +# Constants used to approximate the lengths of the menu or its groups. +# +# MENU_TITLE_LENGTH - The length of the title. +# MENU_SUBTITLE_LENGTH - The length of the subtitle. +# MENU_FILE_LENGTH - The length of one file entry. +# MENU_GROUP_LENGTH - The length of one group entry. +# MENU_TEXT_LENGTH - The length of one text entry. +# MENU_LINK_LENGTH - The length of one link entry. +# +# MENU_LENGTH_LIMIT - The limit of the menu's length. If the total length surpasses this limit, groups that aren't required +# to be open to show the selection will default to closed on browsers that support it. +# +use constant MENU_TITLE_LENGTH => 3; +use constant MENU_SUBTITLE_LENGTH => 1; +use constant MENU_FILE_LENGTH => 1; +use constant MENU_GROUP_LENGTH => 2; # because it's a line and a blank space +use constant MENU_TEXT_LENGTH => 1; +use constant MENU_LINK_LENGTH => 1; +use constant MENU_INDEX_LENGTH => 1; + +use constant MENU_LENGTH_LIMIT => 35; + + +############################################################################### +# Group: Image Package Variables +# +# These variables are for the image generation functions only. Since they're reset on every call to <BuildContent()>, +# and are only used by it and its support functions, they can be shared by all instances of thepackage. + + +# +# var: imageAnchorNumber +# Incremented for each image link in the file that requires an anchor. +# +my $imageAnchorNumber; + + +# +# var: imageContent +# +# The actual embedded image HTML for all image links. When generating an image link, the link HTML is returned and +# the HTML for the target image is added here. Periodically, such as after the end of the paragraph, this content should +# be added to the page and the variable set to undef. +# +my $imageContent; + + + +############################################################################### +# Group: Search Package Variables +# +# These variables are for the search generation functions only. Since they're reset on every call to <BuildIndexSections()> and +# are only used by them and their support functions, they can be shared by all instances of the package. + + +# +# hash: searchResultIDs +# +# A hash mapping lowercase-only search result IDs to the number of times they've been used. This is to work around an IE +# bug where it won't correctly reference IDs if they differ only in case. +# +my %searchResultIDs; + + + +############################################################################### +# Group: Object Functions + + +# +# Function: New +# Creates and returns a new object. +# +sub New + { + my $class = shift; + + my $object = $class->SUPER::New(); + $object->SetMadeEmptySearchResultsPage(0); + + return $object; + }; + + +# Function: MadeEmptySearchResultsPage +# Returns whether the empty search results page was created or not. + +# Function: SetMadeEmptySearchResultsPage +# Sets whether the empty search results page was created or not. + + + +############################################################################### +# Group: Implemented Interface Functions +# +# The behavior of these functions is shared between HTML output formats. +# + + +# +# Function: UpdateImage +# +# Define this function to add or update the passed image in the output. +# +# Parameters: +# +# file - The image <FileName> +# +sub UpdateImage #(file) + { + my ($self, $file) = @_; + + my $outputFile = $self->OutputImageOf($file); + my $outputDirectory = NaturalDocs::File->NoFileName($outputFile); + + if (!-d $outputDirectory) + { NaturalDocs::File->CreatePath($outputDirectory); }; + + NaturalDocs::File->Copy($file, $outputFile); + }; + + +# +# Function: PurgeFiles +# +# Deletes the output files associated with the purged source files. +# +sub PurgeFiles #(filesToPurge) + { + my ($self, $filesToPurge) = @_; + + # Combine directories into a hash to remove duplicate work. + my %directoriesToPurge; + + foreach my $file (keys %$filesToPurge) + { + # It's possible that there may be files there that aren't in a valid input directory anymore. They won't generate an output + # file name so we need to check for undef. + my $outputFile = $self->OutputFileOf($file); + if (defined $outputFile) + { + unlink($outputFile); + $directoriesToPurge{ NaturalDocs::File->NoFileName($outputFile) } = 1; + }; + }; + + foreach my $directory (keys %directoriesToPurge) + { + NaturalDocs::File->RemoveEmptyTree($directory, NaturalDocs::Settings->OutputDirectoryOf($self)); + }; + }; + + +# +# Function: PurgeIndexes +# +# Deletes the output files associated with the purged source files. +# +# Parameters: +# +# indexes - An existence hashref of the index types to purge. The keys are the <TopicTypes> or * for the general index. +# +sub PurgeIndexes #(indexes) + { + my ($self, $indexes) = @_; + + foreach my $index (keys %$indexes) + { + $self->PurgeIndexFiles($index, undef, undef); + }; + }; + + +# +# Function: PurgeImages +# +# Define this function to make the package remove all output related to the passed image files. These files are no longer used +# by the documentation. +# +# Parameters: +# +# files - An existence hashref of the image <FileNames> to purge. +# +sub PurgeImages #(files) + { + my ($self, $filesToPurge) = @_; + + # Combine directories into a hash to remove duplicate work. + my %directoriesToPurge; + + foreach my $file (keys %$filesToPurge) + { + # It's possible that there may be files there that aren't in a valid input directory anymore. They won't generate an output + # file name so we need to check for undef. + my $outputFile = $self->OutputImageOf($file); + if (defined $outputFile) + { + unlink($outputFile); + $directoriesToPurge{ NaturalDocs::File->NoFileName($outputFile) } = 1; + }; + }; + + foreach my $directory (keys %directoriesToPurge) + { + NaturalDocs::File->RemoveEmptyTree($directory, NaturalDocs::Settings->OutputDirectoryOf($self)); + }; + }; + + +# +# Function: BeginBuild +# +# Creates the necessary subdirectories in the output directory. +# +sub BeginBuild #(hasChanged) + { + my ($self, $hasChanged) = @_; + + foreach my $directory ( $self->JavaScriptDirectory(), $self->CSSDirectory(), $self->IndexDirectory(), + $self->SearchResultsDirectory() ) + { + if (!-d $directory) + { NaturalDocs::File->CreatePath($directory); }; + }; + }; + + +# +# Function: EndBuild +# +# Synchronizes the projects CSS and JavaScript files. Also generates the search data JavaScript file. +# +sub EndBuild #(hasChanged) + { + my ($self, $hasChanged) = @_; + + + # Update the style sheets. + + my $styles = NaturalDocs::Settings->Styles(); + my $changed; + + my $cssDirectory = $self->CSSDirectory(); + my $mainCSSFile = $self->MainCSSFile(); + + for (my $i = 0; $i < scalar @$styles; $i++) + { + my $outputCSSFile; + + if (scalar @$styles == 1) + { $outputCSSFile = $mainCSSFile; } + else + { $outputCSSFile = NaturalDocs::File->JoinPaths($cssDirectory, ($i + 1) . '.css'); }; + + + my $masterCSSFile = NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDirectory(), $styles->[$i] . '.css' ); + + if (! -e $masterCSSFile) + { $masterCSSFile = NaturalDocs::File->JoinPaths( NaturalDocs::Settings->StyleDirectory(), $styles->[$i] . '.css' ); }; + + # We check both the date and the size in case the user switches between two styles which just happen to have the same + # date. Should rarely happen, but it might. + if (! -e $outputCSSFile || + (stat($masterCSSFile))[9] != (stat($outputCSSFile))[9] || + -s $masterCSSFile != -s $outputCSSFile) + { + if (!NaturalDocs::Settings->IsQuiet() && !$saidUpdatingCSSFile) + { + print "Updating CSS file...\n"; + $saidUpdatingCSSFile = 1; + }; + + NaturalDocs::File->Copy($masterCSSFile, $outputCSSFile); + + $changed = 1; + }; + }; + + + my $deleteFrom; + + if (scalar @$styles == 1) + { $deleteFrom = 1; } + else + { $deleteFrom = scalar @$styles + 1; }; + + for (;;) + { + my $file = NaturalDocs::File->JoinPaths($cssDirectory, $deleteFrom . '.css'); + + if (! -e $file) + { last; }; + + unlink ($file); + $deleteFrom++; + + $changed = 1; + }; + + + if ($changed) + { + if (scalar @$styles > 1) + { + open(FH_CSS_FILE, '>' . $mainCSSFile); + + for (my $i = 0; $i < scalar @$styles; $i++) + { + print FH_CSS_FILE '@import URL("' . ($i + 1) . '.css");' . "\n"; + }; + + close(FH_CSS_FILE); + }; + }; + + + + # Update the JavaScript files + + my $jsMaster = NaturalDocs::File->JoinPaths( NaturalDocs::Settings->JavaScriptDirectory(), 'NaturalDocs.js' ); + my $jsOutput = $self->MainJavaScriptFile(); + + # We check both the date and the size in case the user switches between two styles which just happen to have the same + # date. Should rarely happen, but it might. + if (! -e $jsOutput || + (stat($jsMaster))[9] != (stat($jsOutput))[9] || + -s $jsMaster != -s $jsOutput) + { + NaturalDocs::File->Copy($jsMaster, $jsOutput); + }; + + + my @indexes = keys %{NaturalDocs::Menu->Indexes()}; + + open(FH_INDEXINFOJS, '>' . NaturalDocs::File->JoinPaths( $self->JavaScriptDirectory(), 'searchdata.js')); + + print FH_INDEXINFOJS + "var indexSectionsWithContent = {\n"; + + for (my $i = 0; $i < scalar @indexes; $i++) + { + if ($i != 0) + { print FH_INDEXINFOJS ",\n"; }; + + print FH_INDEXINFOJS ' "' . NaturalDocs::Topics->NameOfType($indexes[$i], 1, 1) . "\": {\n"; + + my $content = NaturalDocs::SymbolTable->IndexSectionsWithContent($indexes[$i]); + for (my $contentIndex = 0; $contentIndex < 28; $contentIndex++) + { + if ($contentIndex != 0) + { print FH_INDEXINFOJS ",\n"; }; + + print FH_INDEXINFOJS ' "' . $searchExtensions[$contentIndex] . '": ' . ($content->[$contentIndex] ? 'true' : 'false'); + }; + + print FH_INDEXINFOJS "\n }"; + }; + + print FH_INDEXINFOJS + "\n }"; + + close(FH_INDEXINFOJS); + }; + + + +############################################################################### +# Group: Section Functions + + +# +# Function: BuildTitle +# +# Builds and returns the HTML page title of a file. +# +# Parameters: +# +# sourceFile - The source <FileName> to build the title of. +# +# Returns: +# +# The source file's title in HTML. +# +sub BuildTitle #(sourceFile) + { + my ($self, $sourceFile) = @_; + + # If we have a menu title, the page title is [menu title] - [file title]. Otherwise it is just [file title]. + + my $title = NaturalDocs::Project->DefaultMenuTitleOf($sourceFile); + + my $menuTitle = NaturalDocs::Menu->Title(); + if (defined $menuTitle && $menuTitle ne $title) + { $title .= ' - ' . $menuTitle; }; + + $title = $self->StringToHTML($title); + + return $title; + }; + +# +# Function: BuildMenu +# +# Builds and returns the side menu of a file. +# +# Parameters: +# +# sourceFile - The source <FileName> to use if you're looking for a source file. +# indexType - The index <TopicType> to use if you're looking for an index. +# +# Both sourceFile and indexType may be undef. +# +# Returns: +# +# The side menu in HTML. +# +# Dependencies: +# +# - <Builder::HTML::UpdateFile()> and <Builder::HTML::UpdateIndex()> require this section to be surrounded with the exact +# strings "<div id=Menu>" and "</div><!--Menu-->". +# - This function depends on the way <BuildMenuSegment()> formats file and index entries. +# +sub BuildMenu #(FileName sourceFile, TopicType indexType) -> string htmlMenu + { + my ($self, $sourceFile, $indexType) = @_; + + if (!$menuNumbersAndLengthsDone) + { + $menuGroupNumber = 1; + $menuLength = 0; + %menuGroupLengths = ( ); + %menuGroupNumbers = ( ); + $menuRootLength = 0; + }; + + my $outputDirectory; + + if ($sourceFile) + { $outputDirectory = NaturalDocs::File->NoFileName( $self->OutputFileOf($sourceFile) ); } + elsif ($indexType) + { $outputDirectory = NaturalDocs::File->NoFileName( $self->IndexFileOf($indexType) ); } + else + { $outputDirectory = NaturalDocs::Settings->OutputDirectoryOf($self); }; + + + # Comment needed for UpdateFile(). + my $output = '<div id=Menu>'; + + + if (!exists $prebuiltMenus{$outputDirectory}) + { + my $segmentOutput; + + ($segmentOutput, $menuRootLength) = + $self->BuildMenuSegment($outputDirectory, NaturalDocs::Menu->Content(), 1); + + my $titleOutput; + + my $menuTitle = NaturalDocs::Menu->Title(); + if (defined $menuTitle) + { + if (!$menuNumbersAndLengthsDone) + { $menuLength += MENU_TITLE_LENGTH; }; + + $menuRootLength += MENU_TITLE_LENGTH; + + $titleOutput .= + '<div class=MTitle>' + . $self->StringToHTML($menuTitle); + + my $menuSubTitle = NaturalDocs::Menu->SubTitle(); + if (defined $menuSubTitle) + { + if (!$menuNumbersAndLengthsDone) + { $menuLength += MENU_SUBTITLE_LENGTH; }; + + $menuRootLength += MENU_SUBTITLE_LENGTH; + + $titleOutput .= + '<div class=MSubTitle>' + . $self->StringToHTML($menuSubTitle) + . '</div>'; + }; + + $titleOutput .= + '</div>'; + }; + + my $searchOutput; + + if (scalar keys %{NaturalDocs::Menu->Indexes()}) + { + $searchOutput = + '<script type="text/javascript"><!--' . "\n" + . 'var searchPanel = new SearchPanel("searchPanel", "' . $self->CommandLineOption() . '", ' + . '"' . $self->MakeRelativeURL($outputDirectory, $self->SearchResultsDirectory()) . '");' . "\n" + . '--></script>' + + . '<div id=MSearchPanel class=MSearchPanelInactive>' + . '<input type=text id=MSearchField value=Search ' + . 'onFocus="searchPanel.OnSearchFieldFocus(true)" onBlur="searchPanel.OnSearchFieldFocus(false)" ' + . 'onKeyUp="searchPanel.OnSearchFieldChange()">' + . '<select id=MSearchType ' + . 'onFocus="searchPanel.OnSearchTypeFocus(true)" onBlur="searchPanel.OnSearchTypeFocus(false)" ' + . 'onChange="searchPanel.OnSearchTypeChange()">'; + + my @indexes = keys %{NaturalDocs::Menu->Indexes()}; + @indexes = sort + { + if ($a eq ::TOPIC_GENERAL()) { return -1; } + elsif ($b eq ::TOPIC_GENERAL()) { return 1; } + else { return (NaturalDocs::Topics->NameOfType($a, 1) cmp NaturalDocs::Topics->NameOfType($b, 1)) }; + } @indexes; + + foreach my $index (@indexes) + { + my ($name, $extra); + if ($index eq ::TOPIC_GENERAL()) + { + $name = 'Everything'; + $extra = ' id=MSearchEverything selected '; + } + else + { $name = $self->ConvertAmpChars(NaturalDocs::Topics->NameOfType($index, 1)); } + + $searchOutput .= + '<option ' . $extra . 'value="' . NaturalDocs::Topics->NameOfType($index, 1, 1) . '">' + . $name + . '</option>'; + }; + + $searchOutput .= + '</select>' + . '</div>'; + }; + + $prebuiltMenus{$outputDirectory} = $titleOutput . $segmentOutput . $searchOutput; + $output .= $titleOutput . $segmentOutput . $searchOutput; + } + else + { $output .= $prebuiltMenus{$outputDirectory}; }; + + + # Highlight the menu selection. + + if ($sourceFile) + { + # Dependency: This depends on how BuildMenuSegment() formats file entries. + my $outputFile = $self->OutputFileOf($sourceFile); + my $tag = '<div class=MFile><a href="' . $self->MakeRelativeURL($outputDirectory, $outputFile) . '">'; + my $tagIndex = index($output, $tag); + + if ($tagIndex != -1) + { + my $endIndex = index($output, '</a>', $tagIndex); + + substr($output, $endIndex, 4, ''); + substr($output, $tagIndex, length($tag), '<div class=MFile id=MSelected>'); + }; + } + elsif ($indexType) + { + # Dependency: This depends on how BuildMenuSegment() formats index entries. + my $outputFile = $self->IndexFileOf($indexType); + my $tag = '<div class=MIndex><a href="' . $self->MakeRelativeURL($outputDirectory, $outputFile) . '">'; + my $tagIndex = index($output, $tag); + + if ($tagIndex != -1) + { + my $endIndex = index($output, '</a>', $tagIndex); + + substr($output, $endIndex, 4, ''); + substr($output, $tagIndex, length($tag), '<div class=MIndex id=MSelected>'); + }; + }; + + + # If the completely expanded menu is too long, collapse all the groups that aren't in the selection hierarchy or near the + # selection. By doing this instead of having them default to closed via CSS, any browser that doesn't support changing this at + # runtime will keep the menu entirely open so that its still usable. + + if ($menuLength > MENU_LENGTH_LIMIT()) + { + my $menuSelectionHierarchy = $self->GetMenuSelectionHierarchy($sourceFile, $indexType); + + my $toExpand = $self->ExpandMenu($sourceFile, $indexType, $menuSelectionHierarchy, $menuRootLength); + + $output .= + + '<script language=JavaScript><!--' . "\n" + + . 'HideAllBut([' . join(', ', @$toExpand) . '], ' . $menuGroupNumber . ');' + + . '// --></script>'; + }; + + $output .= '</div><!--Menu-->'; + + $menuNumbersAndLengthsDone = 1; + + return $output; + }; + + +# +# Function: BuildMenuSegment +# +# A recursive function to build a segment of the menu. *Remember to reset the <Menu Package Variables> before calling this +# for the first time.* +# +# Parameters: +# +# outputDirectory - The output directory the menu is being built for. +# menuSegment - An arrayref specifying the segment of the menu to build. Either pass the menu itself or the contents +# of a group. +# topLevel - Whether the passed segment is the top level segment or not. +# +# Returns: +# +# The array ( menuHTML, length ). +# +# menuHTML - The menu segment in HTML. +# groupLength - The length of the group, *not* including the contents of any subgroups, as computed from the +# <Menu Length Constants>. +# +# Dependencies: +# +# - <BuildMenu()> depends on the way this function formats file and index entries. +# +sub BuildMenuSegment #(outputDirectory, menuSegment, topLevel) + { + my ($self, $outputDirectory, $menuSegment, $topLevel) = @_; + + my $output; + my $groupLength = 0; + + foreach my $entry (@$menuSegment) + { + if ($entry->Type() == ::MENU_GROUP()) + { + my ($entryOutput, $entryLength) = + $self->BuildMenuSegment($outputDirectory, $entry->GroupContent()); + + my $entryNumber; + + if (!$menuNumbersAndLengthsDone) + { + $entryNumber = $menuGroupNumber; + $menuGroupNumber++; + + $menuGroupLengths{$entry} = $entryLength; + $menuGroupNumbers{$entry} = $entryNumber; + } + else + { $entryNumber = $menuGroupNumbers{$entry}; }; + + $output .= + '<div class=MEntry>' + . '<div class=MGroup>' + + . '<a href="javascript:ToggleMenu(\'MGroupContent' . $entryNumber . '\')"' + . ($self->CommandLineOption() eq 'FramedHTML' ? ' target="_self"' : '') . '>' + . $self->StringToHTML($entry->Title()) + . '</a>' + + . '<div class=MGroupContent id=MGroupContent' . $entryNumber . '>' + . $entryOutput + . '</div>' + + . '</div>' + . '</div>'; + + $groupLength += MENU_GROUP_LENGTH; + } + + elsif ($entry->Type() == ::MENU_FILE()) + { + my $targetOutputFile = $self->OutputFileOf($entry->Target()); + + # Dependency: BuildMenu() depends on how this formats file entries. + $output .= + '<div class=MEntry>' + . '<div class=MFile>' + . '<a href="' . $self->MakeRelativeURL($outputDirectory, $targetOutputFile) . '">' + . $self->StringToHTML( $entry->Title(), ADD_HIDDEN_BREAKS) + . '</a>' + . '</div>' + . '</div>'; + + $groupLength += MENU_FILE_LENGTH; + } + + elsif ($entry->Type() == ::MENU_TEXT()) + { + $output .= + '<div class=MEntry>' + . '<div class=MText>' + . $self->StringToHTML( $entry->Title() ) + . '</div>' + . '</div>'; + + $groupLength += MENU_TEXT_LENGTH; + } + + elsif ($entry->Type() == ::MENU_LINK()) + { + $output .= + '<div class=MEntry>' + . '<div class=MLink>' + . '<a href="' . $entry->Target() . '"' . ($self->CommandLineOption() eq 'FramedHTML' ? ' target="_top"' : '') . '>' + . $self->StringToHTML( $entry->Title() ) + . '</a>' + . '</div>' + . '</div>'; + + $groupLength += MENU_LINK_LENGTH; + } + + elsif ($entry->Type() == ::MENU_INDEX()) + { + my $indexFile = $self->IndexFileOf($entry->Target); + + # Dependency: BuildMenu() depends on how this formats index entries. + $output .= + '<div class=MEntry>' + . '<div class=MIndex>' + . '<a href="' . $self->MakeRelativeURL( $outputDirectory, $self->IndexFileOf($entry->Target()) ) . '">' + . $self->StringToHTML( $entry->Title() ) + . '</a>' + . '</div>' + . '</div>'; + + $groupLength += MENU_INDEX_LENGTH; + }; + }; + + + if (!$menuNumbersAndLengthsDone) + { $menuLength += $groupLength; }; + + return ($output, $groupLength); + }; + + +# +# Function: BuildContent +# +# Builds and returns the main page content. +# +# Parameters: +# +# sourceFile - The source <FileName>. +# parsedFile - The parsed source file as an arrayref of <NaturalDocs::Parser::ParsedTopic> objects. +# +# Returns: +# +# The page content in HTML. +# +sub BuildContent #(sourceFile, parsedFile) + { + my ($self, $sourceFile, $parsedFile) = @_; + + $self->ResetToolTips(); + $imageAnchorNumber = 1; + $imageContent = undef; + + my $output = '<div id=Content>'; + my $i = 0; + + while ($i < scalar @$parsedFile) + { + my $anchor = $self->SymbolToHTMLSymbol($parsedFile->[$i]->Symbol()); + + my $scope = NaturalDocs::Topics->TypeInfo($parsedFile->[$i]->Type())->Scope(); + + + # The anchors are closed, but not around the text, so the :hover CSS style won't accidentally kick in. + + my $headerType; + + if ($i == 0) + { $headerType = 'h1'; } + elsif ($scope == ::SCOPE_START() || $scope == ::SCOPE_END()) + { $headerType = 'h2'; } + else + { $headerType = 'h3'; }; + + $output .= + + '<div class="C' . NaturalDocs::Topics->NameOfType($parsedFile->[$i]->Type(), 0, 1) . '">' + . '<div class=CTopic' . ($i == 0 ? ' id=MainTopic' : '') . '>' + + . '<' . $headerType . ' class=CTitle>' + . '<a name="' . $anchor . '"></a>' + . $self->StringToHTML( $parsedFile->[$i]->Title(), ADD_HIDDEN_BREAKS) + . '</' . $headerType . '>'; + + + my $hierarchy; + if (NaturalDocs::Topics->TypeInfo( $parsedFile->[$i]->Type() )->ClassHierarchy()) + { + $hierarchy = $self->BuildClassHierarchy($sourceFile, $parsedFile->[$i]->Symbol()); + }; + + my $summary; + if ($i == 0 || $scope == ::SCOPE_START() || $scope == ::SCOPE_END()) + { + $summary .= $self->BuildSummary($sourceFile, $parsedFile, $i); + }; + + my $hasBody; + if (defined $hierarchy || defined $summary || + defined $parsedFile->[$i]->Prototype() || defined $parsedFile->[$i]->Body()) + { + $output .= '<div class=CBody>'; + $hasBody = 1; + }; + + $output .= $hierarchy; + + if (defined $parsedFile->[$i]->Prototype()) + { + $output .= $self->BuildPrototype($parsedFile->[$i]->Type(), $parsedFile->[$i]->Prototype(), $sourceFile); + }; + + if (defined $parsedFile->[$i]->Body()) + { + $output .= $self->NDMarkupToHTML( $sourceFile, $parsedFile->[$i]->Body(), $parsedFile->[$i]->Symbol(), + $parsedFile->[$i]->Package(), $parsedFile->[$i]->Type(), + $parsedFile->[$i]->Using() ); + }; + + $output .= $summary; + + + if ($hasBody) + { $output .= '</div>'; }; + + $output .= + '</div>' # CTopic + . '</div>' # CType + . "\n\n"; + + $i++; + }; + + $output .= '</div><!--Content-->'; + + return $output; + }; + + +# +# Function: BuildSummary +# +# Builds a summary, either for the entire file or the current class/section. +# +# Parameters: +# +# sourceFile - The source <FileName> the summary appears in. +# +# parsedFile - A reference to the parsed source file. +# +# index - The index into the parsed file to start at. If undef or zero, it builds a summary for the entire file. If it's the +# index of a <TopicType> that starts or ends a scope, it builds a summary for that scope +# +# Returns: +# +# The summary in HTML. +# +sub BuildSummary #(sourceFile, parsedFile, index) + { + my ($self, $sourceFile, $parsedFile, $index) = @_; + my $completeSummary; + + if (!defined $index || $index == 0) + { + $index = 0; + $completeSummary = 1; + } + else + { + # Skip the scope entry. + $index++; + }; + + if ($index + 1 >= scalar @$parsedFile) + { return undef; }; + + + my $scope = NaturalDocs::Topics->TypeInfo($parsedFile->[$index]->Type())->Scope(); + + # Return nothing if there's only one entry. + if (!$completeSummary && ($scope == ::SCOPE_START() || $scope == ::SCOPE_END()) ) + { return undef; }; + + + my $indent = 0; + my $inGroup; + + my $isMarked = 0; + + my $output = + '<!--START_ND_SUMMARY-->' + . '<div class=Summary><div class=STitle>Summary</div>' + + # Not all browsers get table padding right, so we need a div to apply the border. + . '<div class=SBorder>' + . '<table border=0 cellspacing=0 cellpadding=0 class=STable>'; + + while ($index < scalar @$parsedFile) + { + my $topic = $parsedFile->[$index]; + my $scope = NaturalDocs::Topics->TypeInfo($topic->Type())->Scope(); + + if (!$completeSummary && ($scope == ::SCOPE_START() || $scope == ::SCOPE_END()) ) + { last; }; + + + # Remove modifiers as appropriate for the current entry. + + if ($scope == ::SCOPE_START() || $scope == ::SCOPE_END()) + { + $indent = 0; + $inGroup = 0; + $isMarked = 0; + } + elsif ($topic->Type() eq ::TOPIC_GROUP()) + { + if ($inGroup) + { $indent--; }; + + $inGroup = 0; + $isMarked = 0; + }; + + + $output .= + '<tr class="S' . ($index == 0 ? 'Main' : NaturalDocs::Topics->NameOfType($topic->Type(), 0, 1)) + . ($indent ? ' SIndent' . $indent : '') . ($isMarked ? ' SMarked' : '') .'">' + . '<td class=SEntry>'; + + # Add the entry itself. + + my $toolTipProperties; + + # We only want a tooltip here if there's a protoype. Otherwise it's redundant. + + if (defined $topic->Prototype()) + { + my $tooltipID = $self->BuildToolTip($topic->Symbol(), $sourceFile, $topic->Type(), + $topic->Prototype(), $topic->Summary()); + $toolTipProperties = $self->BuildToolTipLinkProperties($tooltipID); + }; + + $output .= + '<a href="#' . $self->SymbolToHTMLSymbol($parsedFile->[$index]->Symbol()) . '" ' . $toolTipProperties . '>' + . $self->StringToHTML( $parsedFile->[$index]->Title(), ADD_HIDDEN_BREAKS) + . '</a>'; + + + $output .= + '</td><td class=SDescription>'; + + if (defined $parsedFile->[$index]->Body()) + { + $output .= $self->NDMarkupToHTML($sourceFile, $parsedFile->[$index]->Summary(), + $parsedFile->[$index]->Symbol(), $parsedFile->[$index]->Package(), + $parsedFile->[$index]->Type(), $parsedFile->[$index]->Using(), + NDMARKUPTOHTML_SUMMARY); + }; + + + $output .= + '</td></tr>'; + + + # Prepare the modifiers for the next entry. + + if ($scope == ::SCOPE_START() || $scope == ::SCOPE_END()) + { + $indent = 1; + $inGroup = 0; + } + elsif ($topic->Type() eq ::TOPIC_GROUP()) + { + if (!$inGroup) + { + $indent++; + $inGroup = 1; + }; + }; + + $isMarked ^= 1; + $index++; + }; + + $output .= + '</table>' + . '</div>' # Body + . '</div>' # Summary + . "<!--END_ND_SUMMARY-->"; + + return $output; + }; + + +# +# Function: BuildPrototype +# +# Builds and returns the prototype as HTML. +# +# Parameters: +# +# type - The <TopicType> the prototype is from. +# prototype - The prototype to format. +# file - The <FileName> the prototype was defined in. +# +# Returns: +# +# The prototype in HTML. +# +sub BuildPrototype #(type, prototype, file) + { + my ($self, $type, $prototype, $file) = @_; + + my $language = NaturalDocs::Languages->LanguageOf($file); + my $prototypeObject = $language->ParsePrototype($type, $prototype); + + my $output; + + if ($prototypeObject->OnlyBeforeParameters()) + { + $output = + # A blockquote to scroll it if it's too long. + '<blockquote>' + # A surrounding table as a hack to make the div form-fit. + . '<table border=0 cellspacing=0 cellpadding=0 class=Prototype><tr><td>' + . $self->ConvertAmpChars($prototypeObject->BeforeParameters()) + . '</td></tr></table>' + . '</blockquote>'; + } + + else + { + my $params = $prototypeObject->Parameters(); + my $beforeParams = $prototypeObject->BeforeParameters(); + my $afterParams = $prototypeObject->AfterParameters(); + + + # Determine what features the prototype has and its length. + + my ($hasType, $hasTypePrefix, $hasNamePrefix, $hasDefaultValue, $hasDefaultValuePrefix); + my $maxParamLength = 0; + + foreach my $param (@$params) + { + my $paramLength = length($param->Name()); + + if ($param->Type()) + { + $hasType = 1; + $paramLength += length($param->Type()) + 1; + }; + if ($param->TypePrefix()) + { + $hasTypePrefix = 1; + $paramLength += length($param->TypePrefix()) + 1; + }; + if ($param->NamePrefix()) + { + $hasNamePrefix = 1; + $paramLength += length($param->NamePrefix()); + }; + if ($param->DefaultValue()) + { + $hasDefaultValue = 1; + + # The length of the default value part is either the longest word, or 1/3 the total, whichever is longer. We do this + # because we don't want parameter lines wrapping to more than three lines, and there's no guarantee that the line will + # wrap at all. There's a small possibility that it could still wrap to four lines with this code, but we don't need to go + # crazy(er) here. + + my $thirdLength = length($param->DefaultValue()) / 3; + + my @words = split(/ +/, $param->DefaultValue()); + my $maxWordLength = 0; + + foreach my $word (@words) + { + if (length($word) > $maxWordLength) + { $maxWordLength = length($word); }; + }; + + $paramLength += ($maxWordLength > $thirdLength ? $maxWordLength : $thirdLength) + 1; + }; + if ($param->DefaultValuePrefix()) + { + $hasDefaultValuePrefix = 1; + $paramLength += length($param->DefaultValuePrefix()) + 1; + }; + + if ($paramLength > $maxParamLength) + { $maxParamLength = $paramLength; }; + }; + + my $useCondensed = (length($beforeParams) + $maxParamLength + length($afterParams) > 80 ? 1 : 0); + my $parameterColumns = 1 + $hasType + $hasTypePrefix + $hasNamePrefix + + $hasDefaultValue + $hasDefaultValuePrefix + $useCondensed; + + $output = + '<blockquote><table border=0 cellspacing=0 cellpadding=0 class=Prototype><tr><td>' + + # Stupid hack to get it to work right in IE. + . '<table border=0 cellspacing=0 cellpadding=0><tr>' + + . '<td class=PBeforeParameters ' . ($useCondensed ? 'colspan=' . $parameterColumns : 'nowrap') . '>' + . $self->ConvertAmpChars($beforeParams); + + if ($beforeParams && $beforeParams !~ /[\(\[\{\<]$/) + { $output .= ' '; }; + + $output .= + '</td>'; + + for (my $i = 0; $i < scalar @$params; $i++) + { + if ($useCondensed) + { + $output .= '</tr><tr><td> </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/ $/ /; + + $output .= + '<td class=PTypePrefix nowrap>' + . $htmlTypePrefix + . '</td>'; + }; + + if ($hasType) + { + $output .= + '<td class=PType nowrap>' + . $self->ConvertAmpChars($params->[$i]->Type()) . ' ' + . '</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%' : '') . '>' + . ' ' . $self->ConvertAmpChars( $typePrefix . $params->[$i]->Type() ) + . '</td>'; + }; + }; + + if ($hasDefaultValuePrefix) + { + $output .= + '<td class=PDefaultValuePrefix>' + + . ' ' . $self->ConvertAmpChars( $params->[$i]->DefaultValuePrefix() ) . ' ' + . '</td>'; + }; + + if ($hasDefaultValue) + { + $output .= + '<td class=PDefaultValue width=100%>' + . ($hasDefaultValuePrefix ? '' : ' ') . $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 .= ' '; }; + + $output .= + '</td>' + . '</tr></table>' + + # Hack. + . '</td></tr></table></blockquote>'; + }; + + return $output; + }; + + +# +# Function: BuildFooter +# +# Builds and returns the HTML footer for the page. +# +# Parameters: +# +# multiline - Whether it should be formatted on multiple lines or not. +# +# Dependencies: +# +# <Builder::HTML::UpdateFile()> and <Builder::HTML::UpdateIndex()> require this section to be surrounded with the exact +# strings "<div id=Footer>" and "</div><!--Footer-->". +# +sub BuildFooter #(bool multiline) + { + my ($self, $multiline) = @_; + + my $footer = NaturalDocs::Menu->Footer(); + my $timestamp = NaturalDocs::Menu->TimeStamp(); + my $divider; + + if ($multiline) + { $divider = '</p><p>'; } + else + { $divider = ' · '; }; + + + my $output = '<div id=Footer>'; + if ($multiline) + { $output .= '<p>'; }; + + if (defined $footer) + { + $footer =~ s/\(c\)/©/gi; + $footer =~ s/\(tm\)/™/gi; + $footer =~ s/\(r\)/®/gi; + + $output .= $footer . $divider; + }; + + if (defined $timestamp) + { + $output .= $timestamp . $divider; + }; + + $output .= + '<a href="' . NaturalDocs::Settings->AppURL() . '">' + . 'Generated by Natural Docs' + . '</a>'; + + if ($multiline) + { $output .= '</p>'; }; + + $output .= + '</div><!--Footer-->'; + + return $output; + }; + + +# +# Function: BuildToolTip +# +# Builds the HTML for a symbol's tooltip and stores it in <tooltipHTML>. +# +# Parameters: +# +# symbol - The target <SymbolString>. +# file - The <FileName> the target's defined in. +# type - The symbol <TopicType>. +# prototype - The target prototype, or undef for none. +# summary - The target summary, or undef for none. +# +# Returns: +# +# If a tooltip is necessary for the link, returns the tooltip ID. If not, returns undef. +# +sub BuildToolTip #(symbol, file, type, prototype, summary) + { + my ($self, $symbol, $file, $type, $prototype, $summary) = @_; + + if (defined $prototype || defined $summary) + { + my $htmlSymbol = $self->SymbolToHTMLSymbol($symbol); + my $number = $tooltipSymbolsToNumbers{$htmlSymbol}; + + if (!defined $number) + { + $number = $tooltipNumber; + $tooltipNumber++; + + $tooltipSymbolsToNumbers{$htmlSymbol} = $number; + + $tooltipHTML .= + '<div class=CToolTip id="tt' . $number . '">' + . '<div class=C' . NaturalDocs::Topics->NameOfType($type, 0, 1) . '>'; + + if (defined $prototype) + { + $tooltipHTML .= $self->BuildPrototype($type, $prototype, $file); + }; + + if (defined $summary) + { + # The fact that we don't have scope or using shouldn't matter because links shouldn't be included in the style anyway. + $summary = $self->NDMarkupToHTML($file, $summary, undef, undef, $type, undef, NDMARKUPTOHTML_TOOLTIP); + $tooltipHTML .= $summary; + }; + + $tooltipHTML .= + '</div>' + . '</div>'; + }; + + return 'tt' . $number; + } + else + { return undef; }; + }; + +# +# Function: BuildToolTips +# +# Builds and returns the tooltips for the page in HTML. +# +sub BuildToolTips + { + my $self = shift; + return "\n<!--START_ND_TOOLTIPS-->\n" . $tooltipHTML . "<!--END_ND_TOOLTIPS-->\n\n"; + }; + +# +# Function: BuildClassHierarchy +# +# Builds and returns a class hierarchy diagram for the passed class, if applicable. +# +# Parameters: +# +# file - The source <FileName>. +# class - The class <SymbolString> to build the hierarchy of. +# +sub BuildClassHierarchy #(file, symbol) + { + my ($self, $file, $symbol) = @_; + + my @parents = NaturalDocs::ClassHierarchy->ParentsOf($symbol); + @parents = sort { ::StringCompare($a, $b) } @parents; + + my @children = NaturalDocs::ClassHierarchy->ChildrenOf($symbol); + @children = sort { ::StringCompare($a, $b) } @children; + + if (!scalar @parents && !scalar @children) + { return undef; }; + + my $output = + '<div class=ClassHierarchy>'; + + if (scalar @parents) + { + $output .='<table border=0 cellspacing=0 cellpadding=0><tr><td>'; + + foreach my $parent (@parents) + { + $output .= $self->BuildClassHierarchyEntry($file, $parent, 'CHParent', 1); + }; + + $output .= '</td></tr></table><div class=CHIndent>'; + }; + + $output .= + '<table border=0 cellspacing=0 cellpadding=0><tr><td>' + . $self->BuildClassHierarchyEntry($file, $symbol, 'CHCurrent', undef) + . '</td></tr></table>'; + + if (scalar @children) + { + $output .= + '<div class=CHIndent>' + . '<table border=0 cellspacing=0 cellpadding=0><tr><td>'; + + if (scalar @children <= 5) + { + for (my $i = 0; $i < scalar @children; $i++) + { $output .= $self->BuildClassHierarchyEntry($file, $children[$i], 'CHChild', 1); }; + } + else + { + for (my $i = 0; $i < 4; $i++) + { $output .= $self->BuildClassHierarchyEntry($file, $children[$i], 'CHChild', 1); }; + + $output .= '<div class=CHChildNote><div class=CHEntry>' . (scalar @children - 4) . ' other children</div></div>'; + }; + + $output .= + '</td></tr></table>' + . '</div>'; + }; + + if (scalar @parents) + { $output .= '</div>'; }; + + $output .= + '</div>'; + + return $output; + }; + + +# +# Function: BuildClassHierarchyEntry +# +# Builds and returns a single class hierarchy entry. +# +# Parameters: +# +# file - The source <FileName>. +# symbol - The class <SymbolString> whose entry is getting built. +# style - The style to apply to the entry, such as <CHParent>. +# link - Whether to build a link for this class or not. When building the selected class' entry, this should be false. It will +# automatically handle whether the symbol is defined or not. +# +sub BuildClassHierarchyEntry #(file, symbol, style, link) + { + my ($self, $file, $symbol, $style, $link) = @_; + + my @identifiers = NaturalDocs::SymbolString->IdentifiersOf($symbol); + my $name = join(NaturalDocs::Languages->LanguageOf($file)->PackageSeparator(), @identifiers); + $name = $self->StringToHTML($name); + + my $output = '<div class=' . $style . '><div class=CHEntry>'; + + if ($link) + { + my $target = NaturalDocs::SymbolTable->Lookup($symbol, $file); + + if (defined $target) + { + my $targetFile; + + if ($target->File() ne $file) + { $targetFile = $self->MakeRelativeURL( $self->OutputFileOf($file), $self->OutputFileOf($target->File()), 1 ); }; + # else leave it undef + + my $targetTooltipID = $self->BuildToolTip($symbol, $target->File(), $target->Type(), + $target->Prototype(), $target->Summary()); + + my $toolTipProperties = $self->BuildToolTipLinkProperties($targetTooltipID); + + $output .= '<a href="' . $targetFile . '#' . $self->SymbolToHTMLSymbol($symbol) . '" ' + . 'class=L' . NaturalDocs::Topics->NameOfType($target->Type(), 0, 1) . ' ' . $toolTipProperties . '>' + . $name . '</a>'; + } + else + { $output .= $name; }; + } + else + { $output .= $name; }; + + $output .= '</div></div>'; + return $output; + }; + + +# +# Function: OpeningBrowserStyles +# +# Returns the JavaScript that will add opening browser styles if necessary. +# +sub OpeningBrowserStyles + { + my $self = shift; + + return + + '<script language=JavaScript><!--' . "\n" + + # IE 4 and 5 don't understand 'undefined', so you can't say '!= undefined'. + . 'if (browserType) {' + . 'document.write("<div class=" + browserType + ">");' + . 'if (browserVer) {' + . 'document.write("<div class=" + browserVer + ">"); }' + . '}' + + . '// --></script>'; + }; + + +# +# Function: ClosingBrowserStyles +# +# Returns the JavaScript that will close browser styles if necessary. +# +sub ClosingBrowserStyles + { + my $self = shift; + + return + + '<script language=JavaScript><!--' . "\n" + + . 'if (browserType) {' + . 'if (browserVer) {' + . 'document.write("</div>"); }' + . 'document.write("</div>");' + . '}' + + . '// --></script>'; + }; + + +# +# Function: StandardComments +# +# Returns the standard HTML comments that should be included in every generated file. This includes <IEWebMark()>, so this +# really is required for proper functionality. +# +sub StandardComments + { + my $self = shift; + + return "\n\n" + + . '<!-- Generated by Natural Docs, version ' . NaturalDocs::Settings->TextAppVersion() . ' -->' . "\n" + . '<!-- ' . NaturalDocs::Settings->AppURL() . ' -->' . "\n\n" + . $self->IEWebMark() . "\n\n"; + }; + + +# +# Function: IEWebMark +# +# Returns the HTML comment necessary to get around the security warnings in IE starting with Windows XP Service Pack 2. +# +# With this mark, the HTML page is treated as if it were in the Internet security zone instead of the Local Machine zone. This +# prevents the lockdown on scripting that causes an error message to appear with each page. +# +# More Information: +# +# - http://www.microsoft.com/technet/prodtechnol/winxppro/maintain/sp2brows.mspx#EHAA +# - http://www.phdcc.com/xpsp2.htm#markoftheweb +# +sub IEWebMark + { + my $self = shift; + + return '<!-- saved from url=(0026)http://www.naturaldocs.org -->'; + }; + + + +############################################################################### +# Group: Index Functions + + +# +# Function: BuildIndexPages +# +# Builds an index file or files. +# +# Parameters: +# +# type - The <TopicType> the index is limited to, or undef for none. +# indexSections - An arrayref of sections, each section being an arrayref <NaturalDocs::SymbolTable::IndexElement> +# objects. The first section is for symbols, the second for numbers, and the rest for A through Z. +# beginIndexPage - All the content of the HTML page up to where the index content should appear. +# endIndexPage - All the content of the HTML page past where the index should appear. +# beginSearchResultsPage - All the content of the HTML page up to where the search results content should appear. +# endSearchResultsPage - All the content of the HTML page past where the search results content should appear. +# +# Returns: +# +# The number of pages in the index. +# +sub BuildIndexPages #(TopicType type, NaturalDocs::SymbolTable::IndexElement[] indexSections, string beginIndexPage, string endIndexPage, string beginSearchResultsPage, string endSearchResultsPage) => int + { + my ($self, $type, $indexSections, $beginIndexPage, $endIndexPage, $beginSearchResultsPage, $endSearchResultsPage) = @_; + + + # Build the content. + + my ($indexHTMLSections, $tooltipHTMLSections, $searchResultsHTMLSections) = $self->BuildIndexSections($indexSections); + + + # Generate the search result pages. + + for (my $i = 0; $i < 28; $i++) + { + if ($searchResultsHTMLSections->[$i]) + { + my $searchResultsFileName = $self->SearchResultsFileOf($type, $searchExtensions[$i]); + + open(INDEXFILEHANDLE, '>' . $searchResultsFileName) + or die "Couldn't create output file " . $searchResultsFileName . ".\n"; + + print INDEXFILEHANDLE + + $beginSearchResultsPage + + . '<div class=SRStatus id=Loading>Loading...</div>' + + . '<table border=0 cellspacing=0 cellpadding=0>' + . $searchResultsHTMLSections->[$i] + . '</table>' + + . '<div class=SRStatus id=Searching>Searching...</div>' + . '<div class=SRStatus id=NoMatches>No Matches</div>' + + . '<script type="text/javascript"><!--' . "\n" + . 'document.getElementById("Loading").style.display="none";' . "\n" + . 'document.getElementById("NoMatches").style.display="none";' . "\n" + + . 'var searchResults = new SearchResults("searchResults", "' . $self->CommandLineOption() . '");' . "\n" + . 'searchResults.Search();' . "\n" + . '--></script>' + + . $endSearchResultsPage; + + close(INDEXFILEHANDLE); + }; + }; + + if (!$self->MadeEmptySearchResultsPage()) + { + my $emptySearchResultsFileName = NaturalDocs::File->JoinPaths( $self->SearchResultsDirectory(), 'NoResults.html' ); + + open(INDEXFILEHANDLE, '>' . $emptySearchResultsFileName) + or die "Couldn't create output file " . $emptySearchResultsFileName . ".\n"; + + print INDEXFILEHANDLE + + $beginSearchResultsPage + . '<div class=SRStatus id=NoMatches>No Matches</div>' + . $endSearchResultsPage; + + close(INDEXFILEHANDLE); + + $self->SetMadeEmptySearchResultsPage(1); + }; + + + # Generate the index pages. + + my $page = 1; + my $pageSize = 0; + my @pageLocations; + + # The maximum page size acceptable before starting a new page. Note that this doesn't include beginPage and endPage, + # because we don't want something like a large menu screwing up the calculations. + use constant PAGESIZE_LIMIT => 50000; + + + # File the pages. + + for (my $i = 0; $i < scalar @$indexHTMLSections; $i++) + { + if (!defined $indexHTMLSections->[$i]) + { next; }; + + $pageSize += length($indexHTMLSections->[$i]) + length($tooltipHTMLSections->[$i]); + $pageLocations[$i] = $page; + + if ($pageSize + length($indexHTMLSections->[$i+1]) + length($tooltipHTMLSections->[$i+1]) > PAGESIZE_LIMIT) + { + $page++; + $pageSize = 0; + }; + }; + + + # Build the pages. + + my $indexFileName; + $page = -1; + my $oldPage = -1; + my $tooltips; + my $firstHeading; + + for (my $i = 0; $i < scalar @$indexHTMLSections; $i++) + { + if (!defined $indexHTMLSections->[$i]) + { next; }; + + $page = $pageLocations[$i]; + + # Switch files if we need to. + + if ($page != $oldPage) + { + if ($oldPage != -1) + { + print INDEXFILEHANDLE '</table>' . $tooltips . $endIndexPage; + close(INDEXFILEHANDLE); + $tooltips = undef; + }; + + $indexFileName = $self->IndexFileOf($type, $page); + + open(INDEXFILEHANDLE, '>' . $indexFileName) + or die "Couldn't create output file " . $indexFileName . ".\n"; + + print INDEXFILEHANDLE $beginIndexPage . $self->BuildIndexNavigationBar($type, $page, \@pageLocations) + . '<table border=0 cellspacing=0 cellpadding=0>'; + + $oldPage = $page; + $firstHeading = 1; + }; + + print INDEXFILEHANDLE + '<tr>' + . '<td class=IHeading' . ($firstHeading ? ' id=IFirstHeading' : '') . '>' + . '<a name="' . $indexAnchors[$i] . '"></a>' + . $indexHeadings[$i] + . '</td>' + . '<td></td>' + . '</tr>' + + . $indexHTMLSections->[$i]; + + $firstHeading = 0; + $tooltips .= $tooltipHTMLSections->[$i]; + }; + + if ($page != -1) + { + print INDEXFILEHANDLE '</table>' . $tooltips . $endIndexPage; + close(INDEXFILEHANDLE); + } + + # Build a dummy page so there's something at least. + else + { + $indexFileName = $self->IndexFileOf($type, 1); + + open(INDEXFILEHANDLE, '>' . $indexFileName) + or die "Couldn't create output file " . $indexFileName . ".\n"; + + print INDEXFILEHANDLE + $beginIndexPage + . $self->BuildIndexNavigationBar($type, 1, \@pageLocations) + . 'There are no entries in the ' . lc( NaturalDocs::Topics->NameOfType($type) ) . ' index.' + . $endIndexPage; + + close(INDEXFILEHANDLE); + }; + + + return $page; + }; + + +# +# Function: BuildIndexSections +# +# Builds and returns the index and search results sections in HTML. +# +# Parameters: +# +# index - An arrayref of sections, each section being an arrayref <NaturalDocs::SymbolTable::IndexElement> objects. +# The first section is for symbols, the second for numbers, and the rest for A through Z. +# +# Returns: +# +# The arrayref ( indexSections, tooltipSections, searchResultsSections ). +# +# Index 0 is the symbols, index 1 is the numbers, and each following index is A through Z. The content of each section +# is its HTML, or undef if there is nothing for that section. +# +sub BuildIndexSections #(NaturalDocs::SymbolTable::IndexElement[] index) => ( string[], string[], string[] ) + { + my ($self, $indexSections) = @_; + + $self->ResetToolTips(); + %searchResultIDs = ( ); + + my $contentSections = [ ]; + my $tooltipSections = [ ]; + my $searchResultsSections = [ ]; + + for (my $section = 0; $section < scalar @$indexSections; $section++) + { + if (defined $indexSections->[$section]) + { + my $total = scalar @{$indexSections->[$section]}; + + for (my $i = 0; $i < $total; $i++) + { + my $id; + + if ($i == 0) + { + if ($total == 1) + { $id = 'IOnlySymbolPrefix'; } + else + { $id = 'IFirstSymbolPrefix'; }; + } + elsif ($i == $total - 1) + { $id = 'ILastSymbolPrefix'; }; + + my ($content, $searchResult) = $self->BuildIndexElement($indexSections->[$section]->[$i], $id); + $contentSections->[$section] .= $content; + $searchResultsSections->[$section] .= $searchResult; + }; + + $tooltipSections->[$section] .= $self->BuildToolTips(); + $self->ResetToolTips(1); + }; + }; + + + return ( $contentSections, $tooltipSections, $searchResultsSections ); + }; + + +# +# Function: BuildIndexElement +# +# Converts a <NaturalDocs::SymbolTable::IndexElement> to HTML and returns it. It will handle all sub-elements automatically. +# +# Parameters: +# +# element - The <NaturalDocs::SymbolTable::IndexElement> to build. +# cssID - The CSS ID to apply to the prefix. +# +# Recursion-Only Parameters: +# +# These parameters are used internally for recursion, and should not be set. +# +# symbol - If the element is below symbol level, the <SymbolString> to use. +# package - If the element is below package level, the package <SymbolString> to use. +# hasPackage - Whether the element is below package level. Is necessary because package may need to be undef. +# +# Returns: +# +# The array ( indexHTML, searchResultHTML ) which is the element in the respective HTML forms. +# +sub BuildIndexElement #(NaturalDocs::SymbolTable::IndexElement element, string cssID, SymbolString symbol, SymbolString package, bool hasPackage) => ( string, string ) + { + my ($self, $element, $cssID, $symbol, $package, $hasPackage) = @_; + + + # If we're doing a file sub-index entry... + + if ($hasPackage) + { + my ($inputDirectory, $relativePath) = NaturalDocs::Settings->SplitFromInputDirectory($element->File()); + + return $self->BuildIndexLink($self->StringToHTML($relativePath, ADD_HIDDEN_BREAKS), $symbol, + $package, $element->File(), $element->Type(), + $element->Prototype(), $element->Summary(), 'IFile'); + } + + + # If we're doing a package sub-index entry... + + elsif (defined $symbol) + + { + my $text; + + if ($element->Package()) + { + $text = NaturalDocs::SymbolString->ToText($element->Package(), $element->PackageSeparator()); + $text = $self->StringToHTML($text, ADD_HIDDEN_BREAKS); + } + else + { $text = 'Global'; }; + + if (!$element->HasMultipleFiles()) + { + return $self->BuildIndexLink($text, $symbol, $element->Package(), $element->File(), $element->Type(), + $element->Prototype(), $element->Summary(), 'IParent'); + } + + else + { + my $indexHTML = + '<span class=IParent>' + . $text + . '</span>' + . '<div class=ISubIndex>'; + + my $searchResultHTML = $indexHTML; + + my $fileElements = $element->File(); + foreach my $fileElement (@$fileElements) + { + my ($i, $s) = $self->BuildIndexElement($fileElement, $cssID, $symbol, $element->Package(), 1); + $indexHTML .= $i; + $searchResultHTML .= $s; + }; + + $indexHTML .= '</div>'; + $searchResultHTML .= '</div>'; + + return ($indexHTML, $searchResultHTML); + }; + } + + + # If we're doing a top-level symbol entry... + + else + { + my $symbolText = $self->StringToHTML($element->SortableSymbol(), ADD_HIDDEN_BREAKS); + my $symbolPrefix = $self->StringToHTML($element->IgnoredPrefix()); + my $searchResultID = $self->StringToSearchResultID($element->SortableSymbol()); + + my $indexHTML = + '<tr>' + . '<td class=ISymbolPrefix' . ($cssID ? ' id=' . $cssID : '') . '>' + . ($symbolPrefix || ' ') + . '</td><td class=IEntry>'; + + my $searchResultsHTML = + '<div class=SRResult id=' . $searchResultID . '><div class=IEntry>'; + + if ($symbolPrefix) + { $searchResultsHTML .= '<span class=ISymbolPrefix>' . $symbolPrefix . '</span>'; }; + + if (!$element->HasMultiplePackages()) + { + my $packageText; + + if (defined $element->Package()) + { + $packageText = NaturalDocs::SymbolString->ToText($element->Package(), $element->PackageSeparator()); + $packageText = $self->StringToHTML($packageText, ADD_HIDDEN_BREAKS); + }; + + if (!$element->HasMultipleFiles()) + { + my ($i, $s) = + $self->BuildIndexLink($symbolText, $element->Symbol(), $element->Package(), $element->File(), + $element->Type(), $element->Prototype(), $element->Summary(), 'ISymbol'); + $indexHTML .= $i; + $searchResultsHTML .= $s; + + if (defined $packageText) + { + $indexHTML .= + ', <span class=IParent>' + . $packageText + . '</span>'; + + $searchResultsHTML .= + ', <span class=IParent>' + . $packageText + . '</span>'; + }; + } + else # hasMultipleFiles but not multiplePackages + { + $indexHTML .= + '<span class=ISymbol>' + . $symbolText + . '</span>'; + + $searchResultsHTML .= + q{<a href="javascript:searchResults.Toggle('} . $searchResultID . q{')" class=ISymbol>} + . $symbolText + . '</a>'; + + my $output; + + if (defined $packageText) + { + $output .= + ', <span class=IParent>' + . $packageText + . '</span>'; + }; + + $output .= + '<div class=ISubIndex>'; + + $indexHTML .= $output; + $searchResultsHTML .= $output; + + my $fileElements = $element->File(); + foreach my $fileElement (@$fileElements) + { + my ($i, $s) = $self->BuildIndexElement($fileElement, $cssID, $element->Symbol(), $element->Package(), 1); + $indexHTML .= $i; + $searchResultsHTML .= $s; + }; + + $indexHTML .= '</div>'; + $searchResultsHTML .= '</div>'; + }; + } + + else # hasMultiplePackages + { + $indexHTML .= + '<span class=ISymbol>' + . $symbolText + . '</span>' + . '<div class=ISubIndex>'; + + $searchResultsHTML .= + q{<a href="javascript:searchResults.Toggle('} . $searchResultID . q{')" class=ISymbol>} + . $symbolText + . '</a>' + . '<div class=ISubIndex>'; + + my $packageElements = $element->Package(); + foreach my $packageElement (@$packageElements) + { + my ($i, $s) = $self->BuildIndexElement($packageElement, $cssID, $element->Symbol()); + $indexHTML .= $i; + $searchResultsHTML .= $s; + }; + + $indexHTML .= '</div>'; + $searchResultsHTML .= '</div>'; + }; + + $indexHTML .= '</td></tr>'; + $searchResultsHTML .= '</div></div>'; + + return ($indexHTML, $searchResultsHTML); + }; + }; + + +# +# Function: BuildIndexLink +# +# Builds and returns the HTML associated with an index link. The HTML will be the a href tag, the text, and the closing tag. +# +# Parameters: +# +# text - The text of the link *in HTML*. Use <IndexSymbolToHTML()> if necessary. +# symbol - The partial <SymbolString> to link to. +# package - The package <SymbolString> of the symbol. +# file - The <FileName> the symbol is defined in. +# type - The <TopicType> of the symbol. +# prototype - The prototype of the symbol, or undef if none. +# summary - The summary of the symbol, or undef if none. +# style - The CSS style to apply to the link. +# +# Returns: +# +# The array ( indexHTML, searchResultHTML ) which is the link in the respective forms. +# +sub BuildIndexLink #(string text, SymbolString symbol, SymbolString package, FileName file, TopicType type, string prototype, string summary, string style) => ( string, string ) + { + my ($self, $text, $symbol, $package, $file, $type, $prototype, $summary, $style) = @_; + + $symbol = NaturalDocs::SymbolString->Join($package, $symbol); + + my $targetTooltipID = $self->BuildToolTip($symbol, $file, $type, $prototype, $summary); + my $toolTipProperties = $self->BuildToolTipLinkProperties($targetTooltipID); + + my $indexHTML = '<a href="' . $self->MakeRelativeURL( $self->IndexDirectory(), $self->OutputFileOf($file) ) + . '#' . $self->SymbolToHTMLSymbol($symbol) . '" ' . $toolTipProperties . ' ' + . 'class=' . $style . '>' . $text . '</a>'; + my $searchResultHTML = '<a href="' . $self->MakeRelativeURL( $self->SearchResultsDirectory(), $self->OutputFileOf($file) ) + . '#' . $self->SymbolToHTMLSymbol($symbol) . '" ' + . ($self->CommandLineOption eq 'HTML' ? 'target=_parent ' : '') + . 'class=' . $style . '>' . $text . '</a>'; + + return ($indexHTML, $searchResultHTML); + }; + + +# +# Function: BuildIndexNavigationBar +# +# Builds a navigation bar for a page of the index. +# +# Parameters: +# +# type - The <TopicType> of the index, or undef for general. +# page - The page of the index the navigation bar is for. +# locations - An arrayref of the locations of each section. Index 0 is for the symbols, index 1 for the numbers, and the rest +# for each letter. The values are the page numbers where the sections are located. +# +sub BuildIndexNavigationBar #(type, page, locations) + { + my ($self, $type, $page, $locations) = @_; + + my $output = '<div class=INavigationBar>'; + + for (my $i = 0; $i < scalar @indexHeadings; $i++) + { + if ($i != 0) + { $output .= ' · '; }; + + if (defined $locations->[$i]) + { + $output .= '<a href="'; + + if ($locations->[$i] != $page) + { $output .= $self->RelativeIndexFileOf($type, $locations->[$i]); }; + + $output .= '#' . $indexAnchors[$i] . '">' . $indexHeadings[$i] . '</a>'; + } + else + { + $output .= $indexHeadings[$i]; + }; + }; + + $output .= '</div>'; + + return $output; + }; + + + +############################################################################### +# Group: File Functions + + +# +# Function: PurgeIndexFiles +# +# Removes all or some of the output files for an index. +# +# Parameters: +# +# type - The index <TopicType>. +# indexSections - An arrayref of sections, each section being an arrayref <NaturalDocs::SymbolTable::IndexElement> +# objects. The first section is for symbols, the second for numbers, and the rest for A through Z. May be +# undef. +# startingPage - If defined, only pages starting with this number will be removed. Otherwise all pages will be removed. +# +sub PurgeIndexFiles #(TopicType type, optional NaturalDocs::SymbolTable::IndexElement[] indexSections, optional int startingPage) + { + my ($self, $type, $indexSections, $page) = @_; + + # First the regular index pages. + + if (!defined $page) + { $page = 1; }; + + for (;;) + { + my $file = $self->IndexFileOf($type, $page); + + if (-e $file) + { + unlink($file); + $page++; + } + else + { + last; + }; + }; + + + # Next the search results. + + for (my $i = 0; $i < 28; $i++) + { + if (!$indexSections || !$indexSections->[$i]) + { + my $file = $self->SearchResultsFileOf($type, $searchExtensions[$i]); + + if (-e $file) + { unlink($file); }; + }; + }; + }; + + +# +# Function: OutputFileOf +# +# Returns the output file name of the source file. Will be undef if it is not a file from a valid input directory. +# +sub OutputFileOf #(sourceFile) + { + my ($self, $sourceFile) = @_; + + my ($inputDirectory, $relativeSourceFile) = NaturalDocs::Settings->SplitFromInputDirectory($sourceFile); + if (!defined $inputDirectory) + { return undef; }; + + my $outputDirectory = NaturalDocs::Settings->OutputDirectoryOf($self); + my $inputDirectoryName = NaturalDocs::Settings->InputDirectoryNameOf($inputDirectory); + + $outputDirectory = NaturalDocs::File->JoinPaths( $outputDirectory, + 'files' . ($inputDirectoryName != 1 ? $inputDirectoryName : ''), 1 ); + + # We need to change any extensions to dashes because Apache will think file.pl.html is a script. + # We also need to add a dash if the file doesn't have an extension so there'd be no conflicts with index.html, + # FunctionIndex.html, etc. + + if (!($relativeSourceFile =~ tr/./-/)) + { $relativeSourceFile .= '-'; }; + + $relativeSourceFile =~ tr/ &?(){};/_/; + $relativeSourceFile .= '.html'; + + return NaturalDocs::File->JoinPaths($outputDirectory, $relativeSourceFile); + }; + + +# +# Function: OutputImageOf +# +# Returns the output image file name of the source image file. Will be undef if it is not a file from a valid input directory. +# +sub OutputImageOf #(sourceImageFile) + { + my ($self, $sourceImageFile) = @_; + + my $outputDirectory = NaturalDocs::Settings->OutputDirectoryOf($self); + my $topLevelDirectory; + + my ($inputDirectory, $relativeImageFile) = NaturalDocs::Settings->SplitFromInputDirectory($sourceImageFile); + + if (defined $inputDirectory) + { + my $inputDirectoryName = NaturalDocs::Settings->InputDirectoryNameOf($inputDirectory); + $topLevelDirectory = 'files' . ($inputDirectoryName != 1 ? $inputDirectoryName : ''); + } + else + { + ($inputDirectory, $relativeImageFile) = NaturalDocs::Settings->SplitFromImageDirectory($sourceImageFile); + + if (!defined $inputDirectory) + { return undef; }; + + my $inputDirectoryName = NaturalDocs::Settings->ImageDirectoryNameOf($inputDirectory); + $topLevelDirectory = 'images' . ($inputDirectoryName != 1 ? $inputDirectoryName : ''); + } + + + $outputDirectory = NaturalDocs::File->JoinPaths($outputDirectory, $topLevelDirectory, 1); + + $relativeImageFile =~ tr/ /_/; + + return NaturalDocs::File->JoinPaths($outputDirectory, $relativeImageFile); + }; + + +# +# Function: IndexDirectory +# +# Returns the directory of the index files. +# +sub IndexDirectory + { + my $self = shift; + return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->OutputDirectoryOf($self), 'index', 1); + }; + + +# +# Function: IndexFileOf +# +# Returns the output file name of the index file. +# +# Parameters: +# +# type - The <TopicType> of the index. +# page - The page number. Undef is the same as one. +# +sub IndexFileOf #(type, page) + { + my ($self, $type, $page) = @_; + return NaturalDocs::File->JoinPaths( $self->IndexDirectory(), $self->RelativeIndexFileOf($type, $page) ); + }; + + +# +# Function: RelativeIndexFileOf +# +# Returns the output file name of the index file, relative to other index files. +# +# Parameters: +# +# type - The <TopicType> of the index. +# page - The page number. Undef is the same as one. +# +sub RelativeIndexFileOf #(type, page) + { + my ($self, $type, $page) = @_; + return NaturalDocs::Topics->NameOfType($type, 1, 1) . (defined $page && $page != 1 ? $page : '') . '.html'; + }; + + +# +# Function: SearchResultsDirectory +# +# Returns the directory of the search results files. +# +sub SearchResultsDirectory + { + my $self = shift; + return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->OutputDirectoryOf($self), 'search', 1); + }; + + +# +# Function: SearchResultsFileOf +# +# Returns the output file name of the search result file. +# +# Parameters: +# +# type - The <TopicType> of the index. +# extra - The string to add to the end of the file name, such as "A" or "Symbols". +# +sub SearchResultsFileOf #(TopicType type, string extra) + { + my ($self, $type, $extra) = @_; + + my $fileName = NaturalDocs::Topics->NameOfType($type, 1, 1) . $extra . '.html'; + + return NaturalDocs::File->JoinPaths( $self->SearchResultsDirectory(), $fileName ); + }; + + +# +# Function: CSSDirectory +# +# Returns the directory of the CSS files. +# +sub CSSDirectory + { + my $self = shift; + return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->OutputDirectoryOf($self), 'styles', 1); + }; + + +# +# Function: MainCSSFile +# +# Returns the location of the main CSS file. +# +sub MainCSSFile + { + my $self = shift; + return NaturalDocs::File->JoinPaths( $self->CSSDirectory(), 'main.css' ); + }; + + +# +# Function: JavaScriptDirectory +# +# Returns the directory of the JavaScript files. +# +sub JavaScriptDirectory + { + my $self = shift; + return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->OutputDirectoryOf($self), 'javascript', 1); + }; + + +# +# Function: MainJavaScriptFile +# +# Returns the location of the main JavaScript file. +# +sub MainJavaScriptFile + { + my $self = shift; + return NaturalDocs::File->JoinPaths( $self->JavaScriptDirectory(), 'main.js' ); + }; + + +# +# Function: SearchDataJavaScriptFile +# +# Returns the location of the search data JavaScript file. +# +sub SearchDataJavaScriptFile + { + my $self = shift; + return NaturalDocs::File->JoinPaths( $self->JavaScriptDirectory(), 'searchdata.js' ); + }; + + + +############################################################################### +# Group: Support Functions + + +# +# Function: IndexTitleOf +# +# Returns the page title of the index file. +# +# Parameters: +# +# type - The type of index. +# +sub IndexTitleOf #(type) + { + my ($self, $type) = @_; + + return ($type eq ::TOPIC_GENERAL() ? '' : NaturalDocs::Topics->NameOfType($type) . ' ') . 'Index'; + }; + +# +# Function: MakeRelativeURL +# +# Returns a relative path between two files in the output tree and returns it in URL format. +# +# Parameters: +# +# baseFile - The base <FileName> in local format, *not* in URL format. +# targetFile - The target <FileName> of the link in local format, *not* in URL format. +# baseHasFileName - Whether baseFile has a file name attached or is just a path. +# +# Returns: +# +# The relative URL to the target. +# +sub MakeRelativeURL #(FileName baseFile, FileName targetFile, bool baseHasFileName) -> string relativeURL + { + my ($self, $baseFile, $targetFile, $baseHasFileName) = @_; + + if ($baseHasFileName) + { $baseFile = NaturalDocs::File->NoFileName($baseFile) }; + + my $relativePath = NaturalDocs::File->MakeRelativePath($baseFile, $targetFile); + + return $self->ConvertAmpChars( NaturalDocs::File->ConvertToURL($relativePath) ); + }; + +# +# Function: StringToHTML +# +# Converts a text string to HTML. Does not apply paragraph tags or accept formatting tags. +# +# Parameters: +# +# string - The string to convert. +# addHiddenBreaks - Whether to add hidden breaks to the string. You can use <ADD_HIDDEN_BREAKS> for this parameter +# if you want to make the calling code clearer. +# +# Returns: +# +# The string in HTML. +# +sub StringToHTML #(string, addHiddenBreaks) + { + my ($self, $string, $addHiddenBreaks) = @_; + + $string =~ s/&/&/g; + $string =~ s/</</g; + $string =~ s/>/>/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/^\'/‘/gm; + $string =~ s/([\ \(\[\{])\'/$1‘/g; + $string =~ s/\'/’/g; + + $string =~ s/^\"/“/gm; + $string =~ s/([\ \(\[\{])\"/$1“/g; + $string =~ s/\"/”/g; + + # Me likey the double spaces too. As you can probably tell, I like print-formatting better than web-formatting. The indented + # paragraphs without blank lines in between them do become readable when you have fancy quotes and double spaces too. + $string = $self->AddDoubleSpaces($string); + + if ($addHiddenBreaks) + { $string = $self->AddHiddenBreaks($string); }; + + return $string; + }; + + +# +# Function: SymbolToHTMLSymbol +# +# Converts a <SymbolString> to a HTML symbol, meaning one that is safe to include in anchor and link tags. You don't need +# to pass the result to <ConvertAmpChars()>. +# +sub SymbolToHTMLSymbol #(symbol) + { + my ($self, $symbol) = @_; + + my @identifiers = NaturalDocs::SymbolString->IdentifiersOf($symbol); + my $htmlSymbol = join('.', @identifiers); + + # If only Mozilla was nice about putting special characters in URLs like IE and Opera are, I could leave spaces in and replace + # "<>& with their amp chars. But alas, Mozilla shows them as %20, etc. instead. It would have made for nice looking URLs. + $htmlSymbol =~ tr/ \"<>\?&%/_/d; + + return $htmlSymbol; + }; + + +# +# Function: StringToSearchResultID +# +# Takes a text string and translates it into something that can be used as a CSS ID. +# +# Parameters: +# +# string - The string to convert +# dontIncrement - If set, it reuses the last generated ID. Otherwise it generates a new one if it matches a previously +# generated one in a case-insensitive way. +# +sub StringToSearchResultID #(string string, bool dontIncrement = 0) => string + { + my ($self, $string, $dontIncrement) = @_; + + $string =~ s/\_/_und/g; + $string =~ s/ +/_spc/g; + + my %translation = ( '~' => '_til', '!' => '_exc', '@' => '_att', '#' => '_num', '$' => '_dol', '%' => '_pct', '^' => '_car', + '&' => '_amp', '*' => '_ast', '(' => '_lpa', ')' => '_rpa', '-' => '_min', '+' => '_plu', '=' => '_equ', + '{' => '_lbc', '}' => '_rbc', '[' => '_lbk', ']' => '_rbk', ':' => '_col', ';' => '_sco', '"' => '_quo', + '\'' => '_apo', '<' => '_lan', '>' => '_ran', ',' => '_com', '.' => '_per', '?' => '_que', '/' => '_sla' ); + + $string =~ s/([\~\!\@\#\$\%\^\&\*\(\)\-\+\=\{\}\[\]\:\;\"\'\<\>\,\.\?\/])/$translation{$1}/ge; + $string =~ s/[^a-z0-9_]/_zzz/gi; + + my $number = $searchResultIDs{lc($string)}; + + if (!$number) + { $number = 1; } + elsif (!$dontIncrement) + { $number++; }; + + $searchResultIDs{lc($string)} = $number; + + return 'SR' . ($number == 1 ? '' : $number) . '_' . $string; + }; + + +# +# Function: NDMarkupToHTML +# +# Converts a block of <NDMarkup> to HTML. +# +# Parameters: +# +# sourceFile - The source <FileName> the <NDMarkup> appears in. +# text - The <NDMarkup> text to convert. +# symbol - The topic <SymbolString> the <NDMarkup> appears in. +# package - The package <SymbolString> the <NDMarkup> appears in. +# type - The <TopicType> the <NDMarkup> appears in. +# using - An arrayref of scope <SymbolStrings> the <NDMarkup> also has access to, or undef if none. +# style - Set to one of the <NDMarkupToHTML Styles> or leave undef for general. +# +# Returns: +# +# The text in HTML. +# +sub NDMarkupToHTML #(sourceFile, text, symbol, package, type, using, style) + { + my ($self, $sourceFile, $text, $symbol, $package, $type, $using, $style) = @_; + + my $dlSymbolBehavior; + + if ($type == ::TOPIC_ENUMERATION()) + { $dlSymbolBehavior = NaturalDocs::Languages->LanguageOf($sourceFile)->EnumValues(); } + elsif (NaturalDocs::Topics->TypeInfo($type)->Scope() == ::SCOPE_ALWAYS_GLOBAL()) + { $dlSymbolBehavior = ::ENUM_GLOBAL(); } + else + { $dlSymbolBehavior = ::ENUM_UNDER_PARENT(); }; + + my $output; + my $inCode; + + my @splitText = split(/(<\/?code>)/, $text); + + while (scalar @splitText) + { + $text = shift @splitText; + + if ($text eq '<code>') + { + $output .= '<blockquote><pre>'; + $inCode = 1; + } + elsif ($text eq '</code>') + { + $output .= '</pre></blockquote>'; + $inCode = undef; + } + elsif ($inCode) + { + # Leave line breaks in. + $output .= $text; + } + else + { + # Format non-code text. + + # Convert linked images. + if ($text =~ /<img mode=\"link\"/) + { + if ($style == NDMARKUPTOHTML_GENERAL) + { + # Split by tags we would want to see the linked images appear after. For example, an image link appearing in + # the middle of a paragraph would appear after the end of that paragraph. + my @imageBlocks = split(/(<p>.*?<\/p>|<dl>.*?<\/dl>|<ul>.*?<\/ul>)/, $text); + $text = undef; + + foreach my $imageBlock (@imageBlocks) + { + $imageBlock =~ s{<img mode=\"link\" target=\"([^\"]*)\" original=\"([^\"]*)\">} + {$self->BuildImage($sourceFile, 'link', $1, $2)}ge; + + $text .= $imageBlock . $imageContent; + $imageContent = undef; + }; + } + + # Use only the text for tooltips and summaries. + else + { + $text =~ s{<img mode=\"link\" target=\"[^\"]*\" original=\"([^\"]*)\">}{$1}g; + }; + }; + + # Convert quotes to fancy quotes. This has to be done before links because some of them may have JavaScript + # attributes that use the apostrophe character. + $text =~ s/^\'/‘/gm; + $text =~ s/([\ \(\[\{])\'/$1‘/g; + $text =~ s/\'/’/g; + + $text =~ s/^"/“/gm; + $text =~ s/([\ \(\[\{])"/$1“/g; + $text =~ s/"/”/g; + + # Resolve and convert links, except for tooltips. + if ($style != NDMARKUPTOHTML_TOOLTIP) + { + $text =~ s{<link target=\"([^\"]*)\" name=\"([^\"]*)\" original=\"([^\"]*)\">} + {$self->BuildTextLink($1, $2, $3, $package, $using, $sourceFile)}ge; + $text =~ s/<url target=\"([^\"]*)\" name=\"([^\"]*)\">/$self->BuildURLLink($1, $2)/ge; + } + else + { + $text =~ s{<link target=\"[^\"]*\" name=\"([^\"]*)\" original=\"[^\"]*\">}{$1}g; + $text =~ s{<url target=\"[^\"]*\" name=\"([^\"]*)\">}{$1}g; + }; + + # We do full e-mail links anyway just so the obfuscation remains. + $text =~ s/<email target=\"([^\"]*)\" name=\"([^\"]*)\">/$self->BuildEMailLink($1, $2)/ge; + + + # Convert inline images, but only for the general style. + if ($style == NDMARKUPTOHTML_GENERAL) + { + $text =~ s{<img mode=\"inline\" target=\"([^\"]*)\" original=\"([^\"]*)\">} + {$self->BuildImage($sourceFile, 'inline', $1, $2)}ge; + } + else + { + $text =~ s{<img mode=\"inline\" target=\"[^\"]*\" original=\"([^\"]*)\">}{$1}g; + }; + + # Copyright symbols. Prevent conversion when part of (a), (b), (c) lists. + if ($text !~ /\(a\)/i) + { $text =~ s/\(c\)/©/gi; }; + + # Trademark symbols. + $text =~ s/\(tm\)/™/gi; + $text =~ s/\(r\)/®/gi; + + # Add double spaces too. + $text = $self->AddDoubleSpaces($text); + + # Headings + $text =~ s/<h>/<h4 class=CHeading>/g; + $text =~ s/<\/h>/<\/h4>/g; + + # Description Lists + $text =~ s/<dl>/<table border=0 cellspacing=0 cellpadding=0 class=CDescriptionList>/g; + $text =~ s/<\/dl>/<\/table>/g; + + $text =~ s/<de>/<tr><td class=CDLEntry>/g; + $text =~ s/<\/de>/<\/td>/g; + + if ($dlSymbolBehavior == ::ENUM_GLOBAL()) + { $text =~ s/<ds>([^<]+)<\/ds>/$self->MakeDescriptionListSymbol(undef, $1)/ge; } + elsif ($dlSymbolBehavior == ::ENUM_UNDER_PARENT()) + { $text =~ s/<ds>([^<]+)<\/ds>/$self->MakeDescriptionListSymbol($package, $1)/ge; } + else # ($dlSymbolBehavior == ::ENUM_UNDER_TYPE()) + { $text =~ s/<ds>([^<]+)<\/ds>/$self->MakeDescriptionListSymbol($symbol, $1)/ge; } + + sub MakeDescriptionListSymbol #(package, text) + { + my ($self, $package, $text) = @_; + + $text = NaturalDocs::NDMarkup->RestoreAmpChars($text); + my $symbol = NaturalDocs::SymbolString->FromText($text); + + if (defined $package) + { $symbol = NaturalDocs::SymbolString->Join($package, $symbol); }; + + return + '<tr>' + . '<td class=CDLEntry>' + # The anchors are closed, but not around the text, to prevent the :hover CSS style from kicking in. + . '<a name="' . $self->SymbolToHTMLSymbol($symbol) . '"></a>' + . $text + . '</td>'; + }; + + $text =~ s/<dd>/<td class=CDLDescription>/g; + $text =~ s/<\/dd>/<\/td><\/tr>/g; + + $output .= $text; + }; + }; + + return $output; + }; + + +# +# Function: BuildTextLink +# +# Creates a HTML link to a symbol, if it exists. +# +# Parameters: +# +# target - The link text. +# name - The link name. +# original - The original text as it appears in the source. +# package - The package <SymbolString> the link appears in, or undef if none. +# using - An arrayref of additional scope <SymbolStrings> the link has access to, or undef if none. +# sourceFile - The <FileName> the link appears in. +# +# Target, name, and original are assumed to still have <NDMarkup> amp chars. +# +# Returns: +# +# The link in HTML, including tags. If the link doesn't resolve to anything, returns the HTML that should be substituted for it. +# +sub BuildTextLink #(target, name, original, package, using, sourceFile) + { + my ($self, $target, $name, $original, $package, $using, $sourceFile) = @_; + + my $plainTarget = $self->RestoreAmpChars($target); + + my $symbol = NaturalDocs::SymbolString->FromText($plainTarget); + my $symbolTarget = NaturalDocs::SymbolTable->References(::REFERENCE_TEXT(), $symbol, $package, $using, $sourceFile); + + if (defined $symbolTarget) + { + my $symbolTargetFile; + + if ($symbolTarget->File() ne $sourceFile) + { + $symbolTargetFile = $self->MakeRelativeURL( $self->OutputFileOf($sourceFile), + $self->OutputFileOf($symbolTarget->File()), 1 ); + }; + # else leave it undef + + my $symbolTargetTooltipID = $self->BuildToolTip($symbolTarget->Symbol(), $sourceFile, $symbolTarget->Type(), + $symbolTarget->Prototype(), $symbolTarget->Summary()); + + my $toolTipProperties = $self->BuildToolTipLinkProperties($symbolTargetTooltipID); + + return '<a href="' . $symbolTargetFile . '#' . $self->SymbolToHTMLSymbol($symbolTarget->Symbol()) . '" ' + . 'class=L' . NaturalDocs::Topics->NameOfType($symbolTarget->Type(), 0, 1) . ' ' . $toolTipProperties . '>' + . $name + . '</a>'; + } + else + { + return $original; + }; + }; + + +# +# Function: BuildURLLink +# +# Creates a HTML link to an external URL. Long URLs will have hidden breaks to allow them to wrap. +# +# Parameters: +# +# target - The URL to link to. +# name - The label of the link. +# +# Both are assumed to still have <NDMarkup> amp chars. +# +# Returns: +# +# The HTML link, complete with tags. +# +sub BuildURLLink #(target, name) + { + my ($self, $target, $name) = @_; + + # Don't restore amp chars on the target. + + if (length $name < 50 || $name ne $target) + { return '<a href="' . $target . '" class=LURL target=_top>' . $name . '</a>'; }; + + my @segments = split(/([\,\/]|&)/, $target); + my $output = '<a href="' . $target . '" class=LURL target=_top>'; + + # Get past the first batch of slashes, since we don't want to break on things like http://. + + $output .= $segments[0]; + + my $i = 1; + while ($i < scalar @segments && ($segments[$i] eq '/' || !$segments[$i])) + { + $output .= $segments[$i]; + $i++; + }; + + # Now break on each one of those symbols. + + while ($i < scalar @segments) + { + if ($segments[$i] eq ',' || $segments[$i] eq '/' || $segments[$i] eq '&') + { $output .= '<wbr>'; }; + + $output .= $segments[$i]; + $i++; + }; + + $output .= '</a>'; + return $output; + }; + + +# +# Function: BuildEMailLink +# +# Creates a HTML link to an e-mail address. The address will be transparently munged to protect it (hopefully) from spambots. +# +# Parameters: +# +# target - The e-mail address. +# name - The label of the link. +# +# Both are assumed to still have <NDMarkup> amp chars. +# +# Returns: +# +# The HTML e-mail link, complete with tags. +# +sub BuildEMailLink #(target, name) + { + my ($self, $target, $name) = @_; + my @splitAddress; + + + # Hack the address up. We want two user pieces and two host pieces. + + my ($user, $host) = split(/\@/, $self->RestoreAmpChars($target)); + + my $userSplit = length($user) / 2; + + push @splitAddress, NaturalDocs::NDMarkup->ConvertAmpChars( substr($user, 0, $userSplit) ); + push @splitAddress, NaturalDocs::NDMarkup->ConvertAmpChars( substr($user, $userSplit) ); + + push @splitAddress, '@'; + + my $hostSplit = length($host) / 2; + + push @splitAddress, NaturalDocs::NDMarkup->ConvertAmpChars( substr($host, 0, $hostSplit) ); + push @splitAddress, NaturalDocs::NDMarkup->ConvertAmpChars( substr($host, $hostSplit) ); + + + # Now put it back together again. We'll use spans to split the text transparently and JavaScript to split and join the link. + + my $output = + "<a href=\"#\" onClick=\"location.href='mai' + 'lto:' + '" . join("' + '", @splitAddress) . "'; return false;\" class=LEMail>"; + + if ($name eq $target) + { + $output .= + $splitAddress[0] . '<span style="display: none">.nosp@m.</span>' . $splitAddress[1] + . '<span>@</span>' + . $splitAddress[3] . '<span style="display: none">.nosp@m.</span>' . $splitAddress[4]; + } + else + { $output .= $name; }; + + $output .= '</a>'; + return $output; + }; + + +# +# Function: BuildImage +# +# Builds the HTML for an image. +# +# Parameters: +# +# sourceFile - The source <FileName> this image appears in. +# mode - Either "inline" or "link". +# target - The target. +# original - The original text. +# +# All are assumed to still have <NDMarkup> amp chars. +# +# Returns: +# +# The result in HTML. If the mode was "link", the target image's HTML is added to <imageContent>. +# +sub BuildImage #(sourceFile, mode, target, original) + { + my ($self, $sourceFile, $mode, $target, $original) = @_; + + my $targetNoAmp = $self->RestoreAmpChars($target); + + my $image = NaturalDocs::ImageReferenceTable->GetReferenceTarget($sourceFile, $targetNoAmp); + + if ($image) + { + my ($width, $height) = NaturalDocs::Project->ImageFileDimensions($image); + + if ($mode eq 'inline') + { + return + '<img src="' . $self->MakeRelativeURL($self->OutputFileOf($sourceFile), + $self->OutputImageOf($image), 1) . '"' + + . ($width && $height ? ' width="' . $width . '" height="' . $height . '"' : '') + . '>'; + } + else # link + { + # Make the text a little more friendly in the output by removing any folders and file extensions. + # (see images/Table1.gif) will be turned into (see Table1). + my $originalNoAmp = $self->RestoreAmpChars($original); + my $targetIndex = index($originalNoAmp, $targetNoAmp); + my ($shortTarget, $shortTargetNoAmp, $shortOriginal); + + if ($targetIndex != -1) + { + $shortTargetNoAmp = (NaturalDocs::File->SplitPath($targetNoAmp))[2]; + $shortTargetNoAmp = NaturalDocs::File->NoExtension($shortTargetNoAmp); + + substr($originalNoAmp, $targetIndex, length($targetNoAmp), $shortTargetNoAmp); + + $shortOriginal = NaturalDocs::NDMarkup->ConvertAmpChars($originalNoAmp); + $shortTarget = NaturalDocs::NDMarkup->ConvertAmpChars($shortTargetNoAmp); + }; + + my $output = + '<a href="#Image' . $imageAnchorNumber . '" class=CImageLink>' + . ($shortOriginal || $original) + . '</a>'; + + $imageContent .= + '<blockquote>' + . '<div class=CImage>' + . '<a name="Image' . $imageAnchorNumber . '"></a>' + . '<div class=CImageCaption>' . ($shortTarget || $target) . '</div>' + . '<img src="' . $self->MakeRelativeURL($self->OutputFileOf($sourceFile), + $self->OutputImageOf($image), 1) . '"' + + . ($width && $height ? ' width="' . $width . '" height="' . $height . '"' : '') + . '>' + + . '</div></blockquote>'; + + $imageAnchorNumber++; + return $output; + }; + } + else # !$image + { + if ($mode eq 'inline') + { return '<p>' . $original . '</p>'; } + else #($mode eq 'link') + { return $original; }; + }; + }; + + +# +# Function: BuildToolTipLinkProperties +# +# Returns the properties that should go in the link tag to add a tooltip to it. Because the function accepts undef, you can +# call it without checking if <BuildToolTip()> returned undef or not. +# +# Parameters: +# +# toolTipID - The ID of the tooltip. If undef, the function will return undef. +# +# Returns: +# +# The properties that should be put in the link tag, or undef if toolTipID wasn't specified. +# +sub BuildToolTipLinkProperties #(toolTipID) + { + my ($self, $toolTipID) = @_; + + if (defined $toolTipID) + { + my $currentNumber = $tooltipLinkNumber; + $tooltipLinkNumber++; + + return 'id=link' . $currentNumber . ' ' + . 'onMouseOver="ShowTip(event, \'' . $toolTipID . '\', \'link' . $currentNumber . '\')" ' + . 'onMouseOut="HideTip(\'' . $toolTipID . '\')"'; + } + else + { return undef; }; + }; + + +# +# Function: AddDoubleSpaces +# +# Adds second spaces after the appropriate punctuation with 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. + + ("|&[lr][sd]quo;|[\'\"\]\}\)]?) # Tolerate closing quotes, parenthesis, etc. + ((?:<[^>]+>)*) # Tolerate tags + + \ # The space + (?![a-z]) # Not followed by a lowercase character. + + /$1$2$3 \ /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. + + \. + + ("|&[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 ' '; }; + }; + + 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/&/&/g; + $text =~ s/\"/"/g; + $text =~ s/</</g; + $text =~ s/>/>/g; + + return $text; + }; + + +# +# Function: RestoreAmpChars +# +# Restores all amp characters to their original state. This works with both <NDMarkup> amp chars and fancy quotes. +# +# Parameters: +# +# text - The text to convert. +# +# Returns: +# +# The converted text. +# +sub RestoreAmpChars #(text) + { + my ($self, $text) = @_; + + $text = NaturalDocs::NDMarkup->RestoreAmpChars($text); + $text =~ s/&[lr]squo;/\'/g; + $text =~ s/&[lr]dquo;/\"/g; + + return $text; + }; + + +# +# Function: AddHiddenBreaks +# +# Adds hidden breaks to symbols. Puts them after symbol and directory separators so long names won't screw up the layout. +# +# Parameters: +# +# string - The string to break. +# +# Returns: +# +# The string with hidden breaks. +# +sub AddHiddenBreaks #(string) + { + my ($self, $string) = @_; + + # \.(?=.{5,}) instead of \. so file extensions don't get breaks. + # :+ instead of :: because Mac paths are separated by a : and we want to get those too. + + $string =~ s/(\w(?:\.(?=.{5,})|:+|->|\\|\/))(\w)/$1 . '<wbr>' . $2/ge; + + return $string; + }; + + +# +# Function: FindFirstFile +# +# A function that finds and returns the first file entry in the menu, or undef if none. +# +sub FindFirstFile + { + # Hidden parameter: arrayref + # Used for recursion only. + + my ($self, $arrayref) = @_; + + if (!defined $arrayref) + { $arrayref = NaturalDocs::Menu->Content(); }; + + foreach my $entry (@$arrayref) + { + if ($entry->Type() == ::MENU_FILE()) + { + return $entry; + } + elsif ($entry->Type() == ::MENU_GROUP()) + { + my $result = $self->FindFirstFile($entry->GroupContent()); + if (defined $result) + { return $result; }; + }; + }; + + return undef; + }; + + +# +# Function: ExpandMenu +# +# Determines which groups should be expanded. +# +# Parameters: +# +# sourceFile - The source <FileName> to use if you're looking for a source file. +# indexType - The index <TopicType> to use if you're looking for an index. +# selectionHierarchy - The <FileName> the menu is being built for. Does not have to be on the menu itself. +# rootLength - The length of the menu's root group, *not* including the contents of subgroups. +# +# Returns: +# +# An arrayref of all the group numbers that should be expanded. At minimum, it will contain the numbers of the groups +# present in <menuSelectionHierarchy>, though it may contain more. +# +sub ExpandMenu #(FileName sourceFile, TopicType indexType, NaturalDocs::Menu::Entry[] selectionHierarchy, int rootLength) -> int[] groupsToExpand + { + my ($self, $sourceFile, $indexType, $menuSelectionHierarchy, $rootLength) = @_; + + my $toExpand = [ ]; + + + # First expand everything in the selection hierarchy. + + my $length = $rootLength; + + foreach my $entry (@$menuSelectionHierarchy) + { + $length += $menuGroupLengths{$entry}; + push @$toExpand, $menuGroupNumbers{$entry}; + }; + + + # Now do multiple passes of group expansion as necessary. We start from bottomIndex and expand outwards. We stop going + # in a direction if a group there is too long -- we do not skip over it and check later groups as well. However, if one direction + # stops, the other can keep going. + + my $pass = 1; + my $hasSubGroups; + + while ($length < MENU_LENGTH_LIMIT) + { + my $content; + my $topIndex; + my $bottomIndex; + + + if ($pass == 1) + { + # First pass, we expand the selection's siblings. + + if (scalar @$menuSelectionHierarchy) + { $content = $menuSelectionHierarchy->[0]->GroupContent(); } + else + { $content = NaturalDocs::Menu->Content(); }; + + $bottomIndex = 0; + + while ($bottomIndex < scalar @$content && + !($content->[$bottomIndex]->Type() == ::MENU_FILE() && + $content->[$bottomIndex]->Target() eq $sourceFile) && + !($content->[$bottomIndex]->Type() != ::MENU_INDEX() && + $content->[$bottomIndex]->Target() eq $indexType) ) + { $bottomIndex++; }; + + if ($bottomIndex == scalar @$content) + { $bottomIndex = 0; }; + $topIndex = $bottomIndex - 1; + } + + elsif ($pass == 2) + { + # If the section we just expanded had no sub-groups, do another pass trying to expand the parent's sub-groups. The + # net effect is that groups won't collapse as much unnecessarily. Someone can click on a file in a sub-group and the + # groups in the parent will stay open. + + if (!$hasSubGroups && scalar @$menuSelectionHierarchy) + { + if (scalar @$menuSelectionHierarchy > 1) + { $content = $menuSelectionHierarchy->[1]->GroupContent(); } + else + { $content = NaturalDocs::Menu->Content(); }; + + $bottomIndex = 0; + + while ($bottomIndex < scalar @$content && + $content->[$bottomIndex] != $menuSelectionHierarchy->[0]) + { $bottomIndex++; }; + + $topIndex = $bottomIndex - 1; + $bottomIndex++; # Increment past our own group. + $hasSubGroups = undef; + } + else + { last; }; + } + + # No more passes. + else + { last; }; + + + while ( ($topIndex >= 0 || $bottomIndex < scalar @$content) && $length < MENU_LENGTH_LIMIT) + { + # We do the bottom first. + + while ($bottomIndex < scalar @$content && $content->[$bottomIndex]->Type() != ::MENU_GROUP()) + { $bottomIndex++; }; + + if ($bottomIndex < scalar @$content) + { + my $bottomEntry = $content->[$bottomIndex]; + $hasSubGroups = 1; + + if ($length + $menuGroupLengths{$bottomEntry} <= MENU_LENGTH_LIMIT) + { + $length += $menuGroupLengths{$bottomEntry}; + push @$toExpand, $menuGroupNumbers{$bottomEntry}; + $bottomIndex++; + } + else + { $bottomIndex = scalar @$content; }; + }; + + # Top next. + + while ($topIndex >= 0 && $content->[$topIndex]->Type() != ::MENU_GROUP()) + { $topIndex--; }; + + if ($topIndex >= 0) + { + my $topEntry = $content->[$topIndex]; + $hasSubGroups = 1; + + if ($length + $menuGroupLengths{$topEntry} <= MENU_LENGTH_LIMIT) + { + $length += $menuGroupLengths{$topEntry}; + push @$toExpand, $menuGroupNumbers{$topEntry}; + $topIndex--; + } + else + { $topIndex = -1; }; + }; + }; + + + $pass++; + }; + + return $toExpand; + }; + + +# +# Function: GetMenuSelectionHierarchy +# +# Finds the sequence of menu groups that contain the current selection. +# +# Parameters: +# +# sourceFile - The source <FileName> to use if you're looking for a source file. +# indexType - The index <TopicType> to use if you're looking for an index. +# +# Returns: +# +# An arrayref of the <NaturalDocs::Menu::Entry> objects of each group surrounding the selected menu item. First entry is the +# group immediately encompassing it, and each subsequent entry works its way towards the outermost group. +# +sub GetMenuSelectionHierarchy #(FileName sourceFile, TopicType indexType) -> NaturalDocs::Menu::Entry[] selectionHierarchy + { + my ($self, $sourceFile, $indexType) = @_; + + my $hierarchy = [ ]; + + $self->FindMenuSelection($sourceFile, $indexType, $hierarchy, NaturalDocs::Menu->Content()); + + return $hierarchy; + }; + + +# +# Function: FindMenuSelection +# +# A recursive function that deterimes if it or any of its sub-groups has the menu selection. +# +# Parameters: +# +# sourceFile - The source <FileName> to use if you're looking for a source file. +# indexType - The index <TopicType> to use if you're looking for an index. +# hierarchyRef - A reference to the menu selection hierarchy. +# entries - An arrayref of <NaturalDocs::Menu::Entries> to search. +# +# Returns: +# +# Whether this group or any of its subgroups had the selection. If true, it will add any subgroups to the menu selection +# hierarchy but not itself. This prevents the topmost entry from being added. +# +sub FindMenuSelection #(FileName sourceFile, TopicType indexType, NaturalDocs::Menu::Entry[] hierarchyRef, NaturalDocs::Menu::Entry[] entries) -> bool hasSelection + { + my ($self, $sourceFile, $indexType, $hierarchyRef, $entries) = @_; + + foreach my $entry (@$entries) + { + if ($entry->Type() == ::MENU_GROUP()) + { + # If the subgroup has the selection... + if ( $self->FindMenuSelection($sourceFile, $indexType, $hierarchyRef, $entry->GroupContent()) ) + { + push @$hierarchyRef, $entry; + return 1; + }; + } + + elsif ($entry->Type() == ::MENU_FILE()) + { + if ($sourceFile eq $entry->Target()) + { return 1; }; + } + + elsif ($entry->Type() == ::MENU_INDEX()) + { + if ($indexType eq $entry->Target) + { return 1; }; + }; + }; + + return 0; + }; + + +# +# Function: ResetToolTips +# +# Resets the <ToolTip Package Variables> for a new page. +# +# Parameters: +# +# samePage - Set this flag if there's the possibility that the next batch of tooltips may be on the same page as the last. +# +sub ResetToolTips #(samePage) + { + my ($self, $samePage) = @_; + + if (!$samePage) + { + $tooltipLinkNumber = 1; + $tooltipNumber = 1; + }; + + $tooltipHTML = undef; + %tooltipSymbolsToNumbers = ( ); + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/ClassHierarchy.pm b/docs/tool/Modules/NaturalDocs/ClassHierarchy.pm new file mode 100644 index 00000000..9f64cb47 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/ClassHierarchy.pm @@ -0,0 +1,860 @@ +############################################################################### +# +# Package: NaturalDocs::ClassHierarchy +# +############################################################################### +# +# A package that handles all the gory details of managing the class hierarchy. It handles the hierarchy itself, which files define +# them, rebuilding the files that are affected by changes, and loading and saving them to a file. +# +# Usage and Dependencies: +# +# - <NaturalDocs::Settings> and <NaturalDocs::Project> must be initialized before use. +# +# - <NaturalDocs::SymbolTable> must be initialized before <Load()> is called. It must reflect the state as of the last time +# Natural Docs was run. +# +# - <Load()> must be called to initialize the package. At this point, the <Information Functions> will return the state as +# of the last time Natural Docs was run. You are free to resolve <NaturalDocs::SymbolTable()> afterwards. +# +# - <Purge()> must be called, and then <NaturalDocs::Parser->ParseForInformation()> must be called on all files that +# have changed so it can fully resolve the hierarchy via the <Modification Functions()>. Afterwards the +# <Information Functions> will reflect the current state of the code. +# +# - <Save()> must be called to commit any changes to the symbol table back to disk. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + + +use strict; +use integer; + +use NaturalDocs::ClassHierarchy::Class; +use NaturalDocs::ClassHierarchy::File; + +package NaturalDocs::ClassHierarchy; + + +############################################################################### +# Group: Variables + +# +# handle: CLASS_HIERARCHY_FILEHANDLE +# The file handle used with <ClassHierarchy.nd>. +# + +# +# hash: classes +# +# A hash of all the classes. The keys are the class <SymbolStrings> and the values are <NaturalDocs::ClassHierarchy::Classes>. +# +my %classes; + +# +# hash: files +# +# A hash of the hierarchy information referenced by file. The keys are the <FileNames>, and the values are +# <NaturalDocs::ClassHierarchy::File>s. +# +my %files; + +# +# hash: parentReferences +# +# A hash of all the parent reference strings and what they resolve to. The keys are the <ReferenceStrings> and the values are +# the class <SymbolStrings> that they resolve to. +# +my %parentReferences; + +# +# object: watchedFile +# +# A <NaturalDocs::ClassHierarchy::File> object of the file being watched for changes. This is compared to the version in <files> +# to see if anything was changed since the last parse. +# +my $watchedFile; + +# +# string: watchedFileName +# +# The <FileName> of the watched file, if any. If there is no watched file, this will be undef. +# +my $watchedFileName; + +# +# bool: dontRebuildFiles +# +# A bool to set if you don't want changes in the hierarchy to cause files to be rebuilt. +# +my $dontRebuildFiles; + + + +############################################################################### +# Group: Files + + +# +# File: ClassHierarchy.nd +# +# Stores the class hierarchy on disk. +# +# Format: +# +# > [BINARY_FORMAT] +# > [VersionInt: app version] +# +# The standard <BINARY_FORMAT> and <VersionInt> header. +# +# > [SymbolString: class or undef to end] +# +# Next we begin a class segment with its <SymbolString>. These continue until the end of the file. Only defined classes are +# included. +# +# > [UInt32: number of files] +# > [AString16: file] [AString16: file] ... +# +# Next there is the number of files that define that class. It's a UInt32, which seems like overkill, but I could imagine every +# file in a huge C++ project being under the same namespace, and thus contributing its own definition. It's theoretically +# possible. +# +# Following the number is that many file names. You must remember the index of each file, as they will be important later. +# Indexes start at one because zero has a special meaning. +# +# > [UInt8: number of parents] +# > ( [ReferenceString (no type): parent] +# > [UInt32: file index] [UInt32: file index] ... [UInt32: 0] ) ... +# +# Next there is the number of parents defined for this class. For each one, we define a parent segment, which consists of +# its <ReferenceString>, and then a zero-terminated string of indexes of the files that define that parent as part of that class. +# The indexes start at one, and are into the list of files we saw previously. +# +# Note that we do store class segments for classes without parents, but not for undefined classes. +# +# This concludes a class segment. These segments continue until an undef <SymbolString>. +# +# See Also: +# +# <File Format Conventions> +# +# Revisions: +# +# 1.22: +# +# - Classes and parents switched from AString16s to <SymbolStrings> and <ReferenceStrings>. +# - A ending undef <SymbolString> was added to the end. Previously it stopped when the file ran out. +# +# 1.2: +# +# - This file was introduced in 1.2. +# + + +############################################################################### +# Group: File Functions + + +# +# Function: Load +# +# Loads the class hierarchy from disk. +# +sub Load + { + my ($self) = @_; + + $dontRebuildFiles = 1; + + my $fileIsOkay; + my $fileName = NaturalDocs::Project->DataFile('ClassHierarchy.nd'); + + if (!NaturalDocs::Settings->RebuildData() && open(CLASS_HIERARCHY_FILEHANDLE, '<' . $fileName)) + { + # See if it's binary. + binmode(CLASS_HIERARCHY_FILEHANDLE); + + my $firstChar; + read(CLASS_HIERARCHY_FILEHANDLE, $firstChar, 1); + + if ($firstChar != ::BINARY_FORMAT()) + { + close(CLASS_HIERARCHY_FILEHANDLE); + } + else + { + my $version = NaturalDocs::Version->FromBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE); + + # Minor bugs were fixed in 1.33 that may affect the stored data. + + if (NaturalDocs::Version->CheckFileFormat( $version, NaturalDocs::Version->FromString('1.33') )) + { $fileIsOkay = 1; } + else + { close(CLASS_HIERARCHY_FILEHANDLE); }; + }; + }; + + + if (!$fileIsOkay) + { + NaturalDocs::Project->ReparseEverything(); + } + else + { + my $raw; + + for (;;) + { + # [SymbolString: class or undef to end] + + my $class = NaturalDocs::SymbolString->FromBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE); + + if (!defined $class) + { last; }; + + # [UInt32: number of files] + + read(CLASS_HIERARCHY_FILEHANDLE, $raw, 4); + my $numberOfFiles = unpack('N', $raw); + + my @files; + + while ($numberOfFiles) + { + # [AString16: file] + + read(CLASS_HIERARCHY_FILEHANDLE, $raw, 2); + my $fileLength = unpack('n', $raw); + + my $file; + read(CLASS_HIERARCHY_FILEHANDLE, $file, $fileLength); + + push @files, $file; + $self->AddClass($file, $class, NaturalDocs::Languages->LanguageOf($file)->Name()); + + $numberOfFiles--; + }; + + # [UInt8: number of parents] + + read(CLASS_HIERARCHY_FILEHANDLE, $raw, 1); + my $numberOfParents = unpack('C', $raw); + + while ($numberOfParents) + { + # [ReferenceString (no type): parent] + + my $parent = NaturalDocs::ReferenceString->FromBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE, + ::BINARYREF_NOTYPE(), + ::REFERENCE_CH_PARENT()); + + for (;;) + { + # [UInt32: file index or 0] + + read(CLASS_HIERARCHY_FILEHANDLE, $raw, 4); + my $fileIndex = unpack('N', $raw); + + if ($fileIndex == 0) + { last; } + + $self->AddParentReference( $files[$fileIndex - 1], $class, $parent ); + }; + + $numberOfParents--; + }; + }; + + close(CLASS_HIERARCHY_FILEHANDLE); + }; + + $dontRebuildFiles = undef; + }; + + +# +# Function: Save +# +# Saves the class hierarchy to disk. +# +sub Save + { + my ($self) = @_; + + open (CLASS_HIERARCHY_FILEHANDLE, '>' . NaturalDocs::Project->DataFile('ClassHierarchy.nd')) + or die "Couldn't save " . NaturalDocs::Project->DataFile('ClassHierarchy.nd') . ".\n"; + + binmode(CLASS_HIERARCHY_FILEHANDLE); + + print CLASS_HIERARCHY_FILEHANDLE '' . ::BINARY_FORMAT(); + NaturalDocs::Version->ToBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE, NaturalDocs::Settings->AppVersion()); + + while (my ($class, $classObject) = each %classes) + { + if ($classObject->IsDefined()) + { + # [SymbolString: class or undef to end] + + NaturalDocs::SymbolString->ToBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE, $class); + + # [UInt32: number of files] + + my @definitions = $classObject->Definitions(); + my %definitionIndexes; + + print CLASS_HIERARCHY_FILEHANDLE pack('N', scalar @definitions); + + for (my $i = 0; $i < scalar @definitions; $i++) + { + # [AString16: file] + print CLASS_HIERARCHY_FILEHANDLE pack('nA*', length($definitions[$i]), $definitions[$i]); + $definitionIndexes{$definitions[$i]} = $i + 1; + }; + + # [UInt8: number of parents] + + my @parents = $classObject->ParentReferences(); + print CLASS_HIERARCHY_FILEHANDLE pack('C', scalar @parents); + + foreach my $parent (@parents) + { + # [ReferenceString (no type): parent] + + NaturalDocs::ReferenceString->ToBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE, $parent, ::BINARYREF_NOTYPE()); + + # [UInt32: file index] + + my @parentDefinitions = $classObject->ParentReferenceDefinitions($parent); + + foreach my $parentDefinition (@parentDefinitions) + { + print CLASS_HIERARCHY_FILEHANDLE pack('N', $definitionIndexes{$parentDefinition}); + }; + + # [UInt32: 0] + print CLASS_HIERARCHY_FILEHANDLE pack('N', 0); + }; + }; + }; + + # [SymbolString: class or undef to end] + + NaturalDocs::SymbolString->ToBinaryFile(\*CLASS_HIERARCHY_FILEHANDLE, undef); + + close(CLASS_HIERARCHY_FILEHANDLE); + }; + + +# +# Function: Purge +# +# Purges the hierarchy of files that no longer have Natural Docs content. +# +sub Purge + { + my ($self) = @_; + + my $filesToPurge = NaturalDocs::Project->FilesToPurge(); + + foreach my $file (keys %$filesToPurge) + { + $self->DeleteFile($file); + }; + }; + + + +############################################################################### +# Group: Interface Functions + + +# +# Function: OnInterpretationChange +# +# Called by <NaturalDocs::SymbolTable> whenever a class hierarchy reference's intepretation changes, meaning it switched +# from one symbol to another. +# +# reference - The <ReferenceString> whose current interpretation changed. +# +sub OnInterpretationChange #(reference) + { + my ($self, $reference) = @_; + + if (NaturalDocs::ReferenceString->TypeOf($reference) == ::REFERENCE_CH_PARENT()) + { + # The approach here is simply to completely delete the reference and readd it. This is less than optimal efficiency, since it's + # being removed and added from %files too, even though that isn't required. However, the simpler code is worth it + # considering this will only happen when a parent reference becomes defined or undefined, or on the rare languages (like C#) + # that allow relative parent references. + + my $oldTargetSymbol = $parentReferences{$reference}; + my $oldTargetObject = $classes{$oldTargetSymbol}; + + my @classesWithReferenceParent = $oldTargetObject->Children(); + + # Each entry is an arrayref of file names. Indexes are the same as classesWithReferenceParent's. + my @filesDefiningReferenceParent; + + foreach my $classWithReferenceParent (@classesWithReferenceParent) + { + my $fileList = [ $classes{$classWithReferenceParent}->ParentReferenceDefinitions($reference) ]; + push @filesDefiningReferenceParent, $fileList; + + foreach my $fileDefiningReferenceParent (@$fileList) + { + $self->DeleteParentReference($fileDefiningReferenceParent, $classWithReferenceParent, $reference); + }; + }; + + + # This will force the reference to be reinterpreted on the next add. + + delete $parentReferences{$reference}; + + + # Now we can just readd it. + + for (my $i = 0; $i < scalar @classesWithReferenceParent; $i++) + { + foreach my $file (@{$filesDefiningReferenceParent[$i]}) + { + $self->AddParentReference($file, $classesWithReferenceParent[$i], $reference); + }; + }; + }; + + # The only way for a REFERENCE_CH_CLASS reference to change is if the symbol is deleted. That will be handled by + # <AnalyzeChanges()>, so we don't need to do anything here. + }; + + +# +# Function: OnTargetSymbolChange +# +# Called by <NaturalDocs::SymbolTable> whenever a class hierarchy reference's target symbol changes, but the reference +# still resolves to the same symbol. +# +# Parameters: +# +# reference - The <ReferenceString> that was affected by the change. +# +sub OnTargetSymbolChange #(reference) + { + my ($self, $reference) = @_; + + my $type = NaturalDocs::ReferenceString->TypeOf($reference); + my $class; + + if ($type == ::REFERENCE_CH_PARENT()) + { $class = $parentReferences{$reference}; } + else # ($type == ::REFERENCE_CH_CLASS()) + { + # Class references are global absolute, so we can just yank the symbol. + (undef, $class, undef, undef, undef, undef) = NaturalDocs::ReferenceString->InformationOf($reference); + }; + + $self->RebuildFilesFor($class, 1, 0, 1); + }; + + + +############################################################################### +# Group: Modification Functions + + +# +# Function: AddClass +# +# Adds a class to the hierarchy. +# +# Parameters: +# +# file - The <FileName> the class was defined in. +# class - The class <SymbolString>. +# languageName - The name of the language this applies to. +# +# Note: +# +# The file parameter must be defined when using this function externally. It may be undef for internal use only. +# +sub AddClass #(file, class, languageName) + { + my ($self, $file, $class, $languageName) = @_; + + if (!exists $classes{$class}) + { + $classes{$class} = NaturalDocs::ClassHierarchy::Class->New(); + NaturalDocs::SymbolTable->AddReference($self->ClassReferenceOf($class, $languageName), $file) + }; + + if (defined $file) + { + # If this was the first definition for this class... + if ($classes{$class}->AddDefinition($file)) + { $self->RebuildFilesFor($class, 1, 1, 1); }; + + if (!exists $files{$file}) + { $files{$file} = NaturalDocs::ClassHierarchy::File->New(); }; + + $files{$file}->AddClass($class); + + if (defined $watchedFileName) + { $watchedFile->AddClass($class); }; + }; + }; + + +# +# Function: AddParentReference +# +# Adds a class-parent relationship to the hierarchy. The classes will be created if they don't already exist. +# +# Parameters: +# +# file - The <FileName> the reference was defined in. +# class - The class <SymbolString>. +# symbol - The parent class <SymbolString>. +# scope - The package <SymbolString> that the reference appeared in. +# using - An arrayref of package <SymbolStrings> that the reference has access to via "using" statements. +# resolvingFlags - Any <Resolving Flags> to be used when resolving the reference. +# +# Alternate Parameters: +# +# file - The <FileName> the reference was defined in. +# class - The class <SymbolString>. +# reference - The parent <ReferenceString>. +# +sub AddParentReference #(file, class, symbol, scope, using, resolvingFlags) or (file, class, reference) + { + my ($self, $file, $class, $symbol, $parentReference); + + if (scalar @_ == 7) + { + my ($scope, $using, $resolvingFlags); + ($self, $file, $class, $symbol, $scope, $using, $resolvingFlags) = @_; + + $parentReference = NaturalDocs::ReferenceString->MakeFrom(::REFERENCE_CH_PARENT(), $symbol, + NaturalDocs::Languages->LanguageOf($file)->Name(), + $scope, $using, $resolvingFlags); + } + else + { + ($self, $file, $class, $parentReference) = @_; + $symbol = (NaturalDocs::ReferenceString->InformationOf($parentReference))[1]; + }; + + + # In case it doesn't already exist. + $self->AddClass($file, $class); + + my $parent; + if (exists $parentReferences{$parentReference}) + { + $parent = $parentReferences{$parentReference}; + } + else + { + NaturalDocs::SymbolTable->AddReference($parentReference, $file); + my $parentTarget = NaturalDocs::SymbolTable->References($parentReference); + + if (defined $parentTarget) + { $parent = $parentTarget->Symbol(); } + else + { $parent = $symbol; }; + + # In case it doesn't already exist. + $self->AddClass(undef, $parent); + + $parentReferences{$parentReference} = $parent; + }; + + + # If this defined a new parent... + if ($classes{$class}->AddParentReference($parentReference, $file, \%parentReferences)) + { + $classes{$parent}->AddChild($class); + + $self->RebuildFilesFor($class, 0, 1, 0); + $self->RebuildFilesFor($parent, 0, 1, 0); + }; + + $files{$file}->AddParentReference($class, $parentReference); + + if (defined $watchedFileName) + { $watchedFile->AddParentReference($class, $parentReference); }; + }; + + +# +# Function: WatchFileForChanges +# +# Watches a file for changes, which can then be applied by <AnalyzeChanges()>. Definitions are not deleted via a DeleteClass() +# function. Instead, a file is watched for changes, reparsed, and then a comparison is made to look for definitions that +# disappeared and any other relevant changes. +# +# Parameters: +# +# file - The <FileName> to watch. +# +sub WatchFileForChanges #(file) + { + my ($self, $file) = @_; + + $watchedFile = NaturalDocs::ClassHierarchy::File->New(); + $watchedFileName = $file; + }; + + +# +# Function: AnalyzeChanges +# +# Checks the watched file for any changes that occured since the last time is was parsed, and updates the hierarchy as +# necessary. Also sends any files that are affected to <NaturalDocs::Project->RebuildFile()>. +# +sub AnalyzeChanges + { + my ($self) = @_; + + # If the file didn't have any classes before, and it still doesn't, it wont be in %files. + if (exists $files{$watchedFileName}) + { + my @originalClasses = $files{$watchedFileName}->Classes(); + + foreach my $originalClass (@originalClasses) + { + # If the class isn't there the second time around... + if (!$watchedFile->HasClass($originalClass)) + { $self->DeleteClass($watchedFileName, $originalClass); } + + else + { + my @originalParents = $files{$watchedFileName}->ParentReferencesOf($originalClass); + + foreach my $originalParent (@originalParents) + { + # If the parent reference wasn't there the second time around... + if (!$watchedFile->HasParentReference($originalClass, $originalParent)) + { $self->DeleteParentReference($watchedFileName, $originalClass, $originalParent); }; + }; + }; + }; + }; + + + $watchedFile = undef; + $watchedFileName = undef; + }; + + + +############################################################################### +# Group: Information Functions + + +# +# Function: ParentsOf +# Returns a <SymbolString> array of the passed class' parents, or an empty array if none. Note that not all of them may be +# defined. +# +sub ParentsOf #(class) + { + my ($self, $class) = @_; + + if (exists $classes{$class}) + { return $classes{$class}->Parents(); } + else + { return ( ); }; + }; + +# +# Function: ChildrenOf +# Returns a <SymbolString> array of the passed class' children, or an empty array if none. Note that not all of them may be +# defined. +# +sub ChildrenOf #(class) + { + my ($self, $class) = @_; + + if (exists $classes{$class}) + { return $classes{$class}->Children(); } + else + { return ( ); }; + }; + + + +############################################################################### +# Group: Support Functions + + +# +# Function: DeleteFile +# +# Deletes a file and everything defined in it. +# +# Parameters: +# +# file - The <FileName>. +# +sub DeleteFile #(file) + { + my ($self, $file) = @_; + + if (!exists $files{$file}) + { return; }; + + my @classes = $files{$file}->Classes(); + foreach my $class (@classes) + { + $self->DeleteClass($file, $class); + }; + + delete $files{$file}; + }; + +# +# Function: DeleteClass +# +# Deletes a class definition from a file. Will also delete any parent references from this class and file. Will rebuild any file +# affected unless <dontRebuildFiles> is set. +# +# Parameters: +# +# file - The <FileName> that defines the class. +# class - The class <SymbolString>. +# +sub DeleteClass #(file, class) + { + my ($self, $file, $class) = @_; + + my @parents = $files{$file}->ParentReferencesOf($class); + foreach my $parent (@parents) + { + $self->DeleteParentReference($file, $class, $parent); + }; + + $files{$file}->DeleteClass($class); + + # If we're deleting the last definition of this class. + if ($classes{$class}->DeleteDefinition($file)) + { + if (!$classes{$class}->HasChildren()) + { + delete $classes{$class}; + + if (!$dontRebuildFiles) + { NaturalDocs::Project->RebuildFile($file); }; + } + else + { $self->RebuildFilesFor($class, 0, 1, 1); }; + + }; + }; + + +# +# Function: DeleteParentReference +# +# Deletes a class' parent reference and returns whether it resulted in the loss of a parent class. Will rebuild any file affected +# unless <dontRebuildFiles> is set. +# +# Parameters: +# +# file - The <FileName> that defines the reference. +# class - The class <SymbolString>. +# reference - The parent <ReferenceString>. +# +# Returns: +# +# If the class lost a parent as a result of this, it will return its <SymbolString>. It will return undef otherwise. +# +sub DeleteParentReference #(file, class, reference) + { + my ($self, $file, $class, $reference) = @_; + + if (!exists $classes{$class}) + { return; }; + + $files{$file}->DeleteParentReference($class, $reference); + + my $deletedParent = $classes{$class}->DeleteParentReference($reference, $file, \%parentReferences); + + if (defined $deletedParent) + { + my $deletedParentObject = $classes{$deletedParent}; + + $deletedParentObject->DeleteChild($class); + + $self->RebuildFilesFor($deletedParent, 0, 1, 0); + $self->RebuildFilesFor($class, 0, 1, 0); + + if (!$deletedParentObject->HasChildren() && !$deletedParentObject->IsDefined()) + { + delete $classes{$deletedParent}; + NaturalDocs::SymbolTable->DeleteReference( + $self->ClassReferenceOf($class, NaturalDocs::Languages->LanguageOf($file)->Name()) ); + }; + + return $deletedParent; + }; + + return undef; + }; + + +# +# Function: ClassReferenceOf +# +# Returns the <REFERENCE_CH_CLASS> <ReferenceString> of the passed class <SymbolString>. +# +sub ClassReferenceOf #(class, languageName) + { + my ($self, $class, $languageName) = @_; + + return NaturalDocs::ReferenceString->MakeFrom(::REFERENCE_CH_CLASS(), $class, $languageName, undef, undef, + ::RESOLVE_ABSOLUTE() | ::RESOLVE_NOPLURAL()); + }; + + +# +# Function: RebuildFilesFor +# +# Calls <NaturalDocs::Project->RebuildFile()> for every file defining the passed class, its parents, and/or its children. +# Returns without doing anything if <dontRebuildFiles> is set. +# +# Parameters: +# +# class - The class <SymbolString>. +# rebuildParents - Whether to rebuild the class' parents. +# rebuildSelf - Whether to rebuild the class. +# rebuildChildren - Whether to rebuild the class' children. +# +sub RebuildFilesFor #(class, rebuildParents, rebuildSelf, rebuildChildren) + { + my ($self, $class, $rebuildParents, $rebuildSelf, $rebuildChildren) = @_; + + if ($dontRebuildFiles) + { return; }; + + my @classesToBuild; + + if ($rebuildParents) + { @classesToBuild = $classes{$class}->Parents(); }; + if ($rebuildSelf) + { push @classesToBuild, $class; }; + if ($rebuildChildren) + { push @classesToBuild, $classes{$class}->Children(); }; + + foreach my $classToBuild (@classesToBuild) + { + my @definitions = $classes{$classToBuild}->Definitions(); + + foreach my $definition (@definitions) + { NaturalDocs::Project->RebuildFile($definition); }; + }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/ClassHierarchy/Class.pm b/docs/tool/Modules/NaturalDocs/ClassHierarchy/Class.pm new file mode 100644 index 00000000..b6faedfc --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/ClassHierarchy/Class.pm @@ -0,0 +1,412 @@ +############################################################################### +# +# Class: NaturalDocs::ClassHierarchy::Class +# +############################################################################### +# +# An object that stores information about a class in the hierarchy. It does not store its <SymbolString>; it assumes that it will +# be stored in a hashref where the key is the <SymbolString>. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::ClassHierarchy::Class; + + +# +# Constants: Members +# +# The class is implemented as a blessed arrayref. The keys are the constants below. +# +# DEFINITIONS - An existence hashref of all the <FileNames> which define this class. Undef if none. +# PARENTS - An existence hashref of the <SymbolStrings> of all the parents this class has. +# CHILDREN - An existence hashref of the <SymbolStrings> of all the children this class has. +# PARENT_REFERENCES - A hashref of the parent <ReferenceStrings> this class has. The keys are the <ReferenceStrings>, +# and the values are existence hashrefs of all the <FileNames> that define them. Undef if none. +# +use NaturalDocs::DefineMembers 'DEFINITIONS', 'PARENTS', 'CHILDREN', 'PARENT_REFERENCES'; +# Dependency: New() depends on the order of these constants, as well as the class not being derived from any other. + + +############################################################################### +# Group: Modification Functions + + +# +# Function: New +# +# Creates and returns a new class. +# +sub New + { + # Dependency: This function depends on the order of the constants, as well as the class not being derived from any other. + my ($package, $definitionFile) = @_; + + my $object = [ undef, undef, undef, undef ]; + bless $object, $package; + + return $object; + }; + + +# +# Function: AddDefinition +# +# Adds a rew definition of this class and returns if that was the first definition. +# +# Parameters: +# +# file - The <FileName> the definition appears in. +# +# Returns: +# +# Whether this was the first definition of this class. +# +sub AddDefinition #(file) + { + my ($self, $file) = @_; + + my $wasFirst; + + if (!defined $self->[DEFINITIONS]) + { + $self->[DEFINITIONS] = { }; + $wasFirst = 1; + }; + + $self->[DEFINITIONS]->{$file} = 1; + + return $wasFirst; + }; + + +# +# Function: DeleteDefinition +# +# Removes the definition of this class and returns if there are no more definitions. Note that if there are no more +# definitions, you may still want to keep the object around if <HasChildren()> returns true. +# +# Parameters: +# +# file - The <FileName> the definition appears in. +# +# Returns: +# +# Whether this deleted the last definition of this class. +# +sub DeleteDefinition #(file) + { + my ($self, $file) = @_; + + if (defined $self->[DEFINITIONS]) + { + delete $self->[DEFINITIONS]->{$file}; + + if (!scalar keys %{$self->[DEFINITIONS]}) + { + $self->[DEFINITIONS] = undef; + return 1; + }; + }; + + return undef; + }; + + +# +# Function: AddParentReference +# +# Adds a parent reference to the class and return whether it resulted in a new parent class. +# +# Parameters: +# +# reference - The <ReferenceString> used to determine the parent. +# file - The <FileName> the parent reference is in. +# referenceTranslations - A hashref of what each reference currently resolves to. The keys are the +# <ReferenceStrings> and the values are class <SymbolStrings>. It should include an entry for +# the reference parameter above. +# +# Returns: +# +# If the reference adds a new parent, it will return that parent's <SymbolString>. Otherwise it will return undef. +# +sub AddParentReference #(reference, file, referenceTranslations) + { + my ($self, $reference, $file, $referenceTranslations) = @_; + + if (!defined $self->[PARENT_REFERENCES]) + { $self->[PARENT_REFERENCES] = { }; }; + if (!defined $self->[PARENTS]) + { $self->[PARENTS] = { }; }; + + + if (!exists $self->[PARENT_REFERENCES]->{$reference}) + { + $self->[PARENT_REFERENCES]->{$reference} = { $file => 1 }; + + my $symbol = $referenceTranslations->{$reference}; + + if (!exists $self->[PARENTS]->{$symbol}) + { + $self->[PARENTS]->{$symbol} = 1; + return $symbol; + } + else + { return undef; }; + } + else + { + $self->[PARENT_REFERENCES]->{$reference}->{$file} = 1; + return undef; + }; + }; + +# +# Function: DeleteParentReference +# +# Deletes a parent reference from the class and return whether it resulted in a loss of a parent class. +# +# Parameters: +# +# reference - The <ReferenceString> used to determine the parent. +# file - The <FileName> the parent declaration is in. +# referenceTranslations - A hashref of what each reference currently resolves to. The keys are the +# <ReferenceStrings> and the values are class <SymbolStrings>. It should include an entry for +# the reference parameter above. +# +# Returns: +# +# If this causes a parent class to be lost, it will return that parent's <SymbolString>. Otherwise it will return undef. +# +sub DeleteParentReference #(reference, file, referenceTranslations) + { + my ($self, $reference, $file, $referenceTranslations) = @_; + + if (defined $self->[PARENT_REFERENCES] && exists $self->[PARENT_REFERENCES]->{$reference} && + exists $self->[PARENT_REFERENCES]->{$reference}->{$file}) + { + delete $self->[PARENT_REFERENCES]->{$reference}->{$file}; + + # Quit if there are other definitions of this reference. + if (scalar keys %{$self->[PARENT_REFERENCES]->{$reference}}) + { return undef; }; + + delete $self->[PARENT_REFERENCES]->{$reference}; + + if (!scalar keys %{$self->[PARENT_REFERENCES]}) + { $self->[PARENT_REFERENCES] = undef; }; + + my $parent = $referenceTranslations->{$reference}; + + # Check if any other references resolve to the same parent. + if (defined $self->[PARENT_REFERENCES]) + { + foreach my $parentReference (keys %{$self->[PARENT_REFERENCES]}) + { + if ($referenceTranslations->{$parentReference} eq $parent) + { return undef; }; + }; + }; + + # If we got this far, no other parent references resolve to this symbol. + + delete $self->[PARENTS]->{$parent}; + + if (!scalar keys %{$self->[PARENTS]}) + { $self->[PARENTS] = undef; }; + + return $parent; + } + else + { return undef; }; + }; + + +# +# Function: AddChild +# Adds a child <SymbolString> to the class. Unlike <AddParentReference()>, this does not keep track of anything other than +# whether it has it or not. +# +# Parameters: +# +# child - The <SymbolString> to add. +# +sub AddChild #(child) + { + my ($self, $child) = @_; + + if (!defined $self->[CHILDREN]) + { $self->[CHILDREN] = { }; }; + + $self->[CHILDREN]->{$child} = 1; + }; + +# +# Function: DeleteChild +# Deletes a child <SymbolString> from the class. Unlike <DeleteParentReference()>, this does not keep track of anything other +# than whether it has it or not. +# +# Parameters: +# +# child - The <SymbolString> to delete. +# +sub DeleteChild #(child) + { + my ($self, $child) = @_; + + if (defined $self->[CHILDREN]) + { + delete $self->[CHILDREN]->{$child}; + + if (!scalar keys %{$self->[CHILDREN]}) + { $self->[CHILDREN] = undef; }; + }; + }; + + + +############################################################################### +# Group: Information Functions + +# +# Function: Definitions +# Returns an array of the <FileNames> that define this class, or an empty array if none. +# +sub Definitions + { + my ($self) = @_; + + if (defined $self->[DEFINITIONS]) + { return keys %{$self->[DEFINITIONS]}; } + else + { return ( ); }; + }; + +# +# Function: IsDefinedIn +# Returns whether the class is defined in the passed <FileName>. +# +sub IsDefinedIn #(file) + { + my ($self, $file) = @_; + + if (defined $self->[DEFINITIONS]) + { return exists $self->[DEFINITIONS]->{$file}; } + else + { return 0; }; + }; + +# +# Function: IsDefined +# Returns whether the class is defined in any files. +# +sub IsDefined + { + my ($self) = @_; + return defined $self->[DEFINITIONS]; + }; + +# +# Function: ParentReferences +# Returns an array of the parent <ReferenceStrings>, or an empty array if none. +# +sub ParentReferences + { + my ($self) = @_; + + if (defined $self->[PARENT_REFERENCES]) + { return keys %{$self->[PARENT_REFERENCES]}; } + else + { return ( ); }; + }; + +# +# Function: HasParentReference +# Returns whether the class has the passed parent <ReferenceString>. +# +sub HasParentReference #(reference) + { + my ($self, $reference) = @_; + return (defined $self->[PARENT_REFERENCES] && exists $self->[PARENT_REFERENCES]->{$reference}); + }; + +# +# Function: HasParentReferences +# Returns whether the class has any parent <ReferenceStrings>. +# +sub HasParentReferences + { + my ($self) = @_; + return defined $self->[PARENT_REFERENCES]; + }; + +# +# Function: Parents +# Returns an array of the parent <SymbolStrings>, or an empty array if none. +# +sub Parents + { + my ($self) = @_; + + if (defined $self->[PARENTS]) + { return keys %{$self->[PARENTS]}; } + else + { return ( ); }; + }; + +# +# Function: HasParents +# Returns whether the class has any parent <SymbolStrings> defined. +# +sub HasParents + { + my ($self) = @_; + return defined $self->[PARENTS]; + }; + +# +# Function: Children +# Returns an array of the child <SymbolStrings>, or an empty array if none. +# +sub Children + { + my ($self) = @_; + + if (defined $self->[CHILDREN]) + { return keys %{$self->[CHILDREN]}; } + else + { return ( ); }; + }; + +# +# Function: HasChildren +# Returns whether any child <SymbolStrings> are defined. +# +sub HasChildren + { + my ($self) = @_; + return defined $self->[CHILDREN]; + }; + + +# +# Function: ParentReferenceDefinitions +# Returns an array of the <FileNames> which define the passed parent <ReferenceString>, or an empty array if none. +# +sub ParentReferenceDefinitions #(reference) + { + my ($self, $reference) = @_; + + if (defined $self->[PARENT_REFERENCES] && exists $self->[PARENT_REFERENCES]->{$reference}) + { return keys %{$self->[PARENT_REFERENCES]->{$reference}}; } + else + { return ( ); }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/ClassHierarchy/File.pm b/docs/tool/Modules/NaturalDocs/ClassHierarchy/File.pm new file mode 100644 index 00000000..3c664acd --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/ClassHierarchy/File.pm @@ -0,0 +1,157 @@ +############################################################################### +# +# Class: NaturalDocs::ClassHierarchy::File +# +############################################################################### +# +# An object that stores information about what hierarchy information is present in a file. It does not store its <FileName>; it +# assumes that it will be stored in a hashref where the key is the <FileName>. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::ClassHierarchy::File; + + +# +# Topic: Implementation +# +# Since there's only one member in the class, and it's a hashref, the class is simply the hashref itself blessed as a class. +# The keys are the class <SymbolStrings> that are defined in the file, and the values are existence hashrefs of each class' +# parent <ReferenceStrings>, or undef if none. +# + + +############################################################################### +# Group: Modification Functions + + +# +# Function: New +# +# Creates and returns a new class. +# +sub New + { + my ($package) = @_; + + my $object = { }; + bless $object, $package; + + return $object; + }; + +# +# Function: AddClass +# Adds a rew class <SymbolString> to the file. +# +sub AddClass #(class) + { + my ($self, $class) = @_; + + if (!exists $self->{$class}) + { $self->{$class} = undef; }; + }; + +# +# Function: DeleteClass +# Deletes a class <SymbolString> from the file. +# +sub DeleteClass #(class) + { + my ($self, $class) = @_; + delete $self->{$class}; + }; + +# +# Function: AddParentReference +# Adds a parent <ReferenceString> to a class <SymbolString>. +# +sub AddParentReference #(class, parentReference) + { + my ($self, $class, $parent) = @_; + + if (!exists $self->{$class} || !defined $self->{$class}) + { $self->{$class} = { }; }; + + $self->{$class}->{$parent} = 1; + }; + +# +# Function: DeleteParentReference +# Deletes a parent <ReferenceString> from a class <SymbolString>. +# +sub DeleteParentReference #(class, parent) + { + my ($self, $class, $parent) = @_; + + if (exists $self->{$class}) + { + delete $self->{$class}->{$parent}; + + if (!scalar keys %{$self->{$class}}) + { $self->{$class} = undef; }; + }; + }; + + + +############################################################################### +# Group: Information Functions + + +# +# Function: Classes +# Returns an array of the class <SymbolStrings> that are defined by this file, or an empty array if none. +# +sub Classes + { + my ($self) = @_; + return keys %{$self}; + }; + +# +# Function: HasClass +# Returns whether the file defines the passed class <SymbolString>. +# +sub HasClass #(class) + { + my ($self, $class) = @_; + return exists $self->{$class}; + }; + +# +# Function: ParentReferencesOf +# Returns an array of the parent <ReferenceStrings> that are defined by the class, or an empty array if none. +# +sub ParentReferencesOf #(class) + { + my ($self, $class) = @_; + + if (!exists $self->{$class} || !defined $self->{$class}) + { return ( ); } + else + { return keys %{$self->{$class}}; }; + }; + +# +# Function: HasParentReference +# Returns whether the file defines the passed class <SymbolString> and parent <ReferenceString>. +# +sub HasParentReference #(class, parent) + { + my ($self, $class, $parent) = @_; + + if (!$self->HasClass($class)) + { return undef; }; + + return exists $self->{$class}->{$parent}; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/ConfigFile.pm b/docs/tool/Modules/NaturalDocs/ConfigFile.pm new file mode 100644 index 00000000..73bc1caa --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/ConfigFile.pm @@ -0,0 +1,497 @@ +############################################################################### +# +# Package: NaturalDocs::ConfigFile +# +############################################################################### +# +# A package to manage Natural Docs' configuration files. +# +# Usage: +# +# - Only one configuration file can be managed with this package at a time. You must close the file before opening another +# one. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::ConfigFile; + + + +# +# Topic: Format +# +# All configuration files are text files. +# +# > # [comment] +# +# Comments start with the # character. +# +# > Format: [version] +# +# All configuration files *must* have a format line as its first line containing content. Whitespace and comments are permitted +# ahead of it. +# +# > [keyword]: [value] +# +# Keywords can only contain <CFChars>. Keywords are not case sensitive. Values can be anything and run until the end of +# the line or a comment. +# +# > [value] +# +# Lines that don't start with a valid keyword format are considered to be all value. +# +# > [line] { [line] } [line] +# +# Files supporting brace groups (specified in <Open()>) may also have braces that can appear anywhere. It allows more than +# one thing to appear per line, which isn't supported otherwise. Consequently, values may not have braces. +# + + +# +# Type: CFChars +# +# The characters that can appear in configuration file keywords and user-defined element names: letters, numbers, spaces, +# dashes, slashes, apostrophes, and periods. +# +# Although the list above is exhaustive, it should be noted that you especially can *not* use colons (messes up keyword: value +# sequences) commas (messes up item, item, item list sequences) and hashes (messes up comment detection.) +# +# You can search the source code for [CFChars] to find all the instances where this definition is used. +# + + +############################################################################### +# Group: Variables + +# +# handle: CONFIG_FILEHANDLE +# +# The file handle used for the configuration file. +# + + +# +# string: file +# +# The <FileName> for the current configuration file being parsed. +# +my $file; + + +# +# array: errors +# +# An array of errors added by <AddError()>. Every odd entry is the line number, and every even entry following is the +# error message. +# +my @errors; + + +# +# var: lineNumber +# +# The current line number for the configuration file. +# +my $lineNumber; + + +# +# bool: hasBraceGroups +# +# Whether the file has brace groups or not. +# +my $hasBraceGroups; + + +# +# array: virtualLines +# +# An array of virtual lines if a line from the file contained more than one. +# +# Files with brace groups may have more than one virtual line per actual file line, such as "Group: A { Group: B". When that +# happens, any extra virtual lines are put into here so they can be returned on the next call. +# +my @virtualLines; + + + +############################################################################### +# Group: Functions + + +# +# Function: Open +# +# Opens a configuration file for parsing and returns the format <VersionInt>. +# +# Parameters: +# +# file - The <FileName> to parse. +# hasBraceGroups - Whether the file supports brace groups or not. If so, lines with braces will be split apart behind the +# scenes. +# +# Returns: +# +# The <VersionInt> of the file, or undef if the file doesn't exist. +# +sub Open #(file, hasBraceGroups) + { + my $self; + ($self, $file, $hasBraceGroups) = @_; + + @errors = ( ); + + # It will be incremented to one when the first line is read from the file. + $lineNumber = 0; + + open(CONFIG_FILEHANDLE, '<' . $file) or return undef; + + + # Get the format line. + + my ($keyword, $value, $comment) = $self->GetLine(); + + if ($keyword eq 'format') + { return NaturalDocs::Version->FromString($value); } + else + { die "The first content line in " . $file . " must be the Format: line.\n"; }; + }; + + +# +# Function: Close +# +# Closes the current configuration file. +# +sub Close + { + my $self = shift; + close(CONFIG_FILEHANDLE); + }; + + +# +# Function: GetLine +# +# Returns the next line containing content, or an empty array if none. +# +# Returns: +# +# Returns the array ( keyword, value, comment ), or an empty array if none. All tabs will be converted to spaces, and all +# whitespace will be condensed into a single space. +# +# keyword - The keyword part of the line, if any. Is converted to lowercase and doesn't include the colon. If the file supports +# brace groups, opening and closing braces will be returned as keywords. +# value - The value part of the line, minus any whitespace. Keeps its original case. +# comment - The comment following the line, if any. This includes the # symbol and a leading space if there was +# any whitespace, since it may be significant. Otherwise undef. Used for lines where the # character needs to be +# accepted as part of the value. +# +sub GetLine + { + my $self = shift; + + my ($line, $comment); + + + # Get the next line with content. + + do + { + # Get the next line. + + my $isFileLine; + + if (scalar @virtualLines) + { + $line = shift @virtualLines; + $isFileLine = 0; + } + else + { + $line = <CONFIG_FILEHANDLE>; + $lineNumber++; + + if (!defined $line) + { return ( ); }; + + ::XChomp(\$line); + + # Condense spaces and tabs into a single space. + $line =~ tr/\t / /s; + $isFileLine = 1; + }; + + + # Split off the comment. + + if ($line =~ /^(.*?)( ?#.*)$/) + { ($line, $comment) = ($1, $2); } + else + { $comment = undef; }; + + + # Split any brace groups. + + if ($isFileLine && $hasBraceGroups && $line =~ /[\{\}]/) + { + ($line, @virtualLines) = split(/([\{\}])/, $line); + + $virtualLines[-1] .= $comment; + $comment = undef; + }; + + + # Remove whitespace. + + $line =~ s/^ //; + $line =~ s/ $//; + $comment =~ s/ $//; + # We want to keep the leading space on a comment. + } + while (!$line); + + + # Process the line. + + if ($hasBraceGroups && ($line eq '{' || $line eq '}')) + { + return ($line, undef, undef); + }; + + + if ($line =~ /^([a-z0-9\ \'\/\.\-]+?) ?: ?(.*)$/i) # [CFChars] + { + my ($keyword, $value) = ($1, $2); + return (lc($keyword), $value, $comment); + } + + else + { + return (undef, $line, $comment); + }; + }; + + +# +# Function: LineNumber +# +# Returns the line number for the line last returned by <GetLine()>. +# +sub LineNumber + { return $lineNumber; }; + + + +############################################################################### +# Group: Error Functions + + +# +# Function: AddError +# +# Stores an error for the current configuration file. Will be attached to the last line read by <GetLine()>. +# +# Parameters: +# +# message - The error message. +# lineNumber - The line number to use. If not specified, it will use the line number from the last call to <GetLine()>. +# +sub AddError #(message, lineNumber) + { + my ($self, $message, $messageLineNumber) = @_; + + if (!defined $messageLineNumber) + { $messageLineNumber = $lineNumber; }; + + push @errors, $messageLineNumber, $message; + }; + + +# +# Function: ErrorCount +# +# Returns how many errors the configuration file has. +# +sub ErrorCount + { + return (scalar @errors) / 2; + }; + + +# +# Function: PrintErrorsAndAnnotateFile +# +# Prints the errors to STDERR in the standard GNU format and annotates the configuration file with them. It does *not* end +# execution. <Close()> *must* be called before this function. +# +sub PrintErrorsAndAnnotateFile + { + my ($self) = @_; + + if (scalar @errors) + { + open(CONFIG_FILEHANDLE, '<' . $file); + my @lines = <CONFIG_FILEHANDLE>; + close(CONFIG_FILEHANDLE); + + # We need to keep track of both the real and the original line numbers. The original line numbers are for matching errors in + # the errors array, and don't include any comment lines added or deleted. Line number is the current line number including + # those comment lines for sending to the display. + my $lineNumber = 1; + my $originalLineNumber = 1; + + open(CONFIG_FILEHANDLE, '>' . $file); + + # We don't want to keep the old error header, if present. + if ($lines[0] =~ /^\# There (?:is an error|are \d+ errors) in this file\./) + { + shift @lines; + $originalLineNumber++; + + # We want to drop the blank line after it as well. + if ($lines[0] eq "\n") + { + shift @lines; + $originalLineNumber++; + }; + }; + + if ($self->ErrorCount() == 1) + { + print CONFIG_FILEHANDLE + "# There is an error in this file. Search for ERROR to find it.\n\n"; + } + else + { + print CONFIG_FILEHANDLE + "# There are " . $self->ErrorCount() . " errors in this file. Search for ERROR to find them.\n\n"; + }; + + $lineNumber += 2; + + + foreach my $line (@lines) + { + while (scalar @errors && $originalLineNumber == $errors[0]) + { + my $errorLine = shift @errors; + my $errorMessage = shift @errors; + + print CONFIG_FILEHANDLE "# ERROR: " . $errorMessage . "\n"; + + # Use the GNU error format, which should make it easier to handle errors when Natural Docs is part of a build process. + # See http://www.gnu.org/prep/standards_15.html + + $errorMessage = lcfirst($errorMessage); + $errorMessage =~ s/\.$//; + + print STDERR 'NaturalDocs:' . $file . ':' . $lineNumber . ': ' . $errorMessage . "\n"; + + $lineNumber++; + }; + + # We want to remove error lines from previous runs. + if (substr($line, 0, 9) ne '# ERROR: ') + { + print CONFIG_FILEHANDLE $line; + $lineNumber++; + }; + + $originalLineNumber++; + }; + + # Clean up any remaining errors. + while (scalar @errors) + { + my $errorLine = shift @errors; + my $errorMessage = shift @errors; + + print CONFIG_FILEHANDLE "# ERROR: " . $errorMessage . "\n"; + + # Use the GNU error format, which should make it easier to handle errors when Natural Docs is part of a build process. + # See http://www.gnu.org/prep/standards_15.html + + $errorMessage = lcfirst($errorMessage); + $errorMessage =~ s/\.$//; + + print STDERR 'NaturalDocs:' . $file . ':' . $lineNumber . ': ' . $errorMessage . "\n"; + }; + + close(CONFIG_FILEHANDLE); + }; + }; + + + +############################################################################### +# Group: Misc Functions + + +# +# Function: HasOnlyCFChars +# +# Returns whether the passed string contains only <CFChars>. +# +sub HasOnlyCFChars #(string) + { + my ($self, $string) = @_; + return ($string =~ /^[a-z0-9\ \.\-\/\']*$/i); # [CFChars] + }; + + +# +# Function: CFCharNames +# +# Returns a plain-english list of <CFChars> which can be embedded in a sentence. For example, "You can only use +# [CFCharsList()] in the name. +# +sub CFCharNames + { + # [CFChars] + return 'letters, numbers, spaces, periods, dashes, slashes, and apostrophes'; + }; + + +# +# Function: Obscure +# +# Obscures the passed text so that it is not user editable and returns it. The encoding method is not secure; it is just designed +# to be fast and to discourage user editing. +# +sub Obscure #(text) + { + my ($self, $text) = @_; + + # ` is specifically chosen to encode to space because of its rarity. We don't want a trailing one to get cut off before decoding. + $text =~ tr{a-zA-Z0-9\ \\\/\.\:\_\-\`} + {pY9fGc\`R8lAoE\\uIdH6tN\/7sQjKx0B5mW\.vZ41PyFg\:CrLaO\_eUi2DhT\-nSqJkXb3MwVz\ }; + + return $text; + }; + + +# +# Function: Unobscure +# +# Restores text encoded with <Obscure()> and returns it. +# +sub Unobscure #(text) + { + my ($self, $text) = @_; + + $text =~ tr{pY9fGc\`R8lAoE\\uIdH6tN\/7sQjKx0B5mW\.vZ41PyFg\:CrLaO\_eUi2DhT\-nSqJkXb3MwVz\ } + {a-zA-Z0-9\ \\\/\.\:\_\-\`}; + + return $text; + }; + + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Constants.pm b/docs/tool/Modules/NaturalDocs/Constants.pm new file mode 100644 index 00000000..f9e272b3 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Constants.pm @@ -0,0 +1,165 @@ +############################################################################### +# +# Package: NaturalDocs::Constants +# +############################################################################### +# +# Constants that are used throughout the script. All are exported by default. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Constants; + +use vars qw(@EXPORT @ISA); +require Exporter; +@ISA = qw(Exporter); + +@EXPORT = ('MENU_TITLE', 'MENU_SUBTITLE', 'MENU_FILE', 'MENU_GROUP', 'MENU_TEXT', 'MENU_LINK', 'MENU_FOOTER', + 'MENU_INDEX', 'MENU_FORMAT', 'MENU_ENDOFORIGINAL', 'MENU_DATA', + + 'MENU_FILE_NOAUTOTITLE', 'MENU_GROUP_UPDATETITLES', 'MENU_GROUP_UPDATESTRUCTURE', + 'MENU_GROUP_UPDATEORDER', 'MENU_GROUP_HASENDOFORIGINAL', + 'MENU_GROUP_UNSORTED', 'MENU_GROUP_FILESSORTED', + 'MENU_GROUP_FILESANDGROUPSSORTED', 'MENU_GROUP_EVERYTHINGSORTED', + 'MENU_GROUP_ISINDEXGROUP', + + 'FILE_NEW', 'FILE_CHANGED', 'FILE_SAME', 'FILE_DOESNTEXIST'); + +# +# Topic: Assumptions +# +# - No constant here will ever be zero. +# - All constants are exported by default. +# + + +############################################################################### +# Group: Virtual Types +# These are only groups of constants, but should be treated like typedefs or enums. Each one represents a distinct type and +# their values should only be one of their constants or undef. + + +# +# Constants: MenuEntryType +# +# The types of entries that can appear in the menu. +# +# MENU_TITLE - The title of the menu. +# MENU_SUBTITLE - The sub-title of the menu. +# MENU_FILE - A source file, relative to the source directory. +# MENU_GROUP - A group. +# MENU_TEXT - Arbitrary text. +# MENU_LINK - A web link. +# MENU_FOOTER - Footer text. +# MENU_INDEX - An index. +# MENU_FORMAT - The version of Natural Docs the menu file was generated with. +# MENU_ENDOFORIGINAL - A dummy entry that marks where the original group content ends. This is used when automatically +# changing the groups so that the alphabetization or lack thereof can be detected without being +# affected by new entries tacked on to the end. +# MENU_DATA - Data not meant for user editing. +# +# Dependency: +# +# <PreviousMenuState.nd> depends on these values all being able to fit into a UInt8, i.e. <= 255. +# +use constant MENU_TITLE => 1; +use constant MENU_SUBTITLE => 2; +use constant MENU_FILE => 3; +use constant MENU_GROUP => 4; +use constant MENU_TEXT => 5; +use constant MENU_LINK => 6; +use constant MENU_FOOTER => 7; +use constant MENU_INDEX => 8; +use constant MENU_FORMAT => 9; +use constant MENU_ENDOFORIGINAL => 10; +use constant MENU_DATA => 11; + + +# +# Constants: FileStatus +# +# What happened to a file since Natural Docs' last execution. +# +# FILE_NEW - The file has been added since the last run. +# FILE_CHANGED - The file has been modified since the last run. +# FILE_SAME - The file hasn't been modified since the last run. +# FILE_DOESNTEXIST - The file doesn't exist, or was deleted. +# +use constant FILE_NEW => 1; +use constant FILE_CHANGED => 2; +use constant FILE_SAME => 3; +use constant FILE_DOESNTEXIST => 4; + + + +############################################################################### +# Group: Flags +# These constants can be combined with each other. + + +# +# Constants: Menu Entry Flags +# +# The various flags that can apply to a menu entry. You cannot mix flags of different types, since they may overlap. +# +# File Flags: +# +# MENU_FILE_NOAUTOTITLE - Whether the file is auto-titled or not. +# +# Group Flags: +# +# MENU_GROUP_UPDATETITLES - The group should have its auto-titles regenerated. +# MENU_GROUP_UPDATESTRUCTURE - The group should be checked for structural changes, such as being removed or being +# split into subgroups. +# MENU_GROUP_UPDATEORDER - The group should be resorted. +# +# MENU_GROUP_HASENDOFORIGINAL - Whether the group contains a dummy <MENU_ENDOFORIGINAL> entry. +# MENU_GROUP_ISINDEXGROUP - Whether the group is used primarily for <MENU_INDEX> entries. <MENU_TEXT> entries +# are tolerated. +# +# MENU_GROUP_UNSORTED - The group's contents are not sorted. +# MENU_GROUP_FILESSORTED - The group's files are sorted alphabetically. +# MENU_GROUP_FILESANDGROUPSSORTED - The group's files and sub-groups are sorted alphabetically. +# MENU_GROUP_EVERYTHINGSORTED - All entries in the group are sorted alphabetically. +# +use constant MENU_FILE_NOAUTOTITLE => 0x0001; + +use constant MENU_GROUP_UPDATETITLES => 0x0001; +use constant MENU_GROUP_UPDATESTRUCTURE => 0x0002; +use constant MENU_GROUP_UPDATEORDER => 0x0004; +use constant MENU_GROUP_HASENDOFORIGINAL => 0x0008; + +# This could really be a two-bit field instead of four flags, but it's not worth the effort since it's only used internally. +use constant MENU_GROUP_UNSORTED => 0x0010; +use constant MENU_GROUP_FILESSORTED => 0x0020; +use constant MENU_GROUP_FILESANDGROUPSSORTED => 0x0040; +use constant MENU_GROUP_EVERYTHINGSORTED => 0x0080; + +use constant MENU_GROUP_ISINDEXGROUP => 0x0100; + + + + +############################################################################### +# Group: Support Functions + + +# +# Function: IsClassHierarchyReference +# Returns whether the passed <ReferenceType> belongs to <NaturalDocs::ClassHierarchy>. +# +sub IsClassHierarchyReference #(reference) + { + my ($self, $reference) = @_; + return ($reference == ::REFERENCE_CH_CLASS() || $reference == ::REFERENCE_CH_PARENT()); + }; + + + +1; diff --git a/docs/tool/Modules/NaturalDocs/DefineMembers.pm b/docs/tool/Modules/NaturalDocs/DefineMembers.pm new file mode 100644 index 00000000..80a38dd9 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/DefineMembers.pm @@ -0,0 +1,100 @@ +############################################################################### +# +# Package: NaturalDocs::DefineMembers +# +############################################################################### +# +# A custom Perl pragma to define member constants and accessors for use in Natural Docs objects while supporting inheritance. +# +# Each member will be defined as a numeric constant which should be used as that variable's index into the object arrayref. +# They will be assigned sequentially from zero, and take into account any members defined this way in parent classes. Note +# that you can *not* use multiple inheritance with this method. +# +# If a parameter ends in parenthesis, it will be generated as an accessor for the previous member. If it also starts with "Set", +# the accessor will accept a single parameter to replace the value with. If it's followed with "duparrayref", it will assume the +# parameter is either an arrayref or undef, and if the former, will duplicate it to set the value. +# +# Example: +# +# > package MyPackage; +# > +# > use NaturalDocs::DefineMembers 'VAR_A', 'VarA()', 'SetVarA()', +# > 'VAR_B', 'VarB()', +# > 'VAR_C', +# > 'VAR_D', 'VarD()', 'SetVarD() duparrayref'; +# > +# > sub SetC #(C) +# > { +# > my ($self, $c) = @_; +# > $self->[VAR_C] = $c; +# > }; +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + + +package NaturalDocs::DefineMembers; + +sub import #(member, member, ...) + { + my ($self, @parameters) = @_; + my $package = caller(); + + no strict 'refs'; + my $parent = ${$package . '::ISA'}[0]; + use strict 'refs'; + + my $memberConstant = 0; + my $lastMemberName; + + if (defined $parent && $parent->can('END_OF_MEMBERS')) + { $memberConstant = $parent->END_OF_MEMBERS(); }; + + my $code = '{ package ' . $package . ";\n"; + + foreach my $parameter (@parameters) + { + if ($parameter =~ /^(.+)\(\) *(duparrayref)?$/i) + { + my ($functionName, $pragma) = ($1, lc($2)); + + if ($functionName =~ /^Set/) + { + if ($pragma eq 'duparrayref') + { + $code .= + 'sub ' . $functionName . ' + { + if (defined $_[1]) + { $_[0]->[' . $lastMemberName . '] = [ @{$_[1]} ]; } + else + { $_[0]->[' . $lastMemberName . '] = undef; }; + };' . "\n"; + } + else + { + $code .= 'sub ' . $functionName . ' { $_[0]->[' . $lastMemberName . '] = $_[1]; };' . "\n"; + }; + } + else + { + $code .= 'sub ' . $functionName . ' { return $_[0]->[' . $lastMemberName . ']; };' . "\n"; + }; + } + else + { + $code .= 'use constant ' . $parameter . ' => ' . $memberConstant . ";\n"; + $memberConstant++; + $lastMemberName = $parameter; + }; + }; + + $code .= 'use constant END_OF_MEMBERS => ' . $memberConstant . ";\n"; + $code .= '};'; + + eval $code; + }; + +1; diff --git a/docs/tool/Modules/NaturalDocs/Error.pm b/docs/tool/Modules/NaturalDocs/Error.pm new file mode 100644 index 00000000..4a1e4a0a --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Error.pm @@ -0,0 +1,305 @@ +############################################################################### +# +# Package: NaturalDocs::Error +# +############################################################################### +# +# Manages all aspects of error handling in Natural Docs. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +$SIG{'__DIE__'} = \&NaturalDocs::Error::CatchDeath; + + +package NaturalDocs::Error; + + +############################################################################### +# Group: Variables + + +# +# handle: FH_CRASHREPORT +# The filehandle used for generating crash reports. +# + + +# +# var: stackTrace +# The stack trace generated by <CatchDeath()>. +# +my $stackTrace; + + +# +# var: softDeath +# Whether the program exited using <SoftDeath()>. +# +my $softDeath; + + +# +# var: currentAction +# What Natural Docs was doing when it crashed. This stores strings generated by functions like <OnStartParsing()>. +# +my $currentAction; + + +############################################################################### +# Group: Functions + + +# +# Function: SoftDeath +# +# Generates a "soft" death, which means the program exits like with Perl's die(), but no crash report will be generated. +# +# Parameter: +# +# message - The error message to die with. +# +sub SoftDeath #(message) + { + my ($self, $message) = @_; + + $softDeath = 1; + if ($message !~ /\n$/) + { $message .= "\n"; }; + + die $message; + }; + + +# +# Function: OnStartParsing +# +# Called whenever <NaturalDocs::Parser> starts parsing a source file. +# +sub OnStartParsing #(FileName file) + { + my ($self, $file) = @_; + $currentAction = 'Parsing ' . $file; + }; + + +# +# Function: OnEndParsing +# +# Called whenever <NaturalDocs::Parser> is done parsing a source file. +# +sub OnEndParsing #(FileName file) + { + my ($self, $file) = @_; + $currentAction = undef; + }; + + +# +# Function: OnStartBuilding +# +# Called whenever <NaturalDocs::Builder> starts building a source file. +# +sub OnStartBuilding #(FileName file) + { + my ($self, $file) = @_; + $currentAction = 'Building ' . $file; + }; + + +# +# Function: OnEndBuilding +# +# Called whenever <NaturalDocs::Builder> is done building a source file. +# +sub OnEndBuilding #(FileName file) + { + my ($self, $file) = @_; + $currentAction = undef; + }; + + +# +# Function: HandleDeath +# +# Should be called whenever Natural Docs dies out of execution. +# +sub HandleDeath + { + my $self = shift; + + my $reason = $::EVAL_ERROR; + $reason =~ s/[\n\r]+$//; + + my $errorMessage = + "\n" + . "Natural Docs encountered the following error and was stopped:\n" + . "\n" + . " " . $reason . "\n" + . "\n" + + . "You can get help at the following web site:\n" + . "\n" + . " " . NaturalDocs::Settings->AppURL() . "\n" + . "\n"; + + if (!$softDeath) + { + my $crashReport = $self->GenerateCrashReport(); + + if ($crashReport) + { + $errorMessage .= + "If sending an error report, please include the information found in the\n" + . "following file:\n" + . "\n" + . " " . $crashReport . "\n" + . "\n"; + } + else + { + $errorMessage .= + "If sending an error report, please include the following information:\n" + . "\n" + . " Natural Docs version: " . NaturalDocs::Settings->TextAppVersion() . "\n" + . " Perl version: " . $self->PerlVersion() . " on " . $::OSNAME . "\n" + . "\n"; + }; + }; + + die $errorMessage; + }; + + +############################################################################### +# Group: Support Functions + + +# +# Function: PerlVersion +# Returns the current Perl version as a string. +# +sub PerlVersion + { + my $self = shift; + + my $perlVersion; + + if ($^V) + { $perlVersion = sprintf('%vd', $^V); } + if (!$perlVersion || substr($perlVersion, 0, 1) eq '%') + { $perlVersion = $]; }; + + return $perlVersion; + }; + + +# +# Function: GenerateCrashReport +# +# Generates a report and returns the <FileName> it's located at. Returns undef if it could not generate one. +# +sub GenerateCrashReport + { + my $self = shift; + + my $errorMessage = $::EVAL_ERROR; + $errorMessage =~ s/[\r\n]+$//; + + my $reportDirectory = NaturalDocs::Settings->ProjectDirectory(); + + if (!$reportDirectory || !-d $reportDirectory) + { return undef; }; + + my $file = NaturalDocs::File->JoinPaths($reportDirectory, 'LastCrash.txt'); + + open(FH_CRASHREPORT, '>' . $file) or return undef; + + print FH_CRASHREPORT + 'Crash Message:' . "\n\n" + . ' ' . $errorMessage . "\n\n"; + + if ($currentAction) + { + print FH_CRASHREPORT + 'Current Action:' . "\n\n" + . ' ' . $currentAction . "\n\n"; + }; + + print FH_CRASHREPORT + 'Natural Docs version ' . NaturalDocs::Settings->TextAppVersion() . "\n" + . 'Perl version ' . $self->PerlVersion . ' on ' . $::OSNAME . "\n\n" + . 'Command Line:' . "\n\n" + . ' ' . join(' ', @ARGV) . "\n\n"; + + if ($stackTrace) + { + print FH_CRASHREPORT + 'Stack Trace:' . "\n\n" + . $stackTrace; + } + else + { + print FH_CRASHREPORT + 'Stack Trace not available.' . "\n\n"; + }; + + close(FH_CRASHREPORT); + return $file; + }; + + +############################################################################### +# Group: Signal Handlers + + +# +# Function: CatchDeath +# +# Catches Perl die calls. +# +# *IMPORTANT:* This function is a signal handler and should not be called manually. Also, because of this, it does not have +# a $self parameter. +# +# Parameters: +# +# message - The error message to die with. +# +sub CatchDeath #(message) + { + # No $self because it's a signal handler. + my $message = shift; + + if (!$NaturalDocs::Error::softDeath) + { + my $i = 0; + my ($lastPackage, $lastFile, $lastLine, $lastFunction); + + while (my ($package, $file, $line, $function) = caller($i)) + { + if ($i != 0) + { $stackTrace .= ', called from' . "\n"; }; + + $stackTrace .= ' ' . $function; + + if (defined $lastLine) + { + $stackTrace .= ', line ' . $lastLine; + + if ($function !~ /^NaturalDocs::/) + { $stackTrace .= ' of ' . $lastFile; }; + }; + + ($lastPackage, $lastFile, $lastLine, $lastFunction) = ($package, $file, $line, $function); + $i++; + }; + }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/File.pm b/docs/tool/Modules/NaturalDocs/File.pm new file mode 100644 index 00000000..754d708b --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/File.pm @@ -0,0 +1,540 @@ +############################################################################### +# +# Package: NaturalDocs::File +# +############################################################################### +# +# A package to manage file access across platforms. Incorporates functions from various standard File:: packages, but more +# importantly, works around the glorious suckage present in File::Spec, at least in version 0.82 and earlier. Read the "Why oh +# why?" sections for why this package was necessary. +# +# Usage and Dependencies: +# +# - The package doesn't depend on any other Natural Docs packages and is ready to use immediately. +# +# - All functions except <CanonizePath()> assume that all parameters are canonized. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use File::Spec (); +use File::Path (); +use File::Copy (); + +use strict; +use integer; + +package NaturalDocs::File; + + +# +# Function: CheckCompatibility +# +# Checks if the standard packages required by this one are up to snuff and dies if they aren't. This is done because I can't +# tell which versions of File::Spec have splitpath just by the version numbers. +# +sub CheckCompatibility + { + my ($self) = @_; + + eval { + File::Spec->splitpath(''); + }; + + if ($@) + { + NaturalDocs::Error->SoftDeath("Natural Docs requires a newer version of File::Spec than you have. " + . "You must either upgrade it or upgrade Perl."); + }; + }; + + +############################################################################### +# Group: Path String Functions + + +# +# Function: CanonizePath +# +# Takes a path and returns a logically simplified version of it. +# +# Why oh why?: +# +# Because File::Spec->canonpath doesn't strip quotes on Windows. So if you pass in "a b\c" or "a b"\c, they still end up as +# different strings even though they're logically the same. +# +# It also doesn't remove things like "..", so "a/b/../c" doesn't simplify to "a/c" like it should. +# +sub CanonizePath #(path) + { + my ($self, $path) = @_; + + if ($::OSNAME eq 'MSWin32') + { + # We don't have to use a smarter algorithm for dropping quotes because they're invalid characters for actual file and + # directory names. + $path =~ s/\"//g; + }; + + $path = File::Spec->canonpath($path); + + # Condense a/b/../c into a/c. + + my $upDir = File::Spec->updir(); + if (index($path, $upDir) != -1) + { + my ($volume, $directoryString, $file) = $self->SplitPath($path); + my @directories = $self->SplitDirectories($directoryString); + + my $i = 1; + while ($i < scalar @directories) + { + if ($i > 0 && $directories[$i] eq $upDir && $directories[$i - 1] ne $upDir) + { + splice(@directories, $i - 1, 2); + $i--; + } + else + { $i++; }; + }; + + $directoryString = $self->JoinDirectories(@directories); + $path = $self->JoinPath($volume, $directoryString, $file); + }; + + return $path; + }; + + +# +# Function: PathIsAbsolute +# +# Returns whether the passed path is absolute. +# +sub PathIsAbsolute #(path) + { + my ($self, $path) = @_; + return File::Spec->file_name_is_absolute($path); + }; + + +# +# Function: JoinPath +# +# Creates a path from its elements. +# +# Parameters: +# +# volume - The volume, such as the drive letter on Windows. Undef if none. +# dirString - The directory string. Create with <JoinDirectories()> if necessary. +# file - The file name, or undef if none. +# +# Returns: +# +# The joined path. +# +sub JoinPath #(volume, dirString, $file) + { + my ($self, $volume, $dirString, $file) = @_; + return File::Spec->catpath($volume, $dirString, $file); + }; + + +# +# Function: JoinPaths +# +# Joins two paths. +# +# Parameters: +# +# basePath - May be a relative path, an absolute path, or undef. +# extraPath - May be a relative path, a file, a relative path and file together, or undef. +# noFileInExtra - Set this to true if extraPath is a relative path only, and doesn't have a file. +# +# Returns: +# +# The joined path. +# +# Why oh why?: +# +# Because nothing in File::Spec will simply slap two paths together. They have to be split up for catpath/file, and rel2abs +# requires the base to be absolute. +# +sub JoinPaths #(basePath, extraPath, noFileInExtra) + { + my ($self, $basePath, $extraPath, $noFileInExtra) = @_; + + # If both are undef, it will return undef, which is what we want. + if (!defined $basePath) + { return $extraPath; } + elsif (!defined $extraPath) + { return $basePath; }; + + my ($baseVolume, $baseDirString, $baseFile) = File::Spec->splitpath($basePath, 1); + my ($extraVolume, $extraDirString, $extraFile) = File::Spec->splitpath($extraPath, $noFileInExtra); + + my @baseDirectories = $self->SplitDirectories($baseDirString); + my @extraDirectories = $self->SplitDirectories($extraDirString); + + my $fullDirString = $self->JoinDirectories(@baseDirectories, @extraDirectories); + + my $fullPath = File::Spec->catpath($baseVolume, $fullDirString, $extraFile); + + return $self->CanonizePath($fullPath); + }; + + +# +# Function: SplitPath +# +# Takes a path and returns its elements. +# +# Parameters: +# +# path - The path to split. +# noFile - Set to true if the path doesn't have a file at the end. +# +# Returns: +# +# The array ( volume, directoryString, file ). If any don't apply, they will be undef. Use <SplitDirectories()> to split the +# directory string if desired. +# +# Why oh Why?: +# +# Because File::Spec->splitpath may leave a trailing slash/backslash/whatever on the directory string, which makes +# it a bit hard to match it with results from File::Spec->catdir. +# +sub SplitPath #(path, noFile) + { + my ($self, $path, $noFile) = @_; + + my @segments = File::Spec->splitpath($path, $noFile); + + if (!length $segments[0]) + { $segments[0] = undef; }; + if (!length $segments[2]) + { $segments[2] = undef; }; + + $segments[1] = File::Spec->catdir( File::Spec->splitdir($segments[1]) ); + + return @segments; + }; + + +# +# Function: JoinDirectories +# +# Creates a directory string from an array of directory names. +# +# Parameters: +# +# directory - A directory name. There may be as many of these as desired. +# +sub JoinDirectories #(directory, directory, ...) + { + my ($self, @directories) = @_; + return File::Spec->catdir(@directories); + }; + + +# +# Function: SplitDirectories +# +# Takes a string of directories and returns an array of its elements. +# +# Why oh why?: +# +# Because File::Spec->splitdir might leave an empty element at the end of the array, which screws up both joining in +# <ConvertToURL> and navigation in <MakeRelativePath>. +# +sub SplitDirectories #(directoryString) + { + my ($self, $directoryString) = @_; + + my @directories = File::Spec->splitdir($directoryString); + + if (!length $directories[-1]) + { pop @directories; }; + + return @directories; + }; + + +# +# Function: MakeRelativePath +# +# Takes two paths and returns a relative path between them. +# +# Parameters: +# +# basePath - The starting path. May be relative or absolute, so long as the target path is as well. +# targetPath - The target path. May be relative or absolute, so long as the base path is as well. +# +# If both paths are relative, they are assumed to be relative to the same base. +# +# Returns: +# +# The target path relative to base. +# +# Why oh why?: +# +# First, there's nothing that gives a relative path between two relative paths. +# +# Second, if target and base are absolute but on different volumes, File::Spec->abs2rel creates a totally non-functional +# relative path. It should return the target as is, since there is no relative path. +# +# Third, File::Spec->abs2rel between absolute paths on the same volume, at least on Windows, leaves the drive letter +# on. So abs2rel('a:\b\c\d', 'a:\b') returns 'a:c\d' instead of the expected 'c\d'. That makes no sense whatsoever. It's +# not like it was designed to handle only directory names, either; the documentation says 'path' and the code seems to +# explicitly handle it. There's just an 'unless' in there that tacks on the volume, defeating the purpose of a *relative* path +# and making the function worthless. +# +sub MakeRelativePath #(basePath, targetPath) + { + my ($self, $basePath, $targetPath) = @_; + + my ($baseVolume, $baseDirString, $baseFile) = $self->SplitPath($basePath, 1); + my ($targetVolume, $targetDirString, $targetFile) = $self->SplitPath($targetPath); + + # If the volumes are different, there is no possible relative path. + if ($targetVolume ne $baseVolume) + { return $targetPath; }; + + my @baseDirectories = $self->SplitDirectories($baseDirString); + my @targetDirectories = $self->SplitDirectories($targetDirString); + + # Skip the parts of the path that are the same. + while (scalar @baseDirectories && @targetDirectories && $baseDirectories[0] eq $targetDirectories[0]) + { + shift @baseDirectories; + shift @targetDirectories; + }; + + # Back out of the base path until it reaches where they were similar. + for (my $i = 0; $i < scalar @baseDirectories; $i++) + { + unshift @targetDirectories, File::Spec->updir(); + }; + + $targetDirString = $self->JoinDirectories(@targetDirectories); + + return File::Spec->catpath(undef, $targetDirString, $targetFile); + }; + + +# +# Function: IsSubPathOf +# +# Returns whether the path is a descendant of another path. +# +# Parameters: +# +# base - The base path to test against. +# path - The possible subpath to test. +# +# Returns: +# +# Whether path is a descendant of base. +# +sub IsSubPathOf #(base, path) + { + my ($self, $base, $path) = @_; + + # This is a quick test that should find a false quickly. + if ($base eq substr($path, 0, length($base))) + { + # This doesn't guarantee true, because it could be "C:\A B" and "C:\A B C\File". So we test for it by seeing if the last + # directory in base is the same as the equivalent directory in path. + + my ($baseVolume, $baseDirString, $baseFile) = NaturalDocs::File->SplitPath($base, 1); + my @baseDirectories = NaturalDocs::File->SplitDirectories($baseDirString); + + my ($pathVolume, $pathDirString, $pathFile) = NaturalDocs::File->SplitPath($path); + my @pathDirectories = NaturalDocs::File->SplitDirectories($pathDirString); + + return ( $baseDirectories[-1] eq $pathDirectories[ scalar @baseDirectories - 1 ] ); + } + else + { return undef; }; + }; + + +# +# Function: ConvertToURL +# +# Takes a relative path and converts it from the native format to a relative URL. Note that it _doesn't_ convert special characters +# to amp chars. +# +sub ConvertToURL #(path) + { + my ($self, $path) = @_; + + my ($pathVolume, $pathDirString, $pathFile) = $self->SplitPath($path); + my @pathDirectories = $self->SplitDirectories($pathDirString); + + my $i = 0; + while ($i < scalar @pathDirectories && $pathDirectories[$i] eq File::Spec->updir()) + { + $pathDirectories[$i] = '..'; + $i++; + }; + + return join('/', @pathDirectories, $pathFile); + }; + + +# +# Function: NoUpwards +# +# Takes an array of directory entries and returns one without all the entries that refer to the parent directory, such as '.' and '..'. +# +sub NoUpwards #(array) + { + my ($self, @array) = @_; + return File::Spec->no_upwards(@array); + }; + + +# +# Function: NoFileName +# +# Takes a path and returns a version without the file name. Useful for sending paths to <CreatePath()>. +# +sub NoFileName #(path) + { + my ($self, $path) = @_; + + my ($pathVolume, $pathDirString, $pathFile) = File::Spec->splitpath($path); + + return File::Spec->catpath($pathVolume, $pathDirString, undef); + }; + + +# +# Function: NoExtension +# +# Returns the path without an extension. +# +sub NoExtension #(path) + { + my ($self, $path) = @_; + + my $extension = $self->ExtensionOf($path); + + if ($extension) + { $path = substr($path, 0, length($path) - length($extension) - 1); }; + + return $path; + }; + + +# +# Function: ExtensionOf +# +# Returns the extension of the passed path, or undef if none. +# +sub ExtensionOf #(path) + { + my ($self, $path) = @_; + + my ($pathVolume, $pathDirString, $pathFile) = File::Spec->splitpath($path); + + # We need the leading dot in the regex so files that start with a dot but don't have an extension count as extensionless files. + if ($pathFile =~ /.\.([^\.]+)$/) + { return $1; } + else + { return undef; }; + }; + + +# +# Function: IsCaseSensitive +# +# Returns whether the current platform has case-sensitive paths. +# +sub IsCaseSensitive + { + return !(File::Spec->case_tolerant()); + }; + + + +############################################################################### +# Group: Disk Functions + + +# +# Function: CreatePath +# +# Creates a directory tree corresponding to the passed path, regardless of how many directories do or do not already exist. +# Do _not_ include a file name in the path. Use <NoFileName()> first if you need to. +# +sub CreatePath #(path) + { + my ($self, $path) = @_; + File::Path::mkpath($path); + }; + + +# +# Function: RemoveEmptyTree +# +# Removes an empty directory tree. The passed directory will be removed if it's empty, and it will keep removing its parents +# until it reaches one that's not empty or a set limit. +# +# Parameters: +# +# path - The path to start from. It will try to remove this directory and work it's way down. +# limit - The path to stop at if it doesn't find any non-empty directories first. This path will *not* be removed. +# +sub RemoveEmptyTree #(path, limit) + { + my ($self, $path, $limit) = @_; + + my ($volume, $directoryString) = $self->SplitPath($path, 1); + my @directories = $self->SplitDirectories($directoryString); + + my $directory = $path; + + while (-d $directory && $directory ne $limit) + { + opendir FH_ND_FILE, $directory; + my @entries = readdir FH_ND_FILE; + closedir FH_ND_FILE; + + @entries = $self->NoUpwards(@entries); + + if (scalar @entries || !rmdir($directory)) + { last; }; + + pop @directories; + $directoryString = $self->JoinDirectories(@directories); + $directory = $self->JoinPath($volume, $directoryString); + }; + }; + + +# +# Function: Copy +# +# Copies a file from one path to another. If the destination file exists, it is overwritten. +# +# Parameters: +# +# source - The file to copy. +# destination - The destination to copy to. +# +# Returns: +# +# Whether it succeeded +# +sub Copy #(source, destination) => bool + { + my ($self, $source, $destination) = @_; + return File::Copy::copy($source, $destination); + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/ImageReferenceTable.pm b/docs/tool/Modules/NaturalDocs/ImageReferenceTable.pm new file mode 100644 index 00000000..8f13ce5f --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/ImageReferenceTable.pm @@ -0,0 +1,383 @@ +############################################################################### +# +# Package: NaturalDocs::ImageReferenceTable +# +############################################################################### +# +# A <NaturalDocs::SourceDB>-based package that manages all the image references appearing in source files. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +use NaturalDocs::ImageReferenceTable::String; +use NaturalDocs::ImageReferenceTable::Reference; + + +package NaturalDocs::ImageReferenceTable; + +use base 'NaturalDocs::SourceDB::Extension'; + + +############################################################################### +# Group: Information + +# +# Topic: Usage +# +# - <NaturalDocs::Project> and <NaturalDocs::SourceDB> must be initialized before this package can be used. +# +# - Call <Register()> before using. +# +# +# Topic: Programming Notes +# +# When working on this code, remember that there are three things it has to juggle. +# +# - The information in <NaturalDocs::SourceDB>. +# - Image file references in <NaturalDocs::Project>. +# - Source file rebuilding on changes. +# +# Managing the actual image files will be handled between <NaturalDocs::Project> and the <NaturalDocs::Builder> +# sub-packages. +# +# +# Topic: Implementation +# +# Managing image references is simpler than managing the references in <NaturalDocs::SymbolTable>. In SymbolTable, +# you have to worry about reference targets popping into and out of existence. A link may go to a file that hasn't been +# reparsed yet and the target may no longer exist. We have to deal with that when we know it, which may be after the +# reference's file was parsed. Also, a new definition may appear that serves as a better interpretation of a link than its +# current target, and again we may only know that after the reference's file has been parsed already. So we have to deal +# with scores and potential symbols and each symbol knowing exactly what links to it and so forth. +# +# Not so with image references. All possible targets (all possible image files) are known by <NaturalDocs::Project> early +# on and will remain consistent throughout execution. So because of that, we can get away with only storing reference +# counts with each image and determining exactly where a reference points to as we find them. +# +# Reference counts are stored with the image file information in <NaturalDocs::Project>. However, it is not loaded and +# saved to disk by it. Rather, it is regenerated by this package when it loads <ImageReferenceTable.nd>. +# NaturalDocs::Project only stores the last modification time (so it can add files to the build list if they've changed) and +# whether it had any references at all on the last run (so it knows whether it should care if they've changed.) +# ImageReferenceTable.nd stores each reference's target, width, and height. Whether their interpretations have changed is +# dealt with in the <Load()> function, again since the list of targets (image files) is constant. +# +# The package is based on <NaturalDocs::SourceDB>, so read it's documentation for more information on how it works. +# + + +############################################################################### +# Group: Variables + + +# +# var: extensionID +# The <ExtensionID> granted by <NaturalDocs::SourceDB>. +# +my $extensionID; + + + +############################################################################### +# Group: Files + + +# +# File: ImageReferenceTable.nd +# +# The data file which stores all the image references from the last run of Natural Docs. +# +# Format: +# +# > [Standard Binary Header] +# +# It starts with the standard binary header from <NaturalDocs::BinaryFile>. +# +# > [Image Reference String or undef] +# > [AString16: target file] +# > [UInt16: target width or 0] +# > [UInt16: target height or 0] +# +# For each <ImageReferenceString>, it's target, width, and height are stored. The target is needed so we can tell if it +# changed from the last run, and the dimensions are needed because if the target hasn't changed but the file's dimensions +# have, the source files need to be rebuilt. +# +# <ImageReferenceStrings> are encoded by <NaturalDocs::ImageReferenceTable::String>. +# +# > [AString16: definition file or undef] ... +# +# Then comes a series of AString16s for all the files that define the reference until it hits an undef. +# +# This whole series is repeated for each <ImageReferenceString> until it hits an undef. +# +# Revisions: +# +# 1.4: +# +# - The file was added to Natural Docs. +# + + + +############################################################################### +# Group: Functions + + +# +# Function: Register +# Registers the package with <NaturalDocs::SourceDB>. +# +sub Register + { + my $self = shift; + $extensionID = NaturalDocs::SourceDB->RegisterExtension($self, 0); + }; + + +# +# Function: Load +# +# Loads the data from <ImageReferenceTable.nd>. Returns whether it was successful. +# +sub Load # => bool + { + my $self = shift; + + if (NaturalDocs::Settings->RebuildData()) + { return 0; }; + + # The file format hasn't changed since it was introduced. + if (!NaturalDocs::BinaryFile->OpenForReading( NaturalDocs::Project->DataFile('ImageReferenceTable.nd') )) + { return 0; }; + + + # [Image Reference String or undef] + while (my $referenceString = NaturalDocs::ImageReferenceTable::String->FromBinaryFile()) + { + NaturalDocs::SourceDB->AddItem($extensionID, $referenceString, + NaturalDocs::ImageReferenceTable::Reference->New()); + + # [AString16: target file] + # [UInt16: target width or 0] + # [UInt16: target height or 0] + + my $targetFile = NaturalDocs::BinaryFile->GetAString16(); + my $width = NaturalDocs::BinaryFile->GetUInt16(); + my $height = NaturalDocs::BinaryFile->GetUInt16(); + + my $newTargetFile = $self->SetReferenceTarget($referenceString); + my $newWidth; + my $newHeight; + + if ($newTargetFile) + { + NaturalDocs::Project->AddImageFileReference($newTargetFile); + ($newWidth, $newHeight) = NaturalDocs::Project->ImageFileDimensions($newTargetFile); + }; + + my $rebuildDefinitions = ($newTargetFile ne $targetFile || $newWidth != $width || $newHeight != $height); + + + # [AString16: definition file or undef] ... + while (my $definitionFile = NaturalDocs::BinaryFile->GetAString16()) + { + NaturalDocs::SourceDB->AddDefinition($extensionID, $referenceString, $definitionFile); + + if ($rebuildDefinitions) + { NaturalDocs::Project->RebuildFile($definitionFile); }; + }; + }; + + + NaturalDocs::BinaryFile->Close(); + return 1; + }; + + +# +# Function: Save +# +# Saves the data to <ImageReferenceTable.nd>. +# +sub Save + { + my $self = shift; + + my $references = NaturalDocs::SourceDB->GetAllItemsHashRef($extensionID); + + NaturalDocs::BinaryFile->OpenForWriting( NaturalDocs::Project->DataFile('ImageReferenceTable.nd') ); + + while (my ($referenceString, $referenceObject) = each %$references) + { + # [Image Reference String or undef] + # [AString16: target file] + # [UInt16: target width or 0] + # [UInt16: target height or 0] + + NaturalDocs::ImageReferenceTable::String->ToBinaryFile($referenceString); + + my $target = $referenceObject->Target(); + my ($width, $height); + + if ($target) + { ($width, $height) = NaturalDocs::Project->ImageFileDimensions($target); }; + + NaturalDocs::BinaryFile->WriteAString16( $referenceObject->Target() ); + NaturalDocs::BinaryFile->WriteUInt16( ($width || 0) ); + NaturalDocs::BinaryFile->WriteUInt16( ($height || 0) ); + + # [AString16: definition file or undef] ... + + my $definitions = $referenceObject->GetAllDefinitionsHashRef(); + + foreach my $definition (keys %$definitions) + { NaturalDocs::BinaryFile->WriteAString16($definition); }; + + NaturalDocs::BinaryFile->WriteAString16(undef); + }; + + NaturalDocs::ImageReferenceTable::String->ToBinaryFile(undef); + + NaturalDocs::BinaryFile->Close(); + }; + + +# +# Function: AddReference +# +# Adds a new image reference. +# +sub AddReference #(FileName file, string referenceText) + { + my ($self, $file, $referenceText) = @_; + + my $referenceString = NaturalDocs::ImageReferenceTable::String->Make($file, $referenceText); + + if (!NaturalDocs::SourceDB->HasItem($extensionID, $referenceString)) + { + my $referenceObject = NaturalDocs::ImageReferenceTable::Reference->New(); + NaturalDocs::SourceDB->AddItem($extensionID, $referenceString, $referenceObject); + + my $target = $self->SetReferenceTarget($referenceString); + if ($target) + { NaturalDocs::Project->AddImageFileReference($target); }; + }; + + NaturalDocs::SourceDB->AddDefinition($extensionID, $referenceString, $file); + }; + + +# +# Function: OnDeletedDefinition +# +# Called for each definition deleted by <NaturalDocs::SourceDB>. This is called *after* the definition has been deleted from +# the database, so don't expect to be able to read it. +# +sub OnDeletedDefinition #(ImageReferenceString referenceString, FileName file, bool wasLastDefinition) + { + my ($self, $referenceString, $file, $wasLastDefinition) = @_; + + if ($wasLastDefinition) + { + my $referenceObject = NaturalDocs::SourceDB->GetItem($extensionID, $referenceString); + my $target = $referenceObject->Target(); + + if ($target) + { NaturalDocs::Project->DeleteImageFileReference($target); }; + + NaturalDocs::SourceDB->DeleteItem($extensionID, $referenceString); + }; + }; + + +# +# Function: GetReferenceTarget +# +# Returns the image file the reference resolves to, or undef if none. +# +# Parameters: +# +# sourceFile - The source <FileName> the reference appears in. +# text - The reference text. +# +sub GetReferenceTarget #(FileName sourceFile, string text) => FileName + { + my ($self, $sourceFile, $text) = @_; + + my $referenceString = NaturalDocs::ImageReferenceTable::String->Make($sourceFile, $text); + my $reference = NaturalDocs::SourceDB->GetItem($extensionID, $referenceString); + + if (!defined $reference) + { return undef; } + else + { return $reference->Target(); }; + }; + + +# +# Function: SetReferenceTarget +# +# Determines the best target for the passed <ImageReferenceString> and sets it on the +# <NaturalDocs::ImageReferenceTable::Reference> object. Returns the new target <FileName>. Does *not* add any source +# files to the bulid list. +# +sub SetReferenceTarget #(ImageReferenceString referenceString) => FileName + { + my ($self, $referenceString) = @_; + + my $referenceObject = NaturalDocs::SourceDB->GetItem($extensionID, $referenceString); + my ($sourcePath, $text) = NaturalDocs::ImageReferenceTable::String->InformationOf($referenceString); + + + # Try the path relative to the source file first. + + my $target; + + my $imageFile = NaturalDocs::File->JoinPaths($sourcePath, $text); + my $exists = NaturalDocs::Project->ImageFileExists($imageFile); + + + # Then try relative image directories. + + if (!$exists) + { + my $relativeImageDirectories = NaturalDocs::Settings->RelativeImageDirectories(); + + for (my $i = 0; $i < scalar @$relativeImageDirectories && !$exists; $i++) + { + $imageFile = NaturalDocs::File->JoinPaths($sourcePath, $relativeImageDirectories->[$i], 1); + $imageFile = NaturalDocs::File->JoinPaths($imageFile, $text); + + $exists = NaturalDocs::Project->ImageFileExists($imageFile); + }; + }; + + + # Then try absolute image directories. + + if (!$exists) + { + my $imageDirectories = NaturalDocs::Settings->ImageDirectories(); + + for (my $i = 0; $i < scalar @$imageDirectories && !$exists; $i++) + { + $imageFile = NaturalDocs::File->JoinPaths($imageDirectories->[$i], $text); + $exists = NaturalDocs::Project->ImageFileExists($imageFile); + }; + }; + + + if ($exists) + { $target = NaturalDocs::Project->ImageFileCapitalization($imageFile); }; + #else leave it as undef. + + $referenceObject->SetTarget($target); + return $target; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/ImageReferenceTable/Reference.pm b/docs/tool/Modules/NaturalDocs/ImageReferenceTable/Reference.pm new file mode 100644 index 00000000..10d0a96b --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/ImageReferenceTable/Reference.pm @@ -0,0 +1,44 @@ +############################################################################### +# +# Package: NaturalDocs::ImageReferenceTable::Reference +# +############################################################################### +# +# A class for references being tracked in <NaturalDocs::SourceDB>. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + + +package NaturalDocs::ImageReferenceTable::Reference; + +use base 'NaturalDocs::SourceDB::Item'; + + +use NaturalDocs::DefineMembers 'TARGET', 'Target()', 'SetTarget()', + 'NEEDS_REBUILD', 'NeedsRebuild()', 'SetNeedsRebuild()'; + + +# +# Variables: Members +# +# The following constants are indexes into the object array. +# +# TARGET - The image <FileName> this reference resolves to, or undef if none. +# + + +# +# Functions: Member Functions +# +# Target - Returns the image <FileName> this reference resolves to, or undef if none. +# SetTarget - Replaces the image <FileName> this reference resolves to, or undef if none. +# + + +1; diff --git a/docs/tool/Modules/NaturalDocs/ImageReferenceTable/String.pm b/docs/tool/Modules/NaturalDocs/ImageReferenceTable/String.pm new file mode 100644 index 00000000..982e9f13 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/ImageReferenceTable/String.pm @@ -0,0 +1,110 @@ +############################################################################### +# +# Package: NaturalDocs::ImageReferenceTable::String +# +############################################################################### +# +# A package for creating and managing <ImageReferenceStrings>. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + + +package NaturalDocs::ImageReferenceTable::String; + + +# +# Type: ImageReferenceString +# +# A string representing a unique image reference. It's composed of the reference text and the directory of the source file. +# The source file name itself isn't included because two files in the same directory with the same reference text will always go +# to the same targets. +# + + +# +# Function: Make +# +# Converts a source <FileName> and the reference text to an <ImageReferenceString>. +# +sub Make #(FileName sourceFile, string text) => ImageReferenceString + { + my ($self, $sourceFile, $text) = @_; + + my $path = NaturalDocs::File->NoFileName($sourceFile); + + # Condense whitespace and remove any separator characters. + $path =~ tr/ \t\r\n\x1C/ /s; + $text =~ tr/ \t\r\n\x1C/ /s; + + return $path . "\x1C" . $text; + }; + + +# +# Function: InformationOf +# +# Returns the information contained in the <ImageReferenceString> as the array ( path, text ). +# +sub InformationOf #(ImageReferenceString referenceString) + { + my ($self, $referenceString) = @_; + return split(/\x1C/, $referenceString); + }; + + +# +# Function: ToBinaryFile +# +# Writes an <ImageReferenceString> to <NaturalDocs::BinaryFile>. Can also encode an undef. +# +# Format: +# +# > [AString16: path] [AString16: reference text] ... +# +# Undef is represented by the first AString16 being undef. +# +sub ToBinaryFile #(ImageReferenceString referenceString) + { + my ($self, $referenceString) = @_; + + if (defined $referenceString) + { + my ($path, $text) = split(/\x1C/, $referenceString); + + NaturalDocs::BinaryFile->WriteAString16($path); + NaturalDocs::BinaryFile->WriteAString16($text); + } + else + { + NaturalDocs::BinaryFile->WriteAString16(undef); + }; + }; + + +# +# Function: FromBinaryFile +# +# Loads an <ImageReferenceString> or undef from <NaturalDocs::BinaryFile> and returns it. +# +sub FromBinaryFile + { + my $self = shift; + + my $path = NaturalDocs::BinaryFile->GetAString16(); + + if (!defined $path) + { return undef; }; + + my $text = NaturalDocs::BinaryFile->GetAString16(); + + return $path . "\x1C" . $text; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Languages.pm b/docs/tool/Modules/NaturalDocs/Languages.pm new file mode 100644 index 00000000..dde54b9d --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Languages.pm @@ -0,0 +1,1475 @@ +############################################################################### +# +# Package: NaturalDocs::Languages +# +############################################################################### +# +# A package to manage all the programming languages Natural Docs supports. +# +# Usage and Dependencies: +# +# - Prior to use, <NaturalDocs::Settings> must be initialized and <Load()> must be called. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use Text::Wrap(); + +use NaturalDocs::Languages::Prototype; + +use NaturalDocs::Languages::Base; +use NaturalDocs::Languages::Simple; +use NaturalDocs::Languages::Advanced; + +use NaturalDocs::Languages::Perl; +use NaturalDocs::Languages::CSharp; +use NaturalDocs::Languages::ActionScript; + +use NaturalDocs::Languages::Ada; +use NaturalDocs::Languages::PLSQL; +use NaturalDocs::Languages::Pascal; +use NaturalDocs::Languages::Tcl; + +use strict; +use integer; + +package NaturalDocs::Languages; + + +############################################################################### +# Group: Variables + + +# +# handle: FH_LANGUAGES +# +# The file handle used for writing to <Languages.txt>. +# + + +# +# hash: languages +# +# A hash of all the defined languages. The keys are the all-lowercase language names, and the values are +# <NaturalDocs::Languages::Base>-derived objects. +# +my %languages; + +# +# hash: extensions +# +# A hash of all the defined languages' extensions. The keys are the all-lowercase extensions, and the values are the +# all-lowercase names of the languages that defined them. +# +my %extensions; + +# +# hash: shebangStrings +# +# A hash of all the defined languages' strings to search for in the shebang (#!) line. The keys are the all-lowercase strings, and +# the values are the all-lowercase names of the languages that defined them. +# +my %shebangStrings; + +# +# hash: shebangFiles +# +# A hash of all the defined languages for files where it needs to be found via shebang strings. The keys are the file names, +# and the values are language names, or undef if the file isn't supported. These values should be filled in the first time +# each file is parsed for a shebang string so that it doesn't have to be done multiple times. +# +my %shebangFiles; + +# +# array: mainLanguageNames +# +# An array of the language names that are defined in the main <Languages.txt>. +# +my @mainLanguageNames; + + + +############################################################################### +# Group: Files + + +# +# File: Languages.txt +# +# The configuration file that defines or overrides the language definitions for Natural Docs. One version sits in Natural Docs' +# configuration directory, and another can be in a project directory to add to or override them. +# +# > # [comments] +# +# Everything after a # symbol is ignored. However, for this particular file, comments can only appear on their own lines. +# They cannot appear after content on the same line. +# +# > Format: [version] +# +# Specifies the file format version of the file. +# +# +# Sections: +# +# > Ignore[d] Extension[s]: [extension] [extension] ... +# +# Causes the listed file extensions to be ignored, even if they were previously defined to be part of a language. The list is +# space-separated. ex. "Ignore Extensions: cvs txt" +# +# +# > Language: [name] +# +# Creates a new language. Everything underneath applies to this language until the next one. Names can use any +# characters. +# +# The languages "Text File" and "Shebang Script" have special meanings. Text files are considered all comment and don't +# have comment symbols. Shebang scripts have their language determined by the shebang string and automatically +# include files with no extension in addition to the extensions defined. +# +# If "Text File" doesn't define ignored prefixes, a package separator, or enum value behavior, those settings will be copied +# from the language with the most files in the source tree. +# +# +# > Alter Language: [name] +# +# Alters an existing language. Everything underneath it overrides the previous settings until the next one. Note that if a +# property has an [Add/Replace] form and that property has already been defined, you have to specify whether you're adding +# to or replacing the defined list. +# +# +# Language Properties: +# +# > Extension[s]: [extension] [extension] ... +# > [Add/Replace] Extension[s]: ... +# +# Defines file extensions for the language's source files. The list is space-separated. ex. "Extensions: c cpp". You can use +# extensions that were previously used by another language to redefine them. +# +# +# > Shebang String[s]: [string] [string] ... +# > [Add/Replace] Shebang String[s]: ... +# +# Defines a list of strings that can appear in the shebang (#!) line to designate that it's part of this language. They can +# appear anywhere in the line, so "php" will work for "#!/user/bin/php4". You can use strings that were previously used by +# another language to redefine them. +# +# +# > Ignore[d] Prefix[es] in Index: [prefix] [prefix] ... +# > Ignore[d] [Topic Type] Prefix[es] in Index: [prefix] [prefix] ... +# > [Add/Replace] Ignore[d] Prefix[es] in Index: ... +# > [Add/Replace] Ignore[d] [Topic Type] Prefix[es] in Index: ... +# +# Specifies prefixes that should be ignored when sorting symbols for an index. Can be specified in general or for a specific +# <TopicType>. The prefixes will still appear, the symbols will just be sorted as if they're not there. For example, specifying +# "ADO_" for functions will mean that "ADO_DoSomething" will appear under D instead of A. +# +# +# Basic Language Support Properties: +# +# These attributes are only available for languages with basic language support. +# +# +# > Line Comment[s]: [symbol] [symbol] ... +# +# Defines a space-separated list of symbols that are used for line comments, if any. ex. "Line Comment: //". +# +# +# > Block Comment[s]: [opening symbol] [closing symbol] [opening symbol] [closing symbol] ... +# +# Defines a space-separated list of symbol pairs that are used for block comments, if any. ex. "Block Comment: /* */". +# +# +# > Package Separator: [symbol] +# +# Defines the default package separator symbol, such as . or ::. This is for presentation only and will not affect how +# Natural Docs links are parsed. The default is a dot. +# +# +# > [Topic Type] Prototype Ender[s]: [symbol] [symbol] ... +# +# When defined, Natural Docs will attempt to collect prototypes from the code following the specified <TopicType>. It grabs +# code until the first ender symbol or the next Natural Docs comment, and if it contains the topic name, it serves as its +# prototype. Use \n to specify a line break. ex. "Function Prototype Enders: { ;", "Variable Prototype Enders: = ;". +# +# +# > Line Extender: [symbol] +# +# Defines the symbol that allows a prototype to span multiple lines if normally a line break would end it. +# +# +# > Enum Values: [global|under type|under parent] +# +# Defines how enum values are referenced. The default is global. +# +# global - Values are always global, referenced as 'value'. +# under type - Values are under the enum type, referenced as 'package.enum.value'. +# under parent - Values are under the enum's parent, referenced as 'package.value'. +# +# +# > Perl Package: [perl package] +# +# Specifies the Perl package used to fine-tune the language behavior in ways too complex to do in this file. +# +# +# Full Language Support Properties: +# +# These attributes are only available for languages with full language support. +# +# +# > Full Language Support: [perl package] +# +# Specifies the Perl package that has the parsing routines necessary for full language support. +# +# +# Revisions: +# +# 1.32: +# +# - Package Separator is now a basic language support only property. +# - Added Enum Values setting. +# +# 1.3: +# +# - The file was introduced. + + +############################################################################### +# Group: File Functions + + +# +# Function: Load +# +# Loads both the master and the project version of <Languages.txt>. +# +sub Load + { + my $self = shift; + + # Hashrefs where the keys are all-lowercase extensions/shebang strings, and the values are arrayrefs of the languages + # that defined them, earliest first, all lowercase. + my %tempExtensions; + my %tempShebangStrings; + + $self->LoadFile(1, \%tempExtensions, \%tempShebangStrings); # Main + + if (!exists $languages{'shebang script'}) + { NaturalDocs::ConfigFile->AddError('You must define "Shebang Script" in the main languages file.'); }; + if (!exists $languages{'text file'}) + { NaturalDocs::ConfigFile->AddError('You must define "Text File" in the main languages file.'); }; + + my $errorCount = NaturalDocs::ConfigFile->ErrorCount(); + + if ($errorCount) + { + NaturalDocs::ConfigFile->PrintErrorsAndAnnotateFile(); + NaturalDocs::Error->SoftDeath('There ' . ($errorCount == 1 ? 'is an error' : 'are ' . $errorCount . ' errors') + . ' in ' . NaturalDocs::Project->MainConfigFile('Languages.txt')); + } + + + $self->LoadFile(0, \%tempExtensions, \%tempShebangStrings); # User + + $errorCount = NaturalDocs::ConfigFile->ErrorCount(); + + if ($errorCount) + { + NaturalDocs::ConfigFile->PrintErrorsAndAnnotateFile(); + NaturalDocs::Error->SoftDeath('There ' . ($errorCount == 1 ? 'is an error' : 'are ' . $errorCount . ' errors') + . ' in ' . NaturalDocs::Project->UserConfigFile('Languages.txt')); + }; + + + # Convert the temp hashes into the real ones. + + while (my ($extension, $languages) = each %tempExtensions) + { + $extensions{$extension} = $languages->[-1]; + }; + while (my ($shebangString, $languages) = each %tempShebangStrings) + { + $shebangStrings{$shebangString} = $languages->[-1]; + }; + }; + + +# +# Function: LoadFile +# +# Loads a particular version of <Languages.txt>. +# +# Parameters: +# +# isMain - Whether the file is the main file or not. +# tempExtensions - A hashref where the keys are all-lowercase extensions, and the values are arrayrefs of the all-lowercase +# names of the languages that defined them, earliest first. It will be changed by this function. +# tempShebangStrings - A hashref where the keys are all-lowercase shebang strings, and the values are arrayrefs of the +# all-lowercase names of the languages that defined them, earliest first. It will be changed by this +# function. +# +sub LoadFile #(isMain, tempExtensions, tempShebangStrings) + { + my ($self, $isMain, $tempExtensions, $tempShebangStrings) = @_; + + my ($file, $status); + + if ($isMain) + { + $file = NaturalDocs::Project->MainConfigFile('Languages.txt'); + $status = NaturalDocs::Project->MainConfigFileStatus('Languages.txt'); + } + else + { + $file = NaturalDocs::Project->UserConfigFile('Languages.txt'); + $status = NaturalDocs::Project->UserConfigFileStatus('Languages.txt'); + }; + + + my $version; + + # An array of properties for the current language. Each entry is the three consecutive values ( lineNumber, keyword, value ). + my @properties; + + if ($version = NaturalDocs::ConfigFile->Open($file)) + { + # The format hasn't changed significantly since the file was introduced. + + if ($status == ::FILE_CHANGED()) + { + NaturalDocs::Project->ReparseEverything(); + NaturalDocs::SymbolTable->RebuildAllIndexes(); # Because the ignored prefixes could change. + }; + + my ($keyword, $value, $comment); + + while (($keyword, $value, $comment) = NaturalDocs::ConfigFile->GetLine()) + { + $value .= $comment; + $value =~ s/^ //; + + # Process previous properties. + if (($keyword eq 'language' || $keyword eq 'alter language') && scalar @properties) + { + if ($isMain && $properties[1] eq 'language') + { push @mainLanguageNames, $properties[2]; }; + + $self->ProcessProperties(\@properties, $version, $tempExtensions, $tempShebangStrings); + @properties = ( ); + }; + + if ($keyword =~ /^ignored? extensions?$/) + { + $value =~ tr/.*//d; + my @extensions = split(/ /, lc($value)); + + foreach my $extension (@extensions) + { delete $tempExtensions->{$extension}; }; + } + else + { + push @properties, NaturalDocs::ConfigFile->LineNumber(), $keyword, $value; + }; + }; + + if (scalar @properties) + { + if ($isMain && $properties[1] eq 'language') + { push @mainLanguageNames, $properties[2]; }; + + $self->ProcessProperties(\@properties, $version, $tempExtensions, $tempShebangStrings); + }; + } + + else # couldn't open file + { + if ($isMain) + { die "Couldn't open languages file " . $file . "\n"; }; + }; + }; + + +# +# Function: ProcessProperties +# +# Processes an array of language properties from <Languages.txt>. +# +# Parameters: +# +# properties - An arrayref of properties where each entry is the three consecutive values ( lineNumber, keyword, value ). +# It must start with the Language or Alter Language property. +# version - The <VersionInt> of the file. +# tempExtensions - A hashref where the keys are all-lowercase extensions, and the values are arrayrefs of the all-lowercase +# names of the languages that defined them, earliest first. It will be changed by this function. +# tempShebangStrings - A hashref where the keys are all-lowercase shebang strings, and the values are arrayrefs of the +# all-lowercase names of the languages that defined them, earliest first. It will be changed by this +# function. +# +sub ProcessProperties #(properties, version, tempExtensions, tempShebangStrings) + { + my ($self, $properties, $version, $tempExtensions, $tempShebangStrings) = @_; + + + # First validate the name and check whether the language has full support. + + my $language; + my $fullLanguageSupport; + my ($lineNumber, $languageKeyword, $languageName) = @$properties[0..2]; + my $lcLanguageName = lc($languageName); + my ($keyword, $value); + + if ($languageKeyword eq 'alter language') + { + $language = $languages{$lcLanguageName}; + + if (!defined $language) + { + NaturalDocs::ConfigFile->AddError('The language ' . $languageName . ' is not defined.', $lineNumber); + return; + } + else + { + $fullLanguageSupport = (!$language->isa('NaturalDocs::Languages::Simple')); + }; + } + + elsif ($languageKeyword eq 'language') + { + if (exists $languages{$lcLanguageName}) + { + NaturalDocs::ConfigFile->AddError('The language ' . $value . ' is already defined. Use "Alter Language" if you want ' + . 'to override its settings.', $lineNumber); + return; + }; + + # Case is important with these two. + if ($lcLanguageName eq 'shebang script') + { $languageName = 'Shebang Script'; } + elsif ($lcLanguageName eq 'text file') + { $languageName = 'Text File'; }; + + + # Go through the properties looking for whether the language has basic or full support and which package to use to create + # it. + + for (my $i = 3; $i < scalar @$properties; $i += 3) + { + ($lineNumber, $keyword, $value) = @$properties[$i..$i+2]; + + if ($keyword eq 'full language support') + { + $fullLanguageSupport = 1; + + eval + { + $language = $value->New($languageName); + }; + if ($::EVAL_ERROR) + { + NaturalDocs::ConfigFile->AddError('Could not create ' . $value . ' object.', $lineNumber); + return; + }; + + last; + } + + elsif ($keyword eq 'perl package') + { + eval + { + $language = $value->New($languageName); + }; + if ($::EVAL_ERROR) + { + NaturalDocs::ConfigFile->AddError('Could not create ' . $value . ' object.', $lineNumber); + return; + }; + }; + }; + + # If $language was not created by now, it's a generic basic support language. + if (!defined $language) + { $language = NaturalDocs::Languages::Simple->New($languageName); }; + + $languages{$lcLanguageName} = $language; + } + + else # not language or alter language + { + NaturalDocs::ConfigFile->AddError('You must start this line with "Language", "Alter Language", or "Ignore Extensions".', + $lineNumber); + return; + }; + + + # Decode the properties. + + for (my $i = 3; $i < scalar @$properties; $i += 3) + { + ($lineNumber, $keyword, $value) = @$properties[$i..$i+2]; + + if ($keyword =~ /^(?:(add|replace) )?extensions?$/) + { + my $command = $1; + + + # Remove old extensions. + + if (defined $language->Extensions() && $command eq 'replace') + { + foreach my $extension (@{$language->Extensions()}) + { + if (exists $tempExtensions->{$extension}) + { + my $languages = $tempExtensions->{$extension}; + my $i = 0; + + while ($i < scalar @$languages) + { + if ($languages->[$i] eq $lcLanguageName) + { splice(@$languages, $i, 1); } + else + { $i++; }; + }; + + if (!scalar @$languages) + { delete $tempExtensions->{$extension}; }; + }; + }; + }; + + + # Add new extensions. + + # Ignore stars and dots so people could use .ext or *.ext. + $value =~ s/\*\.|\.//g; + + my @extensions = split(/ /, lc($value)); + + foreach my $extension (@extensions) + { + if (!exists $tempExtensions->{$extension}) + { $tempExtensions->{$extension} = [ ]; }; + + push @{$tempExtensions->{$extension}}, $lcLanguageName; + }; + + + # Set the extensions for the language object. + + if (defined $language->Extensions()) + { + if ($command eq 'add') + { push @extensions, @{$language->Extensions()}; } + elsif (!$command) + { + NaturalDocs::ConfigFile->AddError('You need to specify whether you are adding to or replacing the list of extensions.', + $lineNumber); + }; + }; + + $language->SetExtensions(\@extensions); + } + + elsif ($keyword =~ /^(?:(add|replace) )?shebang strings?$/) + { + my $command = $1; + + + # Remove old strings. + + if (defined $language->ShebangStrings() && $command eq 'replace') + { + foreach my $shebangString (@{$language->ShebangStrings()}) + { + if (exists $tempShebangStrings->{$shebangString}) + { + my $languages = $tempShebangStrings->{$shebangString}; + my $i = 0; + + while ($i < scalar @$languages) + { + if ($languages->[$i] eq $lcLanguageName) + { splice(@$languages, $i, 1); } + else + { $i++; }; + }; + + if (!scalar @$languages) + { delete $tempShebangStrings->{$shebangString}; }; + }; + }; + }; + + + # Add new strings. + + my @shebangStrings = split(/ /, lc($value)); + + foreach my $shebangString (@shebangStrings) + { + if (!exists $tempShebangStrings->{$shebangString}) + { $tempShebangStrings->{$shebangString} = [ ]; }; + + push @{$tempShebangStrings->{$shebangString}}, $lcLanguageName; + }; + + + # Set the strings for the language object. + + if (defined $language->ShebangStrings()) + { + if ($command eq 'add') + { push @shebangStrings, @{$language->ShebangStrings()}; } + elsif (!$command) + { + NaturalDocs::ConfigFile->AddError('You need to specify whether you are adding to or replacing the list of shebang ' + . 'strings.', $lineNumber); + }; + }; + + $language->SetShebangStrings(\@shebangStrings); + } + + elsif ($keyword eq 'package separator') + { + if ($fullLanguageSupport) + { + # Prior to 1.32, package separator was used with full language support too. Accept it without complaining, even though + # we ignore it. + if ($version >= NaturalDocs::Version->FromString('1.32')) + { + NaturalDocs::ConfigFile->AddError('You cannot define this property when using full language support.', $lineNumber); + }; + } + else + { $language->SetPackageSeparator($value); }; + } + + elsif ($keyword =~ /^(?:(add|replace) )?ignored? (?:(.+) )?prefix(?:es)? in index$/) + { + my ($command, $topicName) = ($1, $2); + my $topicType; + + if ($topicName) + { + if (!( ($topicType, undef) = NaturalDocs::Topics->NameInfo($topicName) )) + { + NaturalDocs::ConfigFile->AddError($topicName . ' is not a defined topic type.', $lineNumber); + }; + } + else + { $topicType = ::TOPIC_GENERAL(); }; + + if ($topicType) + { + my @prefixes; + + if (defined $language->IgnoredPrefixesFor($topicType)) + { + if ($command eq 'add') + { @prefixes = @{$language->IgnoredPrefixesFor($topicType)}; } + elsif (!$command) + { + NaturalDocs::ConfigFile->AddError('You need to specify whether you are adding to or replacing the list of ' + . 'ignored prefixes.', $lineNumber); + }; + }; + + push @prefixes, split(/ /, $value); + $language->SetIgnoredPrefixesFor($topicType, \@prefixes); + }; + } + + elsif ($keyword eq 'full language support' || $keyword eq 'perl package') + { + if ($languageKeyword eq 'alter language') + { + NaturalDocs::ConfigFile->AddError('You cannot use ' . $keyword . ' with Alter Language.', $lineNumber); + }; + # else ignore it. + } + + elsif ($keyword =~ /^line comments?$/) + { + if ($fullLanguageSupport) + { + NaturalDocs::ConfigFile->AddError('You cannot define this property when using full language support.', $lineNumber); + } + else + { + my @symbols = split(/ /, $value); + $language->SetLineCommentSymbols(\@symbols); + }; + } + + elsif ($keyword =~ /^block comments?$/) + { + if ($fullLanguageSupport) + { + NaturalDocs::ConfigFile->AddError('You cannot define this property when using full language support.', $lineNumber); + } + else + { + my @symbols = split(/ /, $value); + + if ((scalar @symbols) % 2 == 0) + { $language->SetBlockCommentSymbols(\@symbols); } + else + { NaturalDocs::ConfigFile->AddError('Block comment symbols must appear in pairs.', $lineNumber); }; + }; + } + + elsif ($keyword =~ /^(?:(.+) )?prototype enders?$/) + { + if ($fullLanguageSupport) + { + NaturalDocs::ConfigFile->AddError('You cannot define this property when using full language support.', $lineNumber); + } + else + { + my $topicName = $1; + my $topicType; + + if ($topicName) + { + if (!( ($topicType, undef) = NaturalDocs::Topics->NameInfo($topicName) )) + { + NaturalDocs::ConfigFile->AddError($topicName . ' is not a defined topic type.', $lineNumber); + }; + } + else + { $topicType = ::TOPIC_GENERAL(); }; + + if ($topicType) + { + $value =~ s/\\n/\n/g; + my @symbols = split(/ /, $value); + $language->SetPrototypeEndersFor($topicType, \@symbols); + }; + }; + } + + elsif ($keyword eq 'line extender') + { + if ($fullLanguageSupport) + { + NaturalDocs::ConfigFile->AddError('You cannot define this property when using full language support.', $lineNumber); + } + else + { + $language->SetLineExtender($value); + }; + } + + elsif ($keyword eq 'enum values') + { + if ($fullLanguageSupport) + { + NaturalDocs::ConfigFile->AddError('You cannot define this property when using full language support.', $lineNumber); + } + else + { + $value = lc($value); + my $constant; + + if ($value eq 'global') + { $constant = ::ENUM_GLOBAL(); } + elsif ($value eq 'under type') + { $constant = ::ENUM_UNDER_TYPE(); } + elsif ($value eq 'under parent') + { $constant = ::ENUM_UNDER_PARENT(); }; + + if (defined $constant) + { $language->SetEnumValues($constant); } + else + { + NaturalDocs::ConfigFile->AddError('Enum Values must be "Global", "Under Type", or "Under Parent".', $lineNumber); + }; + }; + } + + else + { + NaturalDocs::ConfigFile->AddError($keyword . ' is not a valid keyword.', $lineNumber); + }; + }; + }; + + +# +# Function: Save +# +# Saves the main and user versions of <Languages.txt>. +# +sub Save + { + my $self = shift; + + $self->SaveFile(1); # Main + $self->SaveFile(0); # User + }; + + +# +# Function: SaveFile +# +# Saves a particular version of <Topics.txt>. +# +# Parameters: +# +# isMain - Whether the file is the main file or not. +# +sub SaveFile #(isMain) + { + my ($self, $isMain) = @_; + + my $file; + + if ($isMain) + { + if (NaturalDocs::Project->MainConfigFileStatus('Languages.txt') == ::FILE_SAME()) + { return; }; + $file = NaturalDocs::Project->MainConfigFile('Languages.txt'); + } + else + { + # Have to check the main too because this file lists the languages defined there. + if (NaturalDocs::Project->UserConfigFileStatus('Languages.txt') == ::FILE_SAME() && + NaturalDocs::Project->MainConfigFileStatus('Languages.txt') == ::FILE_SAME()) + { return; }; + $file = NaturalDocs::Project->UserConfigFile('Languages.txt'); + }; + + + # Array of segments, with each being groups of three consecutive entries. The first is the keyword ('language' or + # 'alter language'), the second is the value, and the third is a hashref of all the properties. + # - For properties that can accept a topic type, the property values are hashrefs mapping topic types to the values. + # - For properties that can accept 'add' or 'replace', there is an additional property ending in 'command' that stores it. + # - For properties that can accept both, the 'command' thing is applied to the topic types rather than the properties. + my @segments; + + my @ignoredExtensions; + + my $currentProperties; + my $version; + + if ($version = NaturalDocs::ConfigFile->Open($file)) + { + # We can assume the file is valid. + + while (my ($keyword, $value, $comment) = NaturalDocs::ConfigFile->GetLine()) + { + $value .= $comment; + $value =~ s/^ //; + + if ($keyword eq 'language') + { + $currentProperties = { }; + + # Case is important with these two. + if (lc($value) eq 'shebang script') + { $value = 'Shebang Script'; } + elsif (lc($value) eq 'text file') + { $value = 'Text File'; }; + + push @segments, 'language', $value, $currentProperties; + } + + elsif ($keyword eq 'alter language') + { + $currentProperties = { }; + push @segments, 'alter language', $languages{lc($value)}->Name(), $currentProperties; + } + + elsif ($keyword =~ /^ignored? extensions?$/) + { + $value =~ tr/*.//d; + push @ignoredExtensions, split(/ /, $value); + } + + elsif ($keyword eq 'package separator' || $keyword eq 'full language support' || $keyword eq 'perl package' || + $keyword eq 'line extender' || $keyword eq 'enum values') + { + $currentProperties->{$keyword} = $value; + } + + elsif ($keyword =~ /^line comments?$/) + { + $currentProperties->{'line comments'} = $value; + } + elsif ($keyword =~ /^block comments?$/) + { + $currentProperties->{'block comments'} = $value; + } + + elsif ($keyword =~ /^(?:(add|replace) )?extensions?$/) + { + my $command = $1; + + if ($command eq 'add' && exists $currentProperties->{'extensions'}) + { $currentProperties->{'extensions'} .= ' ' . $value; } + else + { + $currentProperties->{'extensions'} = $value; + $currentProperties->{'extensions command'} = $command; + }; + } + + elsif ($keyword =~ /^(?:(add|replace) )?shebang strings?$/) + { + my $command = $1; + + if ($command eq 'add' && exists $currentProperties->{'shebang strings'}) + { $currentProperties->{'shebang strings'} .= ' ' . $value; } + else + { + $currentProperties->{'shebang strings'} = $value; + $currentProperties->{'shebang strings command'} = $command; + }; + } + + elsif ($keyword =~ /^(?:(.+) )?prototype enders?$/) + { + my $topicName = $1; + my $topicType; + + if ($topicName) + { ($topicType, undef) = NaturalDocs::Topics->NameInfo($topicName); } + else + { $topicType = ::TOPIC_GENERAL(); }; + + my $currentTypeProperties = $currentProperties->{'prototype enders'}; + + if (!defined $currentTypeProperties) + { + $currentTypeProperties = { }; + $currentProperties->{'prototype enders'} = $currentTypeProperties; + }; + + $currentTypeProperties->{$topicType} = $value; + } + + elsif ($keyword =~ /^(?:(add|replace) )?ignored? (?:(.+) )?prefix(?:es)? in index$/) + { + my ($command, $topicName) = ($1, $2); + my $topicType; + + if ($topicName) + { ($topicType, undef) = NaturalDocs::Topics->NameInfo($topicName); } + else + { $topicType = ::TOPIC_GENERAL(); }; + + my $currentTypeProperties = $currentProperties->{'ignored prefixes in index'}; + + if (!defined $currentTypeProperties) + { + $currentTypeProperties = { }; + $currentProperties->{'ignored prefixes in index'} = $currentTypeProperties; + }; + + if ($command eq 'add' && exists $currentTypeProperties->{$topicType}) + { $currentTypeProperties->{$topicType} .= ' ' . $value; } + else + { + $currentTypeProperties->{$topicType} = $value; + $currentTypeProperties->{$topicType . ' command'} = $command; + }; + }; + }; + + NaturalDocs::ConfigFile->Close(); + }; + + + if (!open(FH_LANGUAGES, '>' . $file)) + { + # The main file may be on a shared volume or some other place the user doesn't have write access to. Since this is only to + # reformat the file, we can ignore the failure. + if ($isMain) + { return; } + else + { die "Couldn't save " . $file; }; + }; + + print FH_LANGUAGES 'Format: ' . NaturalDocs::Settings->TextAppVersion() . "\n\n"; + + # Remember the 80 character limit. + + if ($isMain) + { + print FH_LANGUAGES + "# This is the main Natural Docs languages file. If you change anything here,\n" + . "# it will apply to EVERY PROJECT you use Natural Docs on. If you'd like to\n" + . "# change something for just one project, edit the Languages.txt in its project\n" + . "# directory instead.\n"; + } + else + { + print FH_LANGUAGES + "# This is the Natural Docs languages file for this project. If you change\n" + . "# anything here, it will apply to THIS PROJECT ONLY. If you'd like to change\n" + . "# something for all your projects, edit the Languages.txt in Natural Docs'\n" + . "# Config directory instead.\n\n\n"; + + if (scalar @ignoredExtensions == 1) + { + print FH_LANGUAGES + 'Ignore Extension: ' . $ignoredExtensions[0] . "\n"; + } + elsif (scalar @ignoredExtensions) + { + print FH_LANGUAGES + 'Ignore Extensions: ' . join(' ', @ignoredExtensions) . "\n"; + } + else + { + print FH_LANGUAGES + "# You can prevent certain file extensions from being scanned like this:\n" + . "# Ignore Extensions: [extension] [extension] ...\n" + }; + }; + + print FH_LANGUAGES + "\n\n" + . "#-------------------------------------------------------------------------------\n" + . "# SYNTAX:\n" + . "#\n" + . "# Unlike other Natural Docs configuration files, in this file all comments\n" + . "# MUST be alone on a line. Some languages deal with the # character, so you\n" + . "# cannot put comments on the same line as content.\n" + . "#\n" + . "# Also, all lists are separated with spaces, not commas, again because some\n" + . "# languages may need to use them.\n" + . "#\n"; + + if ($isMain) + { + print FH_LANGUAGES + "# Language: [name]\n" + . "# Defines a new language. Its name can use any characters.\n" + . "#\n"; + } + else + { + print FH_LANGUAGES + "# Language: [name]\n" + . "# Alter Language: [name]\n" + . "# Defines a new language or alters an existing one. Its name can use any\n" + . "# characters. If any of the properties below have an add/replace form, you\n" + . "# must use that when using Alter Language.\n" + . "#\n"; + }; + + print FH_LANGUAGES + "# The language Shebang Script is special. It's entry is only used for\n" + . "# extensions, and files with those extensions have their shebang (#!) lines\n" + . "# read to determine the real language of the file. Extensionless files are\n" + . "# always treated this way.\n" + . "#\n" + . "# The language Text File is also special. It's treated as one big comment\n" + . "# so you can put Natural Docs content in them without special symbols. Also,\n" + . "# if you don't specify a package separator, ignored prefixes, or enum value\n" + . "# behavior, it will copy those settings from the language that is used most\n" + . "# in the source tree.\n" + . "#\n" + . "# Extensions: [extension] [extension] ...\n"; + + if ($isMain) + { + print FH_LANGUAGES + "# Defines the file extensions of the language's source files. You can use *\n" + . "# to mean any undefined extension.\n" + . "#\n" + . "# Shebang Strings: [string] [string] ...\n" + . "# Defines a list of strings that can appear in the shebang (#!) line to\n" + . "# designate that it's part of the language.\n" + . "#\n"; + } + else + { + print FH_LANGUAGES + "# [Add/Replace] Extensions: [extension] [extension] ...\n" + . "# Defines the file extensions of the language's source files. You can\n" + . "# redefine extensions found in the main languages file. You can use * to\n" + . "# mean any undefined extension.\n" + . "#\n" + . "# Shebang Strings: [string] [string] ...\n" + . "# [Add/Replace] Shebang Strings: [string] [string] ...\n" + . "# Defines a list of strings that can appear in the shebang (#!) line to\n" + . "# designate that it's part of the language. You can redefine strings found\n" + . "# in the main languages file.\n" + . "#\n"; + }; + + print FH_LANGUAGES + "# Ignore Prefixes in Index: [prefix] [prefix] ...\n" + . (!$isMain ? "# [Add/Replace] Ignored Prefixes in Index: [prefix] [prefix] ...\n#\n" : '') + . "# Ignore [Topic Type] Prefixes in Index: [prefix] [prefix] ...\n" + . (!$isMain ? "# [Add/Replace] Ignored [Topic Type] Prefixes in Index: [prefix] [prefix] ...\n" : '') + . "# Specifies prefixes that should be ignored when sorting symbols in an\n" + . "# index. Can be specified in general or for a specific topic type.\n" + . "#\n" + . "#------------------------------------------------------------------------------\n" + . "# For basic language support only:\n" + . "#\n" + . "# Line Comments: [symbol] [symbol] ...\n" + . "# Defines a space-separated list of symbols that are used for line comments,\n" + . "# if any.\n" + . "#\n" + . "# Block Comments: [opening sym] [closing sym] [opening sym] [closing sym] ...\n" + . "# Defines a space-separated list of symbol pairs that are used for block\n" + . "# comments, if any.\n" + . "#\n" + . "# Package Separator: [symbol]\n" + . "# Defines the default package separator symbol. The default is a dot.\n" + . "#\n" + . "# [Topic Type] Prototype Enders: [symbol] [symbol] ...\n" + . "# When defined, Natural Docs will attempt to get a prototype from the code\n" + . "# immediately following the topic type. It stops when it reaches one of\n" + . "# these symbols. Use \\n for line breaks.\n" + . "#\n" + . "# Line Extender: [symbol]\n" + . "# Defines the symbol that allows a prototype to span multiple lines if\n" + . "# normally a line break would end it.\n" + . "#\n" + . "# Enum Values: [global|under type|under parent]\n" + . "# Defines how enum values are referenced. The default is global.\n" + . "# global - Values are always global, referenced as 'value'.\n" + . "# under type - Values are under the enum type, referenced as\n" + . "# 'package.enum.value'.\n" + . "# under parent - Values are under the enum's parent, referenced as\n" + . "# 'package.value'.\n" + . "#\n" + . "# Perl Package: [perl package]\n" + . "# Specifies the Perl package used to fine-tune the language behavior in ways\n" + . "# too complex to do in this file.\n" + . "#\n" + . "#------------------------------------------------------------------------------\n" + . "# For full language support only:\n" + . "#\n" + . "# Full Language Support: [perl package]\n" + . "# Specifies the Perl package that has the parsing routines necessary for full\n" + . "# language support.\n" + . "#\n" + . "#-------------------------------------------------------------------------------\n\n"; + + if ($isMain) + { + print FH_LANGUAGES + "# The following languages MUST be defined in this file:\n" + . "#\n" + . "# Text File, Shebang Script\n"; + } + else + { + print FH_LANGUAGES + "# The following languages are defined in the main file, if you'd like to alter\n" + . "# them:\n" + . "#\n" + . Text::Wrap::wrap('# ', '# ', join(', ', @mainLanguageNames)) . "\n"; + }; + + print FH_LANGUAGES "\n" + . "# If you add a language that you think would be useful to other developers\n" + . "# and should be included in Natural Docs by default, please e-mail it to\n" + . "# languages [at] naturaldocs [dot] org.\n"; + + my @topicTypeOrder = ( ::TOPIC_GENERAL(), ::TOPIC_CLASS(), ::TOPIC_FUNCTION(), ::TOPIC_VARIABLE(), + ::TOPIC_PROPERTY(), ::TOPIC_TYPE(), ::TOPIC_CONSTANT() ); + + for (my $i = 0; $i < scalar @segments; $i += 3) + { + my ($keyword, $name, $properties) = @segments[$i..$i+2]; + + print FH_LANGUAGES "\n\n"; + + if ($keyword eq 'language') + { print FH_LANGUAGES 'Language: ' . $name . "\n\n"; } + else + { print FH_LANGUAGES 'Alter Language: ' . $name . "\n\n"; }; + + if (exists $properties->{'extensions'}) + { + print FH_LANGUAGES ' '; + + if ($properties->{'extensions command'}) + { print FH_LANGUAGES ucfirst($properties->{'extensions command'}) . ' '; }; + + my @extensions = split(/ /, $properties->{'extensions'}, 2); + + if (scalar @extensions == 1) + { print FH_LANGUAGES 'Extension: '; } + else + { print FH_LANGUAGES 'Extensions: '; }; + + print FH_LANGUAGES lc($properties->{'extensions'}) . "\n"; + }; + + if (exists $properties->{'shebang strings'}) + { + print FH_LANGUAGES ' '; + + if ($properties->{'shebang strings command'}) + { print FH_LANGUAGES ucfirst($properties->{'shebang strings command'}) . ' '; }; + + my @shebangStrings = split(/ /, $properties->{'shebang strings'}, 2); + + if (scalar @shebangStrings == 1) + { print FH_LANGUAGES 'Shebang String: '; } + else + { print FH_LANGUAGES 'Shebang Strings: '; }; + + print FH_LANGUAGES lc($properties->{'shebang strings'}) . "\n"; + }; + + if (exists $properties->{'ignored prefixes in index'}) + { + my $topicTypePrefixes = $properties->{'ignored prefixes in index'}; + + my %usedTopicTypes; + my @topicTypes = ( @topicTypeOrder, keys %$topicTypePrefixes ); + + foreach my $topicType (@topicTypes) + { + if ($topicType !~ / command$/ && + exists $topicTypePrefixes->{$topicType} && + !exists $usedTopicTypes{$topicType}) + { + print FH_LANGUAGES ' '; + + if ($topicTypePrefixes->{$topicType . ' command'}) + { print FH_LANGUAGES ucfirst($topicTypePrefixes->{$topicType . ' command'}) . ' Ignored '; } + else + { print FH_LANGUAGES 'Ignore '; }; + + if ($topicType ne ::TOPIC_GENERAL()) + { print FH_LANGUAGES NaturalDocs::Topics->TypeInfo($topicType)->Name() . ' '; }; + + my @prefixes = split(/ /, $topicTypePrefixes->{$topicType}, 2); + + if (scalar @prefixes == 1) + { print FH_LANGUAGES 'Prefix in Index: '; } + else + { print FH_LANGUAGES 'Prefixes in Index: '; }; + + print FH_LANGUAGES $topicTypePrefixes->{$topicType} . "\n"; + + $usedTopicTypes{$topicType} = 1; + }; + }; + }; + + if (exists $properties->{'line comments'}) + { + my @comments = split(/ /, $properties->{'line comments'}, 2); + + if (scalar @comments == 1) + { print FH_LANGUAGES ' Line Comment: '; } + else + { print FH_LANGUAGES ' Line Comments: '; }; + + print FH_LANGUAGES $properties->{'line comments'} . "\n"; + }; + + if (exists $properties->{'block comments'}) + { + my @comments = split(/ /, $properties->{'block comments'}, 3); + + if (scalar @comments == 2) + { print FH_LANGUAGES ' Block Comment: '; } + else + { print FH_LANGUAGES ' Block Comments: '; }; + + print FH_LANGUAGES $properties->{'block comments'} . "\n"; + }; + + if (exists $properties->{'package separator'}) + { + # Prior to 1.32, Package Separator was allowed for full language support. Ignore it when reformatting. + if ($version >= NaturalDocs::Version->FromString('1.32') || !exists $properties->{'full language support'}) + { print FH_LANGUAGES ' Package Separator: ' . $properties->{'package separator'} . "\n"; }; + }; + + if (exists $properties->{'enum values'}) + { + print FH_LANGUAGES ' Enum Values: ' . ucfirst(lc($properties->{'enum values'})) . "\n"; + }; + + if (exists $properties->{'prototype enders'}) + { + my $topicTypeEnders = $properties->{'prototype enders'}; + + my %usedTopicTypes; + my @topicTypes = ( @topicTypeOrder, keys %$topicTypeEnders ); + + foreach my $topicType (@topicTypes) + { + if ($topicType !~ / command$/ && + exists $topicTypeEnders->{$topicType} && + !exists $usedTopicTypes{$topicType}) + { + print FH_LANGUAGES ' '; + + if ($topicType ne ::TOPIC_GENERAL()) + { print FH_LANGUAGES NaturalDocs::Topics->TypeInfo($topicType)->Name() . ' '; }; + + my @enders = split(/ /, $topicTypeEnders->{$topicType}, 2); + + if (scalar @enders == 1) + { print FH_LANGUAGES 'Prototype Ender: '; } + else + { print FH_LANGUAGES 'Prototype Enders: '; }; + + print FH_LANGUAGES $topicTypeEnders->{$topicType} . "\n"; + + $usedTopicTypes{$topicType} = 1; + }; + }; + }; + + if (exists $properties->{'line extender'}) + { + print FH_LANGUAGES ' Line Extender: ' . $properties->{'line extender'} . "\n"; + }; + + if (exists $properties->{'perl package'}) + { + print FH_LANGUAGES ' Perl Package: ' . $properties->{'perl package'} . "\n"; + }; + + if (exists $properties->{'full language support'}) + { + print FH_LANGUAGES ' Full Language Support: ' . $properties->{'full language support'} . "\n"; + }; + }; + + close(FH_LANGUAGES); + }; + + + +############################################################################### +# Group: Functions + + +# +# Function: LanguageOf +# +# Returns the language of the passed source file. +# +# Parameters: +# +# sourceFile - The source <FileName> to get the language of. +# +# Returns: +# +# A <NaturalDocs::Languages::Base>-derived object for the passed file, or undef if the file is not a recognized language. +# +sub LanguageOf #(sourceFile) + { + my ($self, $sourceFile) = @_; + + my $extension = NaturalDocs::File->ExtensionOf($sourceFile); + if (defined $extension) + { $extension = lc($extension); }; + + my $languageName; + + if (!defined $extension) + { $languageName = 'shebang script'; } + else + { $languageName = $extensions{$extension}; }; + + if (!defined $languageName) + { $languageName = $extensions{'*'}; }; + + if (defined $languageName) + { + if ($languageName eq 'shebang script') + { + if (exists $shebangFiles{$sourceFile}) + { + if (defined $shebangFiles{$sourceFile}) + { return $languages{$shebangFiles{$sourceFile}}; } + else + { return undef; }; + } + + else # (!exists $shebangFiles{$sourceFile}) + { + my $shebangLine; + + if (open(SOURCEFILEHANDLE, '<' . $sourceFile)) + { + read(SOURCEFILEHANDLE, $shebangLine, 2); + if ($shebangLine eq '#!') + { $shebangLine = <SOURCEFILEHANDLE>; } + else + { $shebangLine = undef; }; + + close (SOURCEFILEHANDLE); + } + elsif (defined $extension) + { die 'Could not open ' . $sourceFile; } + # Ignore extensionless files that can't be opened. They may be system files. + + if (!defined $shebangLine) + { + $shebangFiles{$sourceFile} = undef; + return undef; + } + else + { + $shebangLine = lc($shebangLine); + + foreach my $shebangString (keys %shebangStrings) + { + if (index($shebangLine, $shebangString) != -1) + { + $shebangFiles{$sourceFile} = $shebangStrings{$shebangString}; + return $languages{$shebangStrings{$shebangString}}; + }; + }; + + $shebangFiles{$sourceFile} = undef; + return undef; + }; + }; + } + + else # language name ne 'shebang script' + { return $languages{$languageName}; }; + } + else # !defined $language + { + return undef; + }; + }; + + +# +# Function: OnMostUsedLanguageKnown +# +# Called when the most used language is known. +# +sub OnMostUsedLanguageKnown + { + my $self = shift; + + my $language = $languages{lc( NaturalDocs::Project->MostUsedLanguage() )}; + + if ($language) + { + if (!$languages{'text file'}->HasIgnoredPrefixes()) + { $languages{'text file'}->CopyIgnoredPrefixesOf($language); }; + if (!$languages{'text file'}->PackageSeparatorWasSet()) + { $languages{'text file'}->SetPackageSeparator($language->PackageSeparator()); }; + if (!$languages{'text file'}->EnumValuesWasSet()) + { $languages{'text file'}->SetEnumValues($language->EnumValues()); }; + }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Languages/ActionScript.pm b/docs/tool/Modules/NaturalDocs/Languages/ActionScript.pm new file mode 100644 index 00000000..a55abaf2 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Languages/ActionScript.pm @@ -0,0 +1,1473 @@ +############################################################################### +# +# Class: NaturalDocs::Languages::ActionScript +# +############################################################################### +# +# A subclass to handle the language variations of Flash ActionScript. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Languages::ActionScript; + +use base 'NaturalDocs::Languages::Advanced'; + + +################################################################################ +# Group: Constants and Types + + +# +# Constants: XML Tag Type +# +# XML_OPENING_TAG - The tag is an opening one, such as <tag>. +# XML_CLOSING_TAG - The tag is a closing one, such as </tag>. +# XML_SELF_CONTAINED_TAG - The tag is self contained, such as <tag />. +# +use constant XML_OPENING_TAG => 1; +use constant XML_CLOSING_TAG => 2; +use constant XML_SELF_CONTAINED_TAG => 3; + + +################################################################################ +# Group: Package Variables + +# +# hash: classModifiers +# An existence hash of all the acceptable class modifiers. The keys are in all lowercase. +# +my %classModifiers = ( 'dynamic' => 1, + 'intrinsic' => 1, + 'final' => 1, + 'internal' => 1, + 'public' => 1 ); + +# +# hash: memberModifiers +# An existence hash of all the acceptable class member modifiers. The keys are in all lowercase. +# +my %memberModifiers = ( 'public' => 1, + 'private' => 1, + 'protected' => 1, + 'static' => 1, + 'internal' => 1, + 'override' => 1 ); + + +# +# hash: declarationEnders +# An existence hash of all the tokens that can end a declaration. This is important because statements don't require a semicolon +# to end. The keys are in all lowercase. +# +my %declarationEnders = ( ';' => 1, + '}' => 1, + '{' => 1, + 'public' => 1, + 'private' => 1, + 'protected' => 1, + 'static' => 1, + 'internal' => 1, + 'dynamic' => 1, + 'intrinsic' => 1, + 'final' => 1, + 'override' => 1, + 'class' => 1, + 'interface' => 1, + 'var' => 1, + 'function' => 1, + 'const' => 1, + 'namespace' => 1, + 'import' => 1 ); + + +# +# var: isEscaped +# Whether the current file being parsed uses escapement. +# +my $isEscaped; + + + +################################################################################ +# Group: Interface Functions + + +# +# Function: PackageSeparator +# Returns the package separator symbol. +# +sub PackageSeparator + { return '.'; }; + + +# +# Function: EnumValues +# Returns the <EnumValuesType> that describes how the language handles enums. +# +sub EnumValues + { return ::ENUM_GLOBAL(); }; + + +# +# Function: ParseParameterLine +# Parses a prototype parameter line and returns it as a <NaturalDocs::Languages::Prototype::Parameter> object. +# +sub ParseParameterLine #(line) + { + my ($self, $line) = @_; + + if ($line =~ /^ ?\.\.\.\ (.+)$/) + { + # This puts them in the wrong fields as $1 should be the name and ... should be the type. However, this is necessary + # because the order in the source is reversed from other parameter declarations and it's more important for the output + # to match the source. + return NaturalDocs::Languages::Prototype::Parameter->New($1, undef, '...', undef, undef, undef); + } + else + { return $self->ParsePascalParameterLine($line); }; + }; + + +# +# Function: TypeBeforeParameter +# Returns whether the type appears before the parameter in prototypes. +# +sub TypeBeforeParameter + { return 0; }; + + +# +# Function: PreprocessFile +# +# If the file is escaped, strips out all unescaped code. Will translate any unescaped comments into comments surrounded by +# "\x1C\x1D\x1E\x1F" and "\x1F\x1E\x1D" characters, so chosen because they are the same character lengths as <!-- and --> +# and will not appear in normal code. +# +sub PreprocessFile + { + my ($self, $lines) = @_; + + if (!$isEscaped) + { return; }; + + use constant MODE_UNESCAPED_REGULAR => 1; + use constant MODE_UNESCAPED_PI => 2; + use constant MODE_UNESCAPED_CDATA => 3; + use constant MODE_UNESCAPED_COMMENT => 4; + use constant MODE_ESCAPED_UNKNOWN_CDATA => 5; + use constant MODE_ESCAPED_CDATA => 6; + use constant MODE_ESCAPED_NO_CDATA => 7; + + my $mode = MODE_UNESCAPED_REGULAR; + + for (my $i = 0; $i < scalar @$lines; $i++) + { + my @tokens = split(/(<[ \t]*\/?[ \t]*mx:Script[^>]*>|<\?|\?>|<\!--|-->|<\!\[CDATA\[|\]\]\>)/, $lines->[$i]); + my $newLine; + + foreach my $token (@tokens) + { + if ($mode == MODE_UNESCAPED_REGULAR) + { + if ($token eq '<?') + { $mode = MODE_UNESCAPED_PI; } + elsif ($token eq '<![CDATA[') + { $mode = MODE_UNESCAPED_CDATA; } + elsif ($token eq '<!--') + { + $mode = MODE_UNESCAPED_COMMENT; + $newLine .= "\x1C\x1D\x1E\x1F"; + } + elsif ($token =~ /^<[ \t]*mx:Script/) + { $mode = MODE_ESCAPED_UNKNOWN_CDATA; }; + } + + elsif ($mode == MODE_UNESCAPED_PI) + { + if ($token eq '?>') + { $mode = MODE_UNESCAPED_REGULAR; }; + } + + elsif ($mode == MODE_UNESCAPED_CDATA) + { + if ($token eq ']]>') + { $mode = MODE_UNESCAPED_REGULAR; }; + } + + elsif ($mode == MODE_UNESCAPED_COMMENT) + { + if ($token eq '-->') + { + $mode = MODE_UNESCAPED_REGULAR; + $newLine .= "\x1F\x1E\x1D"; + } + else + { $newLine .= $token; }; + } + + elsif ($mode == MODE_ESCAPED_UNKNOWN_CDATA) + { + if ($token eq '<![CDATA[') + { $mode = MODE_ESCAPED_CDATA; } + elsif ($token =~ /^<[ \t]*\/[ \t]*mx:Script/) + { + $mode = MODE_UNESCAPED_REGULAR; + $newLine .= '; '; + } + elsif ($token !~ /^[ \t]*$/) + { + $mode = MODE_ESCAPED_NO_CDATA; + $newLine .= $token; + }; + } + + elsif ($mode == MODE_ESCAPED_CDATA) + { + if ($token eq ']]>') + { + $mode = MODE_UNESCAPED_REGULAR; + $newLine .= '; '; + } + else + { $newLine .= $token; }; + } + + else #($mode == MODE_ESCAPED_NO_CDATA) + { + if ($token =~ /^<[ \t]*\/[ \t]*mx:Script/) + { + $mode = MODE_UNESCAPED_REGULAR; + $newLine .= '; '; + } + else + { $newLine .= $token; }; + }; + + }; + + $lines->[$i] = $newLine; + }; + }; + + +# +# Function: ParseFile +# +# Parses the passed source file, sending comments acceptable for documentation to <NaturalDocs::Parser->OnComment()>. +# +# Parameters: +# +# sourceFile - The <FileName> to parse. +# topicList - A reference to the list of <NaturalDocs::Parser::ParsedTopics> being built by the file. +# +# Returns: +# +# The array ( autoTopics, scopeRecord ). +# +# autoTopics - An arrayref of automatically generated topics from the file, or undef if none. +# scopeRecord - An arrayref of <NaturalDocs::Languages::Advanced::ScopeChanges>, or undef if none. +# +sub ParseFile #(sourceFile, topicsList) + { + my ($self, $sourceFile, $topicsList) = @_; + + # The \x1# comment symbols are inserted by PreprocessFile() to stand in for XML comments in escaped files. + my @parseParameters = ( [ '//' ], [ '/*', '*/', "\x1C\x1D\x1E\x1F", "\x1F\x1E\x1D" ], [ '///' ], [ '/**', '*/' ] ); + + my $extension = lc(NaturalDocs::File->ExtensionOf($sourceFile)); + $isEscaped = ($extension eq 'mxml'); + + $self->ParseForCommentsAndTokens($sourceFile, @parseParameters); + + my $tokens = $self->Tokens(); + my $index = 0; + my $lineNumber = 1; + + while ($index < scalar @$tokens) + { + if ($self->TryToSkipWhitespace(\$index, \$lineNumber) || + $self->TryToGetImport(\$index, \$lineNumber) || + $self->TryToGetClass(\$index, \$lineNumber) || + $self->TryToGetFunction(\$index, \$lineNumber) || + $self->TryToGetVariable(\$index, \$lineNumber) ) + { + # The functions above will handle everything. + } + + elsif ($tokens->[$index] eq '{') + { + $self->StartScope('}', $lineNumber, undef, undef, undef); + $index++; + } + + elsif ($tokens->[$index] eq '}') + { + if ($self->ClosingScopeSymbol() eq '}') + { $self->EndScope($lineNumber); }; + + $index++; + } + + else + { + $self->SkipToNextStatement(\$index, \$lineNumber); + }; + }; + + + # Don't need to keep these around. + $self->ClearTokens(); + + + my $autoTopics = $self->AutoTopics(); + + my $scopeRecord = $self->ScopeRecord(); + if (defined $scopeRecord && !scalar @$scopeRecord) + { $scopeRecord = undef; }; + + return ( $autoTopics, $scopeRecord ); + }; + + + +################################################################################ +# Group: Statement Parsing Functions +# All functions here assume that the current position is at the beginning of a statement. +# +# Note for developers: I am well aware that the code in these functions do not check if we're past the end of the tokens as +# often as it should. We're making use of the fact that Perl will always return undef in these cases to keep the code simpler. + + +# +# Function: TryToGetIdentifier +# +# Determines whether the position is at an identifier, and if so, skips it and returns the complete identifier as a string. Returns +# undef otherwise. +# +# Parameters: +# +# indexRef - A reference to the current token index. +# lineNumberRef - A reference to the current line number. +# allowStar - If set, allows the last identifier to be a star. +# +sub TryToGetIdentifier #(indexRef, lineNumberRef, allowStar) + { + my ($self, $indexRef, $lineNumberRef, $allowStar) = @_; + my $tokens = $self->Tokens(); + + my $index = $$indexRef; + + use constant MODE_IDENTIFIER_START => 1; + use constant MODE_IN_IDENTIFIER => 2; + use constant MODE_AFTER_STAR => 3; + + my $identifier; + my $mode = MODE_IDENTIFIER_START; + + while ($index < scalar @$tokens) + { + if ($mode == MODE_IDENTIFIER_START) + { + if ($tokens->[$index] =~ /^[a-z\$\_]/i) + { + $identifier .= $tokens->[$index]; + $index++; + + $mode = MODE_IN_IDENTIFIER; + } + elsif ($allowStar && $tokens->[$index] eq '*') + { + $identifier .= '*'; + $index++; + + $mode = MODE_AFTER_STAR; + } + else + { return undef; }; + } + + elsif ($mode == MODE_IN_IDENTIFIER) + { + if ($tokens->[$index] eq '.') + { + $identifier .= '.'; + $index++; + + $mode = MODE_IDENTIFIER_START; + } + elsif ($tokens->[$index] =~ /^[a-z0-9\$\_]/i) + { + $identifier .= $tokens->[$index]; + $index++; + } + else + { last; }; + } + + else #($mode == MODE_AFTER_STAR) + { + if ($tokens->[$index] =~ /^[a-z0-9\$\_\.]/i) + { return undef; } + else + { last; }; + }; + }; + + # We need to check again because we may have run out of tokens after a dot. + if ($mode != MODE_IDENTIFIER_START) + { + $$indexRef = $index; + return $identifier; + } + else + { return undef; }; + }; + + +# +# Function: TryToGetImport +# +# Determines whether the position is at a import statement, and if so, adds it as a Using statement to the current scope, skips +# it, and returns true. +# +sub TryToGetImport #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $index = $$indexRef; + my $lineNumber = $$lineNumberRef; + + if ($tokens->[$index] ne 'import') + { return undef; }; + + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + my $identifier = $self->TryToGetIdentifier(\$index, \$lineNumber, 1); + if (!$identifier) + { return undef; }; + + + # Currently we implement importing by stripping the last package level and treating it as a using. So "import p1.p2.p3" makes + # p1.p2 the using path, which is over-tolerant but that's okay. "import p1.p2.*" is treated the same way, but in this case it's + # not over-tolerant. If there's no dot, there's no point to including it. + + if (index($identifier, '.') != -1) + { + $identifier =~ s/\.[^\.]+$//; + $self->AddUsing( NaturalDocs::SymbolString->FromText($identifier) ); + }; + + $$indexRef = $index; + $$lineNumberRef = $lineNumber; + + return 1; + }; + + +# +# Function: TryToGetClass +# +# Determines whether the position is at a class declaration statement, and if so, generates a topic for it, skips it, and +# returns true. +# +# Supported Syntaxes: +# +# - Classes +# - Interfaces +# - Classes and interfaces with _global +# +sub TryToGetClass #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $index = $$indexRef; + my $lineNumber = $$lineNumberRef; + + my @modifiers; + + while ($tokens->[$index] =~ /^[a-z]/i && + exists $classModifiers{lc($tokens->[$index])} ) + { + push @modifiers, lc($tokens->[$index]); + $index++; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + }; + + my $type; + + if ($tokens->[$index] eq 'class' || $tokens->[$index] eq 'interface') + { + $type = $tokens->[$index]; + + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + } + else + { return undef; }; + + my $className = $self->TryToGetIdentifier(\$index, \$lineNumber); + + if (!$className) + { return undef; }; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + my @parents; + + if ($tokens->[$index] eq 'extends') + { + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + my $parent = $self->TryToGetIdentifier(\$index, \$lineNumber); + if (!$parent) + { return undef; }; + + push @parents, $parent; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + }; + + if ($type eq 'class' && $tokens->[$index] eq 'implements') + { + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + for (;;) + { + my $parent = $self->TryToGetIdentifier(\$index, \$lineNumber); + if (!$parent) + { return undef; }; + + push @parents, $parent; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + if ($tokens->[$index] ne ',') + { last; } + else + { + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + }; + }; + }; + + if ($tokens->[$index] ne '{') + { return undef; }; + + $index++; + + + # If we made it this far, we have a valid class declaration. + + my $topicType; + + if ($type eq 'interface') + { $topicType = ::TOPIC_INTERFACE(); } + else + { $topicType = ::TOPIC_CLASS(); }; + + $className =~ s/^_global.//; + + my $autoTopic = NaturalDocs::Parser::ParsedTopic->New($topicType, $className, + undef, $self->CurrentUsing(), + undef, + undef, undef, $$lineNumberRef); + + $self->AddAutoTopic($autoTopic); + NaturalDocs::Parser->OnClass($autoTopic->Package()); + + foreach my $parent (@parents) + { + NaturalDocs::Parser->OnClassParent($autoTopic->Package(), NaturalDocs::SymbolString->FromText($parent), + undef, $self->CurrentUsing(), ::RESOLVE_ABSOLUTE()); + }; + + $self->StartScope('}', $lineNumber, $autoTopic->Package()); + + $$indexRef = $index; + $$lineNumberRef = $lineNumber; + + return 1; + }; + + +# +# Function: TryToGetFunction +# +# Determines if the position is on a function declaration, and if so, generates a topic for it, skips it, and returns true. +# +# Supported Syntaxes: +# +# - Functions +# - Constructors +# - Properties +# - Functions with _global +# - Functions with namespaces +# +sub TryToGetFunction #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $index = $$indexRef; + my $lineNumber = $$lineNumberRef; + + my $startIndex = $index; + my $startLine = $lineNumber; + + my @modifiers; + my $namespace; + + while ($tokens->[$index] =~ /^[a-z]/i) + { + if ($tokens->[$index] eq 'function') + { last; } + + elsif (exists $memberModifiers{lc($tokens->[$index])}) + { + push @modifiers, lc($tokens->[$index]); + $index++; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + } + + elsif (!$namespace) + { + do + { + $namespace .= $tokens->[$index]; + $index++; + } + while ($tokens->[$index] =~ /^[a-z0-9_]/i); + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + } + + else + { last; }; + }; + + if ($tokens->[$index] ne 'function') + { return undef; }; + $index++; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + my $type; + + if ($tokens->[$index] eq 'get' || $tokens->[$index] eq 'set') + { + # This can either be a property ("function get Something()") or a function name ("function get()"). + + my $nextIndex = $index; + my $nextLineNumber = $lineNumber; + + $nextIndex++; + $self->TryToSkipWhitespace(\$nextIndex, \$nextLineNumber); + + if ($tokens->[$nextIndex] eq '(') + { + $type = ::TOPIC_FUNCTION(); + # Ignore the movement and let the code ahead pick it up as the name. + } + else + { + $type = ::TOPIC_PROPERTY(); + $index = $nextIndex; + $lineNumber = $nextLineNumber; + }; + } + else + { $type = ::TOPIC_FUNCTION(); }; + + my $name = $self->TryToGetIdentifier(\$index, \$lineNumber); + if (!$name) + { return undef; }; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + if ($tokens->[$index] ne '(') + { return undef; }; + + $index++; + $self->GenericSkipUntilAfter(\$index, \$lineNumber, ')'); + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + if ($tokens->[$index] eq ':') + { + $index++; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + $self->TryToGetIdentifier(\$index, \$lineNumber, 1); + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + }; + + + my $prototype = $self->NormalizePrototype( $self->CreateString($startIndex, $index) ); + + if ($tokens->[$index] eq '{') + { $self->GenericSkip(\$index, \$lineNumber); } + elsif (!exists $declarationEnders{$tokens->[$index]}) + { return undef; }; + + + my $scope = $self->CurrentScope(); + + if ($name =~ s/^_global.//) + { $scope = undef; }; + if ($namespace) + { $scope = NaturalDocs::SymbolString->Join($scope, $namespace); }; + + $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New($type, $name, + $scope, $self->CurrentUsing(), + $prototype, + undef, undef, $startLine)); + + + # We succeeded if we got this far. + + $$indexRef = $index; + $$lineNumberRef = $lineNumber; + + return 1; + }; + + +# +# Function: TryToGetVariable +# +# Determines if the position is on a variable declaration statement, and if so, generates a topic for each variable, skips the +# statement, and returns true. +# +# Supported Syntaxes: +# +# - Variables +# - Variables with _global +# - Variables with type * (untyped) +# - Constants +# - Variables and constants with namespaces +# +sub TryToGetVariable #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $index = $$indexRef; + my $lineNumber = $$lineNumberRef; + + my $startIndex = $index; + my $startLine = $lineNumber; + + my @modifiers; + my $namespace; + + while ($tokens->[$index] =~ /^[a-z]/i) + { + if ($tokens->[$index] eq 'var' || $tokens->[$index] eq 'const') + { last; } + + elsif (exists $memberModifiers{lc($tokens->[$index])}) + { + push @modifiers, lc($tokens->[$index]); + $index++; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + } + + elsif (!$namespace) + { + do + { + $namespace .= $tokens->[$index]; + $index++; + } + while ($tokens->[$index] =~ /^[a-z0-9_]/i); + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + } + + else + { last; }; + }; + + my $type; + + if ($tokens->[$index] eq 'var') + { $type = ::TOPIC_VARIABLE(); } + elsif ($tokens->[$index] eq 'const') + { $type = ::TOPIC_CONSTANT(); } + else + { return undef; }; + $index++; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + my $endTypeIndex = $index; + my @names; + my @types; + + for (;;) + { + my $name = $self->TryToGetIdentifier(\$index, \$lineNumber); + if (!$name) + { return undef; }; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + my $type; + + if ($tokens->[$index] eq ':') + { + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + $type = ': ' . $self->TryToGetIdentifier(\$index, \$lineNumber, 1); + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + }; + + if ($tokens->[$index] eq '=') + { + do + { + $self->GenericSkip(\$index, \$lineNumber); + } + while ($tokens->[$index] ne ',' && !exists $declarationEnders{$tokens->[$index]} && $index < scalar @$tokens); + }; + + push @names, $name; + push @types, $type; + + if ($tokens->[$index] eq ',') + { + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + } + elsif (exists $declarationEnders{$tokens->[$index]}) + { last; } + else + { return undef; }; + }; + + + # We succeeded if we got this far. + + my $prototypePrefix = $self->CreateString($startIndex, $endTypeIndex); + + for (my $i = 0; $i < scalar @names; $i++) + { + my $prototype = $self->NormalizePrototype( $prototypePrefix . ' ' . $names[$i] . $types[$i]); + my $scope = $self->CurrentScope(); + + if ($names[$i] =~ s/^_global.//) + { $scope = undef; }; + if ($namespace) + { $scope = NaturalDocs::SymbolString->Join($scope, $namespace); }; + + $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New($type, $names[$i], + $scope, $self->CurrentUsing(), + $prototype, + undef, undef, $startLine)); + }; + + $$indexRef = $index; + $$lineNumberRef = $lineNumber; + + return 1; + }; + + + +################################################################################ +# Group: Low Level Parsing Functions + + +# +# Function: GenericSkip +# +# Advances the position one place through general code. +# +# - If the position is on a string, it will skip it completely. +# - If the position is on an opening symbol, it will skip until the past the closing symbol. +# - If the position is on whitespace (including comments), it will skip it completely. +# - Otherwise it skips one token. +# +# Parameters: +# +# indexRef - A reference to the current index. +# lineNumberRef - A reference to the current line number. +# +sub GenericSkip #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + # We can ignore the scope stack because we're just skipping everything without parsing, and we need recursion anyway. + if ($tokens->[$$indexRef] eq '{') + { + $$indexRef++; + $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}'); + } + elsif ($tokens->[$$indexRef] eq '(') + { + $$indexRef++; + $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ')'); + } + elsif ($tokens->[$$indexRef] eq '[') + { + $$indexRef++; + $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ']'); + } + + elsif ($self->TryToSkipWhitespace($indexRef, $lineNumberRef) || + $self->TryToSkipString($indexRef, $lineNumberRef) || + $self->TryToSkipRegExp($indexRef, $lineNumberRef) || + $self->TryToSkipXML($indexRef, $lineNumberRef) ) + { + } + + else + { $$indexRef++; }; + }; + + +# +# Function: GenericSkipUntilAfter +# +# Advances the position via <GenericSkip()> until a specific token is reached and passed. +# +sub GenericSkipUntilAfter #(indexRef, lineNumberRef, token) + { + my ($self, $indexRef, $lineNumberRef, $token) = @_; + my $tokens = $self->Tokens(); + + while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] ne $token) + { $self->GenericSkip($indexRef, $lineNumberRef); }; + + if ($tokens->[$$indexRef] eq "\n") + { $$lineNumberRef++; }; + $$indexRef++; + }; + + +# +# Function: IndiscriminateSkipUntilAfterSequence +# +# Advances the position indiscriminately until a specific token sequence is reached and passed. +# +sub IndiscriminateSkipUntilAfterSequence #(indexRef, lineNumberRef, token, token, ...) + { + my ($self, $indexRef, $lineNumberRef, @sequence) = @_; + my $tokens = $self->Tokens(); + + while ($$indexRef < scalar @$tokens && !$self->IsAtSequence($$indexRef, @sequence)) + { + if ($tokens->[$$indexRef] eq "\n") + { $$lineNumberRef++; }; + $$indexRef++; + }; + + if ($self->IsAtSequence($$indexRef, @sequence)) + { + $$indexRef += scalar @sequence; + foreach my $token (@sequence) + { + if ($token eq "\n") + { $$lineNumberRef++; }; + }; + }; + }; + + +# +# Function: SkipToNextStatement +# +# Advances the position via <GenericSkip()> until the next statement, which is defined as anything in <declarationEnders> not +# appearing in brackets or strings. It will always advance at least one token. +# +sub SkipToNextStatement #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + if ($tokens->[$$indexRef] eq ';') + { $$indexRef++; } + + else + { + do + { + $self->GenericSkip($indexRef, $lineNumberRef); + } + while ( $$indexRef < scalar @$tokens && + !exists $declarationEnders{$tokens->[$$indexRef]} ); + }; + }; + + +# +# Function: TryToSkipRegExp +# If the current position is on a regular expression, skip past it and return true. +# +sub TryToSkipRegExp #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + if ($tokens->[$$indexRef] eq '/') + { + # A slash can either start a regular expression or be a divide symbol. Skip backwards to see what the previous symbol is. + my $index = $$indexRef - 1; + + while ($index >= 0 && $tokens->[$index] =~ /^(?: |\t|\n)/) + { $index--; }; + + if ($index < 0 || $tokens->[$index] !~ /^\=\(\[\,]/) + { return 0; }; + + $$indexRef++; + + while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] ne '/') + { + if ($tokens->[$$indexRef] eq '\\') + { $$indexRef += 2; } + elsif ($tokens->[$$indexRef] eq "\n") + { + $$indexRef++; + $$lineNumberRef++; + } + else + { $$indexRef++; } + }; + + if ($$indexRef < scalar @$tokens) + { + $$indexRef++; + + if ($tokens->[$$indexRef] =~ /^[gimsx]+$/i) + { $$indexRef++; }; + }; + + return 1; + } + else + { return 0; }; + }; + + +# +# Function: TryToSkipXML +# If the current position is on an XML literal, skip past it and return true. +# +sub TryToSkipXML #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + if ($tokens->[$$indexRef] eq '<') + { + # A < can either start an XML literal or be a comparison or shift operator. First check the next character for << or <=. + + my $index = $$indexRef + 1; + + while ($index < scalar @$tokens && $tokens->[$index] =~ /^[\=\<]$/) + { return 0; }; + + + # Next try the previous character. + + $index = $$indexRef - 1; + + while ($index >= 0 && $tokens->[$index] =~ /^[ |\t|\n]/) + { $index--; }; + + if ($index < 0 || $tokens->[$index] !~ /^[\=\(\[\,\>]/) + { return 0; }; + } + else + { return 0; }; + + + # Only handle the tag here if it's not an irregular XML section. + if (!$self->TryToSkipIrregularXML($indexRef, $lineNumberRef)) + { + my @tagStack; + + my ($tagType, $tagIdentifier) = $self->GetAndSkipXMLTag($indexRef, $lineNumberRef); + if ($tagType == XML_OPENING_TAG) + { push @tagStack, $tagIdentifier; }; + + while (scalar @tagStack && $$indexRef < scalar @$tokens) + { + $self->SkipToNextXMLTag($indexRef, $lineNumberRef); + ($tagType, $tagIdentifier) = $self->GetAndSkipXMLTag($indexRef, $lineNumberRef); + + if ($tagType == XML_OPENING_TAG) + { push @tagStack, $tagIdentifier; } + elsif ($tagType == XML_CLOSING_TAG && $tagIdentifier eq $tagStack[-1]) + { pop @tagStack; }; + }; + }; + + + return 1; + }; + + +# +# Function: TryToSkipIrregularXML +# +# If the current position is on an irregular XML tag, skip past it and return true. Irregular XML tags are defined as +# +# CDATA - <![CDATA[ ... ]]> +# Comments - <!-- ... --> +# PI - <? ... ?> +# +sub TryToSkipIrregularXML #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + + if ($self->IsAtSequence($$indexRef, '<', '!', '[', 'CDATA', '[')) + { + $$indexRef += 5; + $self->IndiscriminateSkipUntilAfterSequence($indexRef, $lineNumberRef, ']', ']', '>'); + return 1; + } + + elsif ($self->IsAtSequence($$indexRef, '<', '!', '-', '-')) + { + $$indexRef += 4; + $self->IndiscriminateSkipUntilAfterSequence($indexRef, $lineNumberRef, '-', '-', '>'); + return 1; + } + + elsif ($self->IsAtSequence($$indexRef, '<', '?')) + { + $$indexRef += 2; + $self->IndiscriminateSkipUntilAfterSequence($indexRef, $lineNumberRef, '?', '>'); + return 1; + } + + else + { return 0; }; + }; + + +# +# Function: GetAndSkipXMLTag +# +# Processes the XML tag at the current position, moves beyond it, and returns information about it. Assumes the position is on +# the opening angle bracket of the tag and the tag is a normal XML tag, not one of the ones handled by +# <TryToSkipIrregularXML()>. +# +# Parameters: +# +# indexRef - A reference to the index of the position of the opening angle bracket. +# lineNumberRef - A reference to the line number of the position of the opening angle bracket. +# +# Returns: +# +# The array ( tagType, name ). +# +# tagType - One of the <XML Tag Type> constants. +# identifier - The identifier of the tag. If it's an empty tag (<> or </>), this will be "(anonymous)". +# +sub GetAndSkipXMLTag #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + if ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] ne '<') + { die "Tried to call GetXMLTag when the position isn't on an opening bracket."; }; + + # Get the anonymous ones out of the way so we don't have to worry about them below, since they're rather exceptional. + + if ($self->IsAtSequence($$indexRef, '<', '>')) + { + $$indexRef += 2; + return ( XML_OPENING_TAG, '(anonymous)' ); + } + elsif ($self->IsAtSequence($$indexRef, '<', '/', '>')) + { + $$indexRef += 3; + return ( XML_CLOSING_TAG, '(anonymous)' ); + }; + + + # Grab the identifier. + + my $tagType = XML_OPENING_TAG; + my $identifier; + + $$indexRef++; + + if ($tokens->[$$indexRef] eq '/') + { + $$indexRef++; + $tagType = XML_CLOSING_TAG; + }; + + $self->TryToSkipXMLWhitespace($indexRef, $lineNumberRef); + + + # The identifier could be a native expression in braces. + + if ($tokens->[$$indexRef] eq '{') + { + my $startOfIdentifier = $$indexRef; + + $$indexRef++; + $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}'); + + $identifier = $self->CreateString($startOfIdentifier, $$indexRef); + } + + + # Otherwise just grab content until whitespace or the end of the tag. + + else + { + while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] !~ /^[\/\>\ \t]$/) + { + $identifier .= $tokens->[$$indexRef]; + $$indexRef++; + }; + }; + + + # Skip to the end of the tag. + + while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] !~ /^[\/\>]$/) + { + if ($tokens->[$$indexRef] eq '{') + { + $$indexRef++; + $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}'); + } + + elsif ($self->TryToSkipXMLWhitespace($indexRef, $lineNumberRef)) + { } + + # We don't need to do special handling for attribute quotes or anything like that because there's no backslashing in + # XML. It's all handled with entity characters. + else + { $$indexRef++; }; + }; + + + if ($tokens->[$$indexRef] eq '/') + { + if ($tagType == XML_OPENING_TAG) + { $tagType = XML_SELF_CONTAINED_TAG; }; + + $$indexRef++; + }; + + if ($tokens->[$$indexRef] eq '>') + { $$indexRef++; }; + + if (!$identifier) + { $identifier = '(anonymous)'; }; + + + return ( $tagType, $identifier ); + }; + + +# +# Function: SkipToNextXMLTag +# Skips to the next normal XML tag. It will not stop at elements handled by <TryToSkipIrregularXML()>. Note that if the +# position is already at an XML tag, it will not move. +# +sub SkipToNextXMLTag #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + while ($$indexRef < scalar @$tokens) + { + if ($tokens->[$$indexRef] eq '{') + { + $$indexRef++; + $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}'); + } + + elsif ($self->TryToSkipIrregularXML($indexRef, $lineNumberRef)) + { } + + elsif ($tokens->[$$indexRef] eq '<') + { last; } + + else + { + if ($tokens->[$$indexRef] eq "\n") + { $$lineNumberRef++; }; + + $$indexRef++; + }; + }; + }; + + +# +# Function: TryToSkipXMLWhitespace +# If the current position is on XML whitespace, skip past it and return true. +# +sub TryToSkipXMLWhitespace #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $result; + + while ($$indexRef < scalar @$tokens) + { + if ($tokens->[$$indexRef] =~ /^[ \t]/) + { + $$indexRef++; + $result = 1; + } + elsif ($tokens->[$$indexRef] eq "\n") + { + $$indexRef++; + $$lineNumberRef++; + $result = 1; + } + else + { last; }; + }; + + return $result; + }; + + +# +# Function: TryToSkipString +# If the current position is on a string delimiter, skip past the string and return true. +# +# Parameters: +# +# indexRef - A reference to the index of the position to start at. +# lineNumberRef - A reference to the line number of the position. +# +# Returns: +# +# Whether the position was at a string. +# +# Syntax Support: +# +# - Supports quotes and apostrophes. +# +sub TryToSkipString #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + + return ($self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '\'') || + $self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '"') ); + }; + + +# +# Function: TryToSkipWhitespace +# If the current position is on a whitespace token, a line break token, or a comment, it skips them and returns true. If there are +# a number of these in a row, it skips them all. +# +sub TryToSkipWhitespace #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $result; + + while ($$indexRef < scalar @$tokens) + { + if ($tokens->[$$indexRef] =~ /^[ \t]/) + { + $$indexRef++; + $result = 1; + } + elsif ($tokens->[$$indexRef] eq "\n") + { + $$indexRef++; + $$lineNumberRef++; + $result = 1; + } + elsif ($self->TryToSkipComment($indexRef, $lineNumberRef)) + { + $result = 1; + } + else + { last; }; + }; + + return $result; + }; + + +# +# Function: TryToSkipComment +# If the current position is on a comment, skip past it and return true. +# +sub TryToSkipComment #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + + return ( $self->TryToSkipLineComment($indexRef, $lineNumberRef) || + $self->TryToSkipMultilineComment($indexRef, $lineNumberRef) ); + }; + + +# +# Function: TryToSkipLineComment +# If the current position is on a line comment symbol, skip past it and return true. +# +sub TryToSkipLineComment #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + if ($tokens->[$$indexRef] eq '/' && $tokens->[$$indexRef+1] eq '/') + { + $self->SkipRestOfLine($indexRef, $lineNumberRef); + return 1; + } + else + { return undef; }; + }; + + +# +# Function: TryToSkipMultilineComment +# If the current position is on an opening comment symbol, skip past it and return true. +# +sub TryToSkipMultilineComment #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + if ($tokens->[$$indexRef] eq '/' && $tokens->[$$indexRef+1] eq '*') + { + $self->SkipUntilAfter($indexRef, $lineNumberRef, '*', '/'); + return 1; + } + else + { return undef; }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Languages/Ada.pm b/docs/tool/Modules/NaturalDocs/Languages/Ada.pm new file mode 100644 index 00000000..d7369ac6 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Languages/Ada.pm @@ -0,0 +1,38 @@ +############################################################################### +# +# Class: NaturalDocs::Languages::Ada +# +############################################################################### +# +# A subclass to handle the language variations of Ada +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Languages::Ada; + +use base 'NaturalDocs::Languages::Simple'; + + +# +# Function: ParseParameterLine +# Overridden because Ada uses Pascal-style parameters +# +sub ParseParameterLine #(...) + { + my ($self, @params) = @_; + return $self->SUPER::ParsePascalParameterLine(@params); + }; + +sub TypeBeforeParameter + { + return 0; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Languages/Advanced.pm b/docs/tool/Modules/NaturalDocs/Languages/Advanced.pm new file mode 100644 index 00000000..8ae27bfc --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Languages/Advanced.pm @@ -0,0 +1,828 @@ +############################################################################### +# +# Class: NaturalDocs::Languages::Advanced +# +############################################################################### +# +# The base class for all languages that have full support in Natural Docs. Each one will have a custom parser capable +# of documenting undocumented aspects of the code. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +use NaturalDocs::Languages::Advanced::Scope; +use NaturalDocs::Languages::Advanced::ScopeChange; + +package NaturalDocs::Languages::Advanced; + +use base 'NaturalDocs::Languages::Base'; + + +############################################################################# +# Group: Implementation + +# +# Constants: Members +# +# The class is implemented as a blessed arrayref. The following constants are used as indexes. +# +# TOKENS - An arrayref of tokens used in all the <Parsing Functions>. +# SCOPE_STACK - An arrayref of <NaturalDocs::Languages::Advanced::Scope> objects serving as a scope stack for parsing. +# There will always be one available, with a symbol of undef, for the top level. +# SCOPE_RECORD - An arrayref of <NaturalDocs::Languages::Advanced::ScopeChange> objects, as generated by the scope +# stack. If there is more than one change per line, only the last is stored. +# AUTO_TOPICS - An arrayref of <NaturalDocs::Parser::ParsedTopics> generated automatically from the code. +# +use NaturalDocs::DefineMembers 'TOKENS', 'SCOPE_STACK', 'SCOPE_RECORD', 'AUTO_TOPICS'; + + +############################################################################# +# Group: Functions + +# +# Function: New +# +# Creates and returns a new object. +# +# Parameters: +# +# name - The name of the language. +# +sub New #(name) + { + my ($package, @parameters) = @_; + + my $object = $package->SUPER::New(@parameters); + $object->[TOKENS] = undef; + $object->[SCOPE_STACK] = undef; + $object->[SCOPE_RECORD] = undef; + + return $object; + }; + + +# Function: Tokens +# Returns the tokens found by <ParseForCommentsAndTokens()>. +sub Tokens + { return $_[0]->[TOKENS]; }; + +# Function: SetTokens +# Replaces the tokens. +sub SetTokens #(tokens) + { $_[0]->[TOKENS] = $_[1]; }; + +# Function: ClearTokens +# Resets the token list. You may want to do this after parsing is over to save memory. +sub ClearTokens + { $_[0]->[TOKENS] = undef; }; + +# Function: AutoTopics +# Returns the arrayref of automatically generated topics, or undef if none. +sub AutoTopics + { return $_[0]->[AUTO_TOPICS]; }; + +# Function: AddAutoTopic +# Adds a <NaturalDocs::Parser::ParsedTopic> to <AutoTopics()>. +sub AddAutoTopic #(topic) + { + my ($self, $topic) = @_; + if (!defined $self->[AUTO_TOPICS]) + { $self->[AUTO_TOPICS] = [ ]; }; + push @{$self->[AUTO_TOPICS]}, $topic; + }; + +# Function: ClearAutoTopics +# Resets the automatic topic list. Not necessary if you call <ParseForCommentsAndTokens()>. +sub ClearAutoTopics + { $_[0]->[AUTO_TOPICS] = undef; }; + +# Function: ScopeRecord +# Returns an arrayref of <NaturalDocs::Languages::Advanced::ScopeChange> objects describing how and when the scope +# changed thoughout the file. There will always be at least one entry, which will be for line 1 and undef as the scope. +sub ScopeRecord + { return $_[0]->[SCOPE_RECORD]; }; + + + +############################################################################### +# +# Group: Parsing Functions +# +# These functions are good general language building blocks. Use them to create your language-specific parser. +# +# All functions work on <Tokens()> and assume it is set by <ParseForCommentsAndTokens()>. +# + + +# +# Function: ParseForCommentsAndTokens +# +# Loads the passed file, sends all appropriate comments to <NaturalDocs::Parser->OnComment()>, and breaks the rest into +# an arrayref of tokens. Tokens are defined as +# +# - All consecutive alphanumeric and underscore characters. +# - All consecutive whitespace. +# - A single line break. It will always be "\n"; you don't have to worry about platform differences. +# - A single character not included above, which is usually a symbol. Multiple consecutive ones each get their own token. +# +# The result will be placed in <Tokens()>. +# +# Parameters: +# +# sourceFile - The source <FileName> to load and parse. +# lineCommentSymbols - An arrayref of symbols that designate line comments, or undef if none. +# blockCommentSymbols - An arrayref of symbol pairs that designate multiline comments, or undef if none. Symbol pairs are +# designated as two consecutive array entries, the opening symbol appearing first. +# javadocLineCommentSymbols - An arrayref of symbols that designate the start of a JavaDoc comment, or undef if none. +# javadocBlockCommentSymbols - An arrayref of symbol pairs that designate multiline JavaDoc comments, or undef if none. +# +# Notes: +# +# - This function automatically calls <ClearAutoTopics()> and <ClearScopeStack()>. You only need to call those functions +# manually if you override this one. +# - To save parsing time, all comment lines sent to <NaturalDocs::Parser->OnComment()> will be replaced with blank lines +# in <Tokens()>. It's all the same to most languages. +# +sub ParseForCommentsAndTokens #(FileName sourceFile, string[] lineCommentSymbols, string[] blockCommentSymbols, string[] javadocLineCommentSymbols, string[] javadocBlockCommentSymbols) + { + my ($self, $sourceFile, $lineCommentSymbols, $blockCommentSymbols, + $javadocLineCommentSymbols, $javadocBlockCommentSymbols) = @_; + + open(SOURCEFILEHANDLE, '<' . $sourceFile) + or die "Couldn't open input file " . $sourceFile . "\n"; + + my $tokens = [ ]; + $self->SetTokens($tokens); + + # For convenience. + $self->ClearAutoTopics(); + $self->ClearScopeStack(); + + + # Load and preprocess the file + + my @lines; + my $line = <SOURCEFILEHANDLE>; + + # On the very first line, remove a Unicode BOM if present. Information on it available at: + # http://www.unicode.org/faq/utf_bom.html#BOM + $line =~ s/^\xEF\xBB\xBF//; + + while (defined $line) + { + ::XChomp(\$line); + push @lines, $line; + + $line = <SOURCEFILEHANDLE>; + }; + + close(SOURCEFILEHANDLE); + + $self->PreprocessFile(\@lines); + + + # Go through the file + + my $lineIndex = 0; + + while ($lineIndex < scalar @lines) + { + $line = $lines[$lineIndex]; + + my @commentLines; + my $commentLineNumber; + my $isJavaDoc; + my $closingSymbol; + + + # Retrieve single line comments. This leaves $lineIndex at the next line. + + if ( ($isJavaDoc = $self->StripOpeningJavaDocSymbols(\$line, $javadocLineCommentSymbols)) || + $self->StripOpeningSymbols(\$line, $lineCommentSymbols)) + { + $commentLineNumber = $lineIndex + 1; + + do + { + push @commentLines, $line; + push @$tokens, "\n"; + + $lineIndex++; + + if ($lineIndex >= scalar @lines) + { goto EndDo; }; + + $line = $lines[$lineIndex]; + } + while ($self->StripOpeningSymbols(\$line, $lineCommentSymbols)); + + EndDo: # I hate Perl sometimes. + } + + + # Retrieve multiline comments. This leaves $lineIndex at the next line. + + elsif ( ($isJavaDoc = $self->StripOpeningJavaDocBlockSymbols(\$line, $javadocBlockCommentSymbols)) || + ($closingSymbol = $self->StripOpeningBlockSymbols(\$line, $blockCommentSymbols)) ) + { + $commentLineNumber = $lineIndex + 1; + + if ($isJavaDoc) + { $closingSymbol = $isJavaDoc; }; + + # Note that it is possible for a multiline comment to start correctly but not end so. We want those comments to stay in + # the code. For example, look at this prototype with this splint annotation: + # + # int get_array(integer_t id, + # /*@out@*/ array_t array); + # + # The annotation starts correctly but doesn't end so because it is followed by code on the same line. + + my ($lineRemainder, $isMultiLine); + + for (;;) + { + $lineRemainder = $self->StripClosingSymbol(\$line, $closingSymbol); + + push @commentLines, $line; + + # If we found an end comment symbol... + if (defined $lineRemainder) + { last; }; + + push @$tokens, "\n"; + $lineIndex++; + $isMultiLine = 1; + + if ($lineIndex >= scalar @lines) + { last; }; + + $line = $lines[$lineIndex]; + }; + + if ($lineRemainder !~ /^[ \t]*$/) + { + # If there was something past the closing symbol this wasn't an acceptable comment. + + if ($isMultiLine) + { $self->TokenizeLine($lineRemainder); } + else + { + # We go back to the original line if it wasn't a multiline comment because we want the comment to stay in the + # code. Otherwise the /*@out@*/ from the example would be removed. + $self->TokenizeLine($lines[$lineIndex]); + }; + + @commentLines = ( ); + } + else + { + push @$tokens, "\n"; + }; + + $lineIndex++; + } + + + # Otherwise just add it to the code. + + else + { + $self->TokenizeLine($line); + $lineIndex++; + }; + + + # If there were comments, send them to Parser->OnComment(). + + if (scalar @commentLines) + { + NaturalDocs::Parser->OnComment(\@commentLines, $commentLineNumber, $isJavaDoc); + @commentLines = ( ); + $isJavaDoc = undef; + }; + + # $lineIndex was incremented by the individual code paths above. + + }; # while ($lineIndex < scalar @lines) + }; + + +# +# Function: PreprocessFile +# +# An overridable function if you'd like to preprocess the file before it goes into <ParseForCommentsAndTokens()>. +# +# Parameters: +# +# lines - An arrayref to the file's lines. Each line has its line break stripped off, but is otherwise untouched. +# +sub PreprocessFile #(lines) + { + }; + + +# +# Function: TokenizeLine +# +# Converts the passed line to tokens as described in <ParseForCommentsAndTokens> and adds them to <Tokens()>. Also +# adds a line break token after it. +# +sub TokenizeLine #(line) + { + my ($self, $line) = @_; + push @{$self->Tokens()}, $line =~ /(\w+|[ \t]+|.)/g, "\n"; + }; + + +# +# Function: TryToSkipString +# +# If the position is on a string delimiter, moves the position to the token following the closing delimiter, or past the end of the +# tokens if there is none. Assumes all other characters are allowed in the string, the delimiter itself is allowed if it's preceded by +# a backslash, and line breaks are allowed in the string. +# +# Parameters: +# +# indexRef - A reference to the position's index into <Tokens()>. +# lineNumberRef - A reference to the position's line number. +# openingDelimiter - The opening string delimiter, such as a quote or an apostrophe. +# closingDelimiter - The closing string delimiter, if different. If not defined, assumes the same as openingDelimiter. +# startContentIndexRef - A reference to a variable in which to store the index of the first token of the string's content. +# May be undef. +# endContentIndexRef - A reference to a variable in which to store the index of the end of the string's content, which is one +# past the last index of content. May be undef. +# +# Returns: +# +# Whether the position was on the passed delimiter or not. The index, line number, and content index ref variables will be +# updated only if true. +# +sub TryToSkipString #(indexRef, lineNumberRef, openingDelimiter, closingDelimiter, startContentIndexRef, endContentIndexRef) + { + my ($self, $index, $lineNumber, $openingDelimiter, $closingDelimiter, $startContentIndexRef, $endContentIndexRef) = @_; + my $tokens = $self->Tokens(); + + if (!defined $closingDelimiter) + { $closingDelimiter = $openingDelimiter; }; + + if ($tokens->[$$index] ne $openingDelimiter) + { return undef; }; + + + $$index++; + if (defined $startContentIndexRef) + { $$startContentIndexRef = $$index; }; + + while ($$index < scalar @$tokens) + { + if ($tokens->[$$index] eq "\\") + { + # Skip the token after it. + $$index += 2; + } + elsif ($tokens->[$$index] eq "\n") + { + $$lineNumber++; + $$index++; + } + elsif ($tokens->[$$index] eq $closingDelimiter) + { + if (defined $endContentIndexRef) + { $$endContentIndexRef = $$index; }; + + $$index++; + last; + } + else + { + $$index++; + }; + }; + + if ($$index >= scalar @$tokens && defined $endContentIndexRef) + { $$endContentIndexRef = scalar @$tokens; }; + + return 1; + }; + + +# +# Function: SkipRestOfLine +# +# Moves the position to the token following the next line break, or past the end of the tokens array if there is none. Useful for +# line comments. +# +# Note that it skips blindly. It assumes there cannot be anything of interest, such as a string delimiter, between the position +# and the end of the line. +# +# Parameters: +# +# indexRef - A reference to the position's index into <Tokens()>. +# lineNumberRef - A reference to the position's line number. + +sub SkipRestOfLine #(indexRef, lineNumberRef) + { + my ($self, $index, $lineNumber) = @_; + my $tokens = $self->Tokens(); + + while ($$index < scalar @$tokens) + { + if ($tokens->[$$index] eq "\n") + { + $$lineNumber++; + $$index++; + last; + } + else + { + $$index++; + }; + }; + }; + + +# +# Function: SkipUntilAfter +# +# Moves the position to the token following the next occurance of a particular token sequence, or past the end of the tokens +# array if it never occurs. Useful for multiline comments. +# +# Note that it skips blindly. It assumes there cannot be anything of interest, such as a string delimiter, between the position +# and the end of the line. +# +# Parameters: +# +# indexRef - A reference to the position's index. +# lineNumberRef - A reference to the position's line number. +# token - A token that must be matched. Can be specified multiple times to match a sequence of tokens. +# +sub SkipUntilAfter #(indexRef, lineNumberRef, token, token, ...) + { + my ($self, $index, $lineNumber, @target) = @_; + my $tokens = $self->Tokens(); + + while ($$index < scalar @$tokens) + { + if ($tokens->[$$index] eq $target[0] && ($$index + scalar @target) <= scalar @$tokens) + { + my $match = 1; + + for (my $i = 1; $i < scalar @target; $i++) + { + if ($tokens->[$$index+$i] ne $target[$i]) + { + $match = 0; + last; + }; + }; + + if ($match) + { + $$index += scalar @target; + return; + }; + }; + + if ($tokens->[$$index] eq "\n") + { + $$lineNumber++; + $$index++; + } + else + { + $$index++; + }; + }; + }; + + +# +# Function: IsFirstLineToken +# +# Returns whether the position is at the first token of a line, not including whitespace. +# +# Parameters: +# +# index - The index of the position. +# +sub IsFirstLineToken #(index) + { + my ($self, $index) = @_; + my $tokens = $self->Tokens(); + + if ($index == 0) + { return 1; }; + + $index--; + + if ($tokens->[$index] =~ /^[ \t]/) + { $index--; }; + + if ($index <= 0 || $tokens->[$index] eq "\n") + { return 1; } + else + { return undef; }; + }; + + +# +# Function: IsLastLineToken +# +# Returns whether the position is at the last token of a line, not including whitespace. +# +# Parameters: +# +# index - The index of the position. +# +sub IsLastLineToken #(index) + { + my ($self, $index) = @_; + my $tokens = $self->Tokens(); + + do + { $index++; } + while ($index < scalar @$tokens && $tokens->[$index] =~ /^[ \t]/); + + if ($index >= scalar @$tokens || $tokens->[$index] eq "\n") + { return 1; } + else + { return undef; }; + }; + + +# +# Function: IsAtSequence +# +# Returns whether the position is at a sequence of tokens. +# +# Parameters: +# +# index - The index of the position. +# token - A token to match. Specify multiple times to specify the sequence. +# +sub IsAtSequence #(index, token, token, token ...) + { + my ($self, $index, @target) = @_; + my $tokens = $self->Tokens(); + + if ($index + scalar @target > scalar @$tokens) + { return undef; }; + + for (my $i = 0; $i < scalar @target; $i++) + { + if ($tokens->[$index + $i] ne $target[$i]) + { return undef; }; + }; + + return 1; + }; + + +# +# Function: IsBackslashed +# +# Returns whether the position is after a backslash. +# +# Parameters: +# +# index - The index of the postition. +# +sub IsBackslashed #(index) + { + my ($self, $index) = @_; + my $tokens = $self->Tokens(); + + if ($index > 0 && $tokens->[$index - 1] eq "\\") + { return 1; } + else + { return undef; }; + }; + + + +############################################################################### +# +# Group: Scope Functions +# +# These functions provide a nice scope stack implementation for language-specific parsers to use. The default implementation +# makes the following assumptions. +# +# - Packages completely replace one another, rather than concatenating. You need to concatenate manually if that's the +# behavior. +# +# - Packages inherit, so if a scope level doesn't set its own, the package is the same as the parent scope's. +# + + +# +# Function: ClearScopeStack +# +# Clears the scope stack for a new file. Not necessary if you call <ParseForCommentsAndTokens()>. +# +sub ClearScopeStack + { + my ($self) = @_; + $self->[SCOPE_STACK] = [ NaturalDocs::Languages::Advanced::Scope->New(undef, undef) ]; + $self->[SCOPE_RECORD] = [ NaturalDocs::Languages::Advanced::ScopeChange->New(undef, 1) ]; + }; + + +# +# Function: StartScope +# +# Records a new scope level. +# +# Parameters: +# +# closingSymbol - The closing symbol of the scope. +# lineNumber - The line number where the scope begins. +# package - The package <SymbolString> of the scope. Undef means no change. +# +sub StartScope #(closingSymbol, lineNumber, package) + { + my ($self, $closingSymbol, $lineNumber, $package) = @_; + + push @{$self->[SCOPE_STACK]}, + NaturalDocs::Languages::Advanced::Scope->New($closingSymbol, $package, $self->CurrentUsing()); + + $self->AddToScopeRecord($self->CurrentScope(), $lineNumber); + }; + + +# +# Function: EndScope +# +# Records the end of the current scope level. Note that this is blind; you need to manually check <ClosingScopeSymbol()> if +# you need to determine if it is correct to do so. +# +# Parameters: +# +# lineNumber - The line number where the scope ends. +# +sub EndScope #(lineNumber) + { + my ($self, $lineNumber) = @_; + + if (scalar @{$self->[SCOPE_STACK]} > 1) + { pop @{$self->[SCOPE_STACK]}; }; + + $self->AddToScopeRecord($self->CurrentScope(), $lineNumber); + }; + + +# +# Function: ClosingScopeSymbol +# +# Returns the symbol that ends the current scope level, or undef if we are at the top level. +# +sub ClosingScopeSymbol + { + my ($self) = @_; + return $self->[SCOPE_STACK]->[-1]->ClosingSymbol(); + }; + + +# +# Function: CurrentScope +# +# Returns the current calculated scope, or undef if global. The default implementation just returns <CurrentPackage()>. This +# is a separate function because C++ may need to track namespaces and classes separately, and so the current scope would +# be a concatenation of them. +# +sub CurrentScope + { + return $_[0]->CurrentPackage(); + }; + + +# +# Function: CurrentPackage +# +# Returns the current calculated package or class, or undef if none. +# +sub CurrentPackage + { + my ($self) = @_; + + my $package; + + for (my $index = scalar @{$self->[SCOPE_STACK]} - 1; $index >= 0 && !defined $package; $index--) + { + $package = $self->[SCOPE_STACK]->[$index]->Package(); + }; + + return $package; + }; + + +# +# Function: SetPackage +# +# Sets the package for the current scope level. +# +# Parameters: +# +# package - The new package <SymbolString>. +# lineNumber - The line number the new package starts on. +# +sub SetPackage #(package, lineNumber) + { + my ($self, $package, $lineNumber) = @_; + $self->[SCOPE_STACK]->[-1]->SetPackage($package); + + $self->AddToScopeRecord($self->CurrentScope(), $lineNumber); + }; + + +# +# Function: CurrentUsing +# +# Returns the current calculated arrayref of <SymbolStrings> from Using statements, or undef if none. +# +sub CurrentUsing + { + my ($self) = @_; + return $self->[SCOPE_STACK]->[-1]->Using(); + }; + + +# +# Function: AddUsing +# +# Adds a Using <SymbolString> to the current scope. +# +sub AddUsing #(using) + { + my ($self, $using) = @_; + $self->[SCOPE_STACK]->[-1]->AddUsing($using); + }; + + + +############################################################################### +# Group: Support Functions + + +# +# Function: AddToScopeRecord +# +# Adds a change to the scope record, condensing unnecessary entries. +# +# Parameters: +# +# newScope - What the scope <SymbolString> changed to. +# lineNumber - Where the scope changed. +# +sub AddToScopeRecord #(newScope, lineNumber) + { + my ($self, $scope, $lineNumber) = @_; + my $scopeRecord = $self->ScopeRecord(); + + if ($scope ne $scopeRecord->[-1]->Scope()) + { + if ($scopeRecord->[-1]->LineNumber() == $lineNumber) + { $scopeRecord->[-1]->SetScope($scope); } + else + { push @$scopeRecord, NaturalDocs::Languages::Advanced::ScopeChange->New($scope, $lineNumber); }; + }; + }; + + +# +# Function: CreateString +# +# Converts the specified tokens into a string and returns it. +# +# Parameters: +# +# startIndex - The starting index to convert. +# endIndex - The ending index, which is *not inclusive*. +# +# Returns: +# +# The string. +# +sub CreateString #(startIndex, endIndex) + { + my ($self, $startIndex, $endIndex) = @_; + my $tokens = $self->Tokens(); + + my $string; + + while ($startIndex < $endIndex && $startIndex < scalar @$tokens) + { + $string .= $tokens->[$startIndex]; + $startIndex++; + }; + + return $string; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Languages/Advanced/Scope.pm b/docs/tool/Modules/NaturalDocs/Languages/Advanced/Scope.pm new file mode 100644 index 00000000..e1e50a95 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Languages/Advanced/Scope.pm @@ -0,0 +1,95 @@ +############################################################################### +# +# Class: NaturalDocs::Languages::Advanced::Scope +# +############################################################################### +# +# A class used to store a scope level. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Languages::Advanced::Scope; + +# +# Constants: Implementation +# +# The object is implemented as a blessed arrayref. The constants below are used as indexes. +# +# CLOSING_SYMBOL - The closing symbol character of the scope. +# PACKAGE - The package <SymbolString> of the scope. +# USING - An arrayref of <SymbolStrings> for using statements, or undef if none. +# +use NaturalDocs::DefineMembers 'CLOSING_SYMBOL', 'PACKAGE', 'USING'; +# Dependency: New() depends on the order of these constants as well as that there is no inherited members. + + +# +# Function: New +# +# Creates and returns a new object. +# +# Parameters: +# +# closingSymbol - The closing symbol character of the scope. +# package - The package <SymbolString> of the scope. +# using - An arrayref of using <SymbolStrings>, or undef if none. The contents of the array will be duplicated. +# +# If package is set to undef, it is assumed that it inherits the value of the previous scope on the stack. +# +sub New #(closingSymbol, package, using) + { + # Dependency: This depends on the order of the parameters matching the constants, and that there are no inherited + # members. + my $package = shift; + + my $object = [ @_ ]; + bless $object, $package; + + if (defined $object->[USING]) + { $object->[USING] = [ @{$object->[USING]} ]; }; + + return $object; + }; + + +# Function: ClosingSymbol +# Returns the closing symbol character of the scope. +sub ClosingSymbol + { return $_[0]->[CLOSING_SYMBOL]; }; + +# Function: Package +# Returns the package <SymbolString> of the scope, or undef if none. +sub Package + { return $_[0]->[PACKAGE]; }; + +# Function: SetPackage +# Sets the package <SymbolString> of the scope. +sub SetPackage #(package) + { $_[0]->[PACKAGE] = $_[1]; }; + +# Function: Using +# Returns an arrayref of <SymbolStrings> for using statements, or undef if none +sub Using + { return $_[0]->[USING]; }; + +# Function: AddUsing +# Adds a <SymbolString> to the <Using()> array. +sub AddUsing #(using) + { + my ($self, $using) = @_; + + if (!defined $self->[USING]) + { $self->[USING] = [ ]; }; + + push @{$self->[USING]}, $using; + }; + + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Languages/Advanced/ScopeChange.pm b/docs/tool/Modules/NaturalDocs/Languages/Advanced/ScopeChange.pm new file mode 100644 index 00000000..24de5503 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Languages/Advanced/ScopeChange.pm @@ -0,0 +1,70 @@ +############################################################################### +# +# Class: NaturalDocs::Languages::Advanced::ScopeChange +# +############################################################################### +# +# A class used to store a scope change. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Languages::Advanced::ScopeChange; + +# +# Constants: Implementation +# +# The object is implemented as a blessed arrayref. The constants below are used as indexes. +# +# SCOPE - The new scope <SymbolString>. +# LINE_NUMBER - The line number of the change. +# +use NaturalDocs::DefineMembers 'SCOPE', 'LINE_NUMBER'; +# Dependency: New() depends on the order of these constants as well as that there is no inherited members. + + +# +# Function: New +# +# Creates and returns a new object. +# +# Parameters: +# +# scope - The <SymbolString> the scope was changed to. +# lineNumber - What line it occurred on. +# +sub New #(scope, lineNumber) + { + # Dependency: This depends on the order of the parameters matching the constants, and that there are no inherited + # members. + my $self = shift; + + my $object = [ @_ ]; + bless $object, $self; + + return $object; + }; + + +# Function: Scope +# Returns the <SymbolString> the scope was changed to. +sub Scope + { return $_[0]->[SCOPE]; }; + +# Function: SetScope +# Replaces the <SymbolString> the scope was changed to. +sub SetScope #(scope) + { $_[0]->[SCOPE] = $_[1]; }; + +# Function: LineNumber +# Returns the line number of the change. +sub LineNumber + { return $_[0]->[LINE_NUMBER]; }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Languages/Base.pm b/docs/tool/Modules/NaturalDocs/Languages/Base.pm new file mode 100644 index 00000000..f89b7045 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Languages/Base.pm @@ -0,0 +1,832 @@ +############################################################################### +# +# Class: NaturalDocs::Languages::Base +# +############################################################################### +# +# A base class for all programming language parsers. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Languages::Base; + +use NaturalDocs::DefineMembers 'NAME', 'Name()', + 'EXTENSIONS', 'Extensions()', 'SetExtensions() duparrayref', + 'SHEBANG_STRINGS', 'ShebangStrings()', 'SetShebangStrings() duparrayref', + 'IGNORED_PREFIXES', + 'ENUM_VALUES'; + +use base 'Exporter'; +our @EXPORT = ('ENUM_GLOBAL', 'ENUM_UNDER_TYPE', 'ENUM_UNDER_PARENT'); + + +# +# Constants: EnumValuesType +# +# How enum values are handled in the language. +# +# ENUM_GLOBAL - Values are always global and thus 'value'. +# ENUM_UNDER_TYPE - Values are under the type in the hierarchy, and thus 'package.enum.value'. +# ENUM_UNDER_PARENT - Values are under the parent in the hierarchy, putting them on the same level as the enum itself. Thus +# 'package.value'. +# +use constant ENUM_GLOBAL => 1; +use constant ENUM_UNDER_TYPE => 2; +use constant ENUM_UNDER_PARENT => 3; + + +# +# Handle: SOURCEFILEHANDLE +# +# The handle of the source file currently being parsed. +# + + +# +# Function: New +# +# Creates and returns a new object. +# +# Parameters: +# +# name - The name of the language. +# +sub New #(name) + { + my ($selfPackage, $name) = @_; + + my $object = [ ]; + + $object->[NAME] = $name; + + bless $object, $selfPackage; + return $object; + }; + + +# +# Functions: Members +# +# Name - Returns the language's name. +# Extensions - Returns an arrayref of the language's file extensions, or undef if none. +# SetExtensions - Replaces the arrayref of the language's file extensions. +# ShebangStrings - Returns an arrayref of the language's shebang strings, or undef if none. +# SetShebangStrings - Replaces the arrayref of the language's shebang strings. +# + +# +# Function: PackageSeparator +# Returns the language's package separator string. +# +sub PackageSeparator + { return '.'; }; + +# +# Function: PackageSeparatorWasSet +# Returns whether the language's package separator string was ever changed from the default. +# +sub PackageSeparatorWasSet + { return 0; }; + + +# +# Function: EnumValues +# Returns the <EnumValuesType> that describes how the language handles enums. +# +sub EnumValues + { return ENUM_GLOBAL; }; + + +# +# Function: IgnoredPrefixesFor +# +# Returns an arrayref of ignored prefixes for the passed <TopicType>, or undef if none. The array is sorted so that the longest +# prefixes are first. +# +sub IgnoredPrefixesFor #(type) + { + my ($self, $type) = @_; + + if (defined $self->[IGNORED_PREFIXES]) + { return $self->[IGNORED_PREFIXES]->{$type}; } + else + { return undef; }; + }; + + +# +# Function: SetIgnoredPrefixesFor +# +# Replaces the arrayref of ignored prefixes for the passed <TopicType>. +# +sub SetIgnoredPrefixesFor #(type, prefixes) + { + my ($self, $type, $prefixesRef) = @_; + + if (!defined $self->[IGNORED_PREFIXES]) + { $self->[IGNORED_PREFIXES] = { }; }; + + if (!defined $prefixesRef) + { delete $self->[IGNORED_PREFIXES]->{$type}; } + else + { + my $prefixes = [ @$prefixesRef ]; + + # Sort prefixes to be longest to shortest. + @$prefixes = sort { length $b <=> length $a } @$prefixes; + + $self->[IGNORED_PREFIXES]->{$type} = $prefixes; + }; + }; + + +# +# Function: HasIgnoredPrefixes +# +# Returns whether the language has any ignored prefixes at all. +# +sub HasIgnoredPrefixes + { return defined $_[0]->[IGNORED_PREFIXES]; }; + + +# +# Function: CopyIgnoredPrefixesOf +# +# Copies all the ignored prefix settings of the passed <NaturalDocs::Languages::Base> object. +# +sub CopyIgnoredPrefixesOf #(language) + { + my ($self, $language) = @_; + + if ($language->HasIgnoredPrefixes()) + { + $self->[IGNORED_PREFIXES] = { }; + + while (my ($topicType, $prefixes) = each %{$language->[IGNORED_PREFIXES]}) + { + $self->[IGNORED_PREFIXES]->{$topicType} = [ @$prefixes ]; + }; + }; + }; + + + +############################################################################### +# Group: Parsing Functions + + +# +# Function: ParseFile +# +# Parses the passed source file, sending comments acceptable for documentation to <NaturalDocs::Parser->OnComment()>. +# This *must* be defined by a subclass. +# +# Parameters: +# +# sourceFile - The <FileName> of the source file to parse. +# topicList - A reference to the list of <NaturalDocs::Parser::ParsedTopics> being built by the file. +# +# Returns: +# +# The array ( autoTopics, scopeRecord ). +# +# autoTopics - An arrayref of automatically generated <NaturalDocs::Parser::ParsedTopics> from the file, or undef if none. +# scopeRecord - An arrayref of <NaturalDocs::Languages::Advanced::ScopeChanges>, or undef if none. +# + + +# +# Function: ParsePrototype +# +# Parses the prototype and returns it as a <NaturalDocs::Languages::Prototype> object. +# +# Parameters: +# +# type - The <TopicType>. +# prototype - The text prototype. +# +# Returns: +# +# A <NaturalDocs::Languages::Prototype> object. +# +sub ParsePrototype #(type, prototype) + { + my ($self, $type, $prototype) = @_; + + my $isClass = NaturalDocs::Topics->TypeInfo($type)->ClassHierarchy(); + + if ($prototype !~ /\(.*[^ ].*\)/ && (!$isClass || $prototype !~ /\{.*[^ ].*\}/)) + { + my $object = NaturalDocs::Languages::Prototype->New($prototype); + return $object; + }; + + + # Parse the parameters out of the prototype. + + my @tokens = $prototype =~ /([^\(\)\[\]\{\}\<\>\'\"\,\;]+|.)/g; + + my $parameter; + my @parameterLines; + + my @symbolStack; + my $finishedParameters; + + my ($beforeParameters, $afterParameters); + + foreach my $token (@tokens) + { + if ($finishedParameters) + { $afterParameters .= $token; } + + elsif ($symbolStack[-1] eq '\'' || $symbolStack[-1] eq '"') + { + if ($symbolStack[0] eq '(' || ($isClass && $symbolStack[0] eq '{')) + { $parameter .= $token; } + else + { $beforeParameters .= $token; }; + + if ($token eq $symbolStack[-1]) + { pop @symbolStack; }; + } + + elsif ($token =~ /^[\(\[\{\<\'\"]$/) + { + if ($symbolStack[0] eq '(' || ($isClass && $symbolStack[0] eq '{')) + { $parameter .= $token; } + else + { $beforeParameters .= $token; }; + + push @symbolStack, $token; + } + + elsif ( ($token eq ')' && $symbolStack[-1] eq '(') || + ($token eq ']' && $symbolStack[-1] eq '[') || + ($token eq '}' && $symbolStack[-1] eq '{') || + ($token eq '>' && $symbolStack[-1] eq '<') ) + { + if ($symbolStack[0] eq '(') + { + if ($token eq ')' && scalar @symbolStack == 1) + { + if ($parameter ne ' ') + { push @parameterLines, $parameter; }; + + $finishedParameters = 1; + $afterParameters .= $token; + } + else + { $parameter .= $token; }; + } + elsif ($isClass && $symbolStack[0] eq '{') + { + if ($token eq '}' && scalar @symbolStack == 1) + { + if ($parameter ne ' ') + { push @parameterLines, $parameter; }; + + $finishedParameters = 1; + $afterParameters .= $token; + } + else + { $parameter .= $token; }; + } + else + { + $beforeParameters .= $token; + }; + + pop @symbolStack; + } + + elsif ($token eq ',' || $token eq ';') + { + if ($symbolStack[0] eq '(' || ($isClass && $symbolStack[0] eq '{')) + { + if (scalar @symbolStack == 1) + { + push @parameterLines, $parameter . $token; + $parameter = undef; + } + else + { + $parameter .= $token; + }; + } + else + { + $beforeParameters .= $token; + }; + } + + else + { + if ($symbolStack[0] eq '(' || ($isClass && $symbolStack[0] eq '{')) + { $parameter .= $token; } + else + { $beforeParameters .= $token; }; + }; + }; + + foreach my $part (\$beforeParameters, \$afterParameters) + { + $$part =~ s/^ //; + $$part =~ s/ $//; + }; + + my $prototypeObject = NaturalDocs::Languages::Prototype->New($beforeParameters, $afterParameters); + + + # Parse the actual parameters. + + foreach my $parameterLine (@parameterLines) + { + $prototypeObject->AddParameter( $self->ParseParameterLine($parameterLine) ); + }; + + return $prototypeObject; + }; + + +# +# Function: ParseParameterLine +# +# Parses a prototype parameter line and returns it as a <NaturalDocs::Languages::Prototype::Parameter> object. +# +# This vesion assumes a C++ style line. If you need a Pascal style line, override this function to forward to +# <ParsePascalParameterLine()>. +# +# > Function(parameter, type parameter, type parameter = value); +# +sub ParseParameterLine #(line) + { + my ($self, $line) = @_; + + $line =~ s/^ //; + $line =~ s/ $//; + + my @tokens = $line =~ /([^ \(\)\{\}\[\]\<\>\'\"\=]+|.)/g; + + my @symbolStack; + my @parameterWords = ( undef ); + my ($defaultValue, $defaultValuePrefix, $inDefaultValue); + + foreach my $token (@tokens) + { + if ($inDefaultValue) + { $defaultValue .= $token; } + + elsif ($symbolStack[-1] eq '\'' || $symbolStack[-1] eq '"') + { + $parameterWords[-1] .= $token; + + if ($token eq $symbolStack[-1]) + { pop @symbolStack; }; + } + + elsif ($token =~ /^[\(\[\{\<\'\"]$/) + { + push @symbolStack, $token; + $parameterWords[-1] .= $token; + } + + elsif ( ($token eq ')' && $symbolStack[-1] eq '(') || + ($token eq ']' && $symbolStack[-1] eq '[') || + ($token eq '}' && $symbolStack[-1] eq '{') || + ($token eq '>' && $symbolStack[-1] eq '<') ) + { + pop @symbolStack; + $parameterWords[-1] .= $token; + } + + elsif ($token eq ' ') + { + if (!scalar @symbolStack) + { push @parameterWords, undef; } + else + { $parameterWords[-1] .= $token; }; + } + + elsif ($token eq '=') + { + if (!scalar @symbolStack) + { + $defaultValuePrefix = $token; + $inDefaultValue = 1; + } + else + { $parameterWords[-1] .= $token; }; + } + + else + { + $parameterWords[-1] .= $token; + }; + }; + + my ($name, $namePrefix, $type, $typePrefix); + + if (!$parameterWords[-1]) + { pop @parameterWords; }; + + $name = pop @parameterWords; + + if ($parameterWords[-1]=~ /([\*\&]+)$/) + { + $namePrefix = $1; + $parameterWords[-1] = substr($parameterWords[-1], 0, 0 - length($namePrefix)); + $parameterWords[-1] =~ s/ $//; + + if (!$parameterWords[-1]) + { pop @parameterWords; }; + } + elsif ($name =~ /^([\*\&]+)/) + { + $namePrefix = $1; + $name = substr($name, length($namePrefix)); + $name =~ s/^ //; + }; + + $type = pop @parameterWords; + $typePrefix = join(' ', @parameterWords); + + if ($typePrefix) + { $typePrefix .= ' '; }; + + if ($type =~ /^([a-z0-9_\:\.]+(?:\.|\:\:))[a-z0-9_]/i) + { + my $attachedTypePrefix = $1; + + $typePrefix .= $attachedTypePrefix; + $type = substr($type, length($attachedTypePrefix)); + }; + + $defaultValue =~ s/ $//; + + return NaturalDocs::Languages::Prototype::Parameter->New($type, $typePrefix, $name, $namePrefix, + $defaultValue, $defaultValuePrefix); + }; + + +# +# Function: ParsePascalParameterLine +# +# Parses a Pascal-like prototype parameter line and returns it as a <NaturalDocs::Languages::Prototype::Parameter> object. +# Pascal lines are as follows: +# +# > Function (name: type; name, name: type := value) +# +# Also supports ActionScript lines +# +# > Function (name: type, name, name: type = value) +# +sub ParsePascalParameterLine #(line) + { + my ($self, $line) = @_; + + $line =~ s/^ //; + $line =~ s/ $//; + + my @tokens = $line =~ /([^\(\)\{\}\[\]\<\>\'\"\=\:]+|\:\=|.)/g; + my ($type, $name, $defaultValue, $defaultValuePrefix, $afterName, $afterDefaultValue); + my @symbolStack; + + foreach my $token (@tokens) + { + if ($afterDefaultValue) + { $defaultValue .= $token; } + + elsif ($symbolStack[-1] eq '\'' || $symbolStack[-1] eq '"') + { + if ($afterName) + { $type .= $token; } + else + { $name .= $token; }; + + if ($token eq $symbolStack[-1]) + { pop @symbolStack; }; + } + + elsif ($token =~ /^[\(\[\{\<\'\"]$/) + { + push @symbolStack, $token; + + if ($afterName) + { $type .= $token; } + else + { $name .= $token; }; + } + + elsif ( ($token eq ')' && $symbolStack[-1] eq '(') || + ($token eq ']' && $symbolStack[-1] eq '[') || + ($token eq '}' && $symbolStack[-1] eq '{') || + ($token eq '>' && $symbolStack[-1] eq '<') ) + { + pop @symbolStack; + + if ($afterName) + { $type .= $token; } + else + { $name .= $token; }; + } + + elsif ($afterName) + { + if (($token eq ':=' || $token eq '=') && !scalar @symbolStack) + { + $defaultValuePrefix = $token; + $afterDefaultValue = 1; + } + else + { $type .= $token; }; + } + + elsif ($token eq ':' && !scalar @symbolStack) + { + $name .= $token; + $afterName = 1; + } + + else + { $name .= $token; }; + }; + + foreach my $part (\$type, \$name, \$defaultValue) + { + $$part =~ s/^ //; + $$part =~ s/ $//; + }; + + return NaturalDocs::Languages::Prototype::Parameter->New($type, undef, $name, undef, $defaultValue, $defaultValuePrefix); + }; + + +# +# Function: TypeBeforeParameter +# +# Returns whether the type appears before the parameter in prototypes. +# +# For example, it does in C++ +# > void Function (int a, int b) +# +# but does not in Pascal +# > function Function (a: int; b, c: int) +# +sub TypeBeforeParameter + { + return 1; + }; + + + +# +# Function: IgnoredPrefixLength +# +# Returns the length of the prefix that should be ignored in the index, or zero if none. +# +# Parameters: +# +# name - The name of the symbol. +# type - The symbol's <TopicType>. +# +# Returns: +# +# The length of the prefix to ignore, or zero if none. +# +sub IgnoredPrefixLength #(name, type) + { + my ($self, $name, $type) = @_; + + foreach my $prefixes ($self->IgnoredPrefixesFor($type), $self->IgnoredPrefixesFor(::TOPIC_GENERAL())) + { + if (defined $prefixes) + { + foreach my $prefix (@$prefixes) + { + if (substr($name, 0, length($prefix)) eq $prefix) + { return length($prefix); }; + }; + }; + }; + + return 0; + }; + + + +############################################################################### +# Group: Support Functions + + +# +# Function: StripOpeningSymbols +# +# Determines if the line starts with any of the passed symbols, and if so, replaces it with spaces. This only happens +# if the only thing before it on the line is whitespace. +# +# Parameters: +# +# lineRef - A reference to the line to check. +# symbols - An arrayref of the symbols to check for. +# +# Returns: +# +# If the line starts with any of the passed comment symbols, it will replace it in the line with spaces and return the symbol. +# If the line doesn't, it will leave the line alone and return undef. +# +sub StripOpeningSymbols #(lineRef, symbols) + { + my ($self, $lineRef, $symbols) = @_; + + if (!defined $symbols) + { return undef; }; + + my ($index, $symbol) = ::FindFirstSymbol($$lineRef, $symbols); + + if ($index != -1 && substr($$lineRef, 0, $index) =~ /^[ \t]*$/) + { + return substr($$lineRef, $index, length($symbol), ' ' x length($symbol)); + }; + + return undef; + }; + + +# +# Function: StripOpeningJavaDocSymbols +# +# Determines if the line starts with any of the passed symbols, and if so, replaces it with spaces. This only happens +# if the only thing before it on the line is whitespace and the next character after it is whitespace or the end of the line. +# +# Parameters: +# +# lineRef - A reference to the line to check. +# symbols - An arrayref of the symbols to check for. +# +# Returns: +# +# If the line starts with any of the passed comment symbols, it will replace it in the line with spaces and return the symbol. +# If the line doesn't, it will leave the line alone and return undef. +# +sub StripOpeningJavaDocSymbols #(lineRef, symbols) + { + my ($self, $lineRef, $symbols) = @_; + + if (!defined $symbols) + { return undef; }; + + my ($index, $symbol) = ::FindFirstSymbol($$lineRef, $symbols); + + if ($index != -1 && substr($$lineRef, 0, $index) =~ /^[ \t]*$/ && substr($$lineRef, $index + length($symbol), 1) =~ /^[ \t]?$/) + { + return substr($$lineRef, $index, length($symbol), ' ' x length($symbol)); + }; + + return undef; + }; + + +# +# Function: StripOpeningBlockSymbols +# +# Determines if the line starts with any of the opening symbols in the passed symbol pairs, and if so, replaces it with spaces. +# This only happens if the only thing before it on the line is whitespace. +# +# Parameters: +# +# lineRef - A reference to the line to check. +# symbolPairs - An arrayref of the symbol pairs to check for. Pairs are specified as two consecutive array entries, with the +# opening symbol first. +# +# Returns: +# +# If the line starts with any of the opening symbols, it will replace it in the line with spaces and return the closing symbol. +# If the line doesn't, it will leave the line alone and return undef. +# +sub StripOpeningBlockSymbols #(lineRef, symbolPairs) + { + my ($self, $lineRef, $symbolPairs) = @_; + + if (!defined $symbolPairs) + { return undef; }; + + for (my $i = 0; $i < scalar @$symbolPairs; $i += 2) + { + my $index = index($$lineRef, $symbolPairs->[$i]); + + if ($index != -1 && substr($$lineRef, 0, $index) =~ /^[ \t]*$/) + { + substr($$lineRef, $index, length($symbolPairs->[$i]), ' ' x length($symbolPairs->[$i])); + return $symbolPairs->[$i + 1]; + }; + }; + + return undef; + }; + + +# +# Function: StripOpeningJavaDocBlockSymbols +# +# Determines if the line starts with any of the opening symbols in the passed symbol pairs, and if so, replaces it with spaces. +# This only happens if the only thing before it on the line is whitespace and the next character is whitespace or the end of the line. +# +# Parameters: +# +# lineRef - A reference to the line to check. +# symbolPairs - An arrayref of the symbol pairs to check for. Pairs are specified as two consecutive array entries, with the +# opening symbol first. +# +# Returns: +# +# If the line starts with any of the opening symbols, it will replace it in the line with spaces and return the closing symbol. +# If the line doesn't, it will leave the line alone and return undef. +# +sub StripOpeningJavaDocBlockSymbols #(lineRef, symbolPairs) + { + my ($self, $lineRef, $symbolPairs) = @_; + + if (!defined $symbolPairs) + { return undef; }; + + for (my $i = 0; $i < scalar @$symbolPairs; $i += 2) + { + my $index = index($$lineRef, $symbolPairs->[$i]); + + if ($index != -1 && substr($$lineRef, 0, $index) =~ /^[ \t]*$/ && + substr($$lineRef, $index + length($symbolPairs->[$i]), 1) =~ /^[ \t]?$/) + { + substr($$lineRef, $index, length($symbolPairs->[$i]), ' ' x length($symbolPairs->[$i])); + return $symbolPairs->[$i + 1]; + }; + }; + + return undef; + }; + + +# +# Function: StripClosingSymbol +# +# Determines if the line contains a symbol, and if so, truncates it just before the symbol. +# +# Parameters: +# +# lineRef - A reference to the line to check. +# symbol - The symbol to check for. +# +# Returns: +# +# The remainder of the line, or undef if the symbol was not found. +# +sub StripClosingSymbol #(lineRef, symbol) + { + my ($self, $lineRef, $symbol) = @_; + + my $index = index($$lineRef, $symbol); + + if ($index != -1) + { + my $lineRemainder = substr($$lineRef, $index + length($symbol)); + $$lineRef = substr($$lineRef, 0, $index); + + return $lineRemainder; + } + else + { return undef; }; + }; + + +# +# Function: NormalizePrototype +# +# Normalizes a prototype. Specifically, condenses spaces, tabs, and line breaks into single spaces and removes leading and +# trailing ones. +# +# Parameters: +# +# prototype - The original prototype string. +# +# Returns: +# +# The normalized prototype. +# +sub NormalizePrototype #(prototype) + { + my ($self, $prototype) = @_; + + $prototype =~ tr/ \t\r\n/ /s; + $prototype =~ s/^ //; + $prototype =~ s/ $//; + + return $prototype; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Languages/CSharp.pm b/docs/tool/Modules/NaturalDocs/Languages/CSharp.pm new file mode 100644 index 00000000..5bcd50be --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Languages/CSharp.pm @@ -0,0 +1,1484 @@ +############################################################################### +# +# Class: NaturalDocs::Languages::CSharp +# +############################################################################### +# +# A subclass to handle the language variations of C#. +# +# +# Topic: Language Support +# +# Supported: +# +# - Classes +# - Namespaces (no topic generated) +# - Functions +# - Constructors and Destructors +# - Properties +# - Indexers +# - Operators +# - Delegates +# - Variables +# - Constants +# - Events +# - Enums +# +# Not supported yet: +# +# - Autodocumenting enum members +# - Using alias +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Languages::CSharp; + +use base 'NaturalDocs::Languages::Advanced'; + + +############################################################################### +# Group: Package Variables + +# +# hash: classKeywords +# An existence hash of all the acceptable class keywords. The keys are in all lowercase. +# +my %classKeywords = ( 'class' => 1, + 'struct' => 1, + 'interface' => 1 ); + +# +# hash: classModifiers +# An existence hash of all the acceptable class modifiers. The keys are in all lowercase. +# +my %classModifiers = ( 'new' => 1, + 'public' => 1, + 'protected' => 1, + 'internal' => 1, + 'private' => 1, + 'abstract' => 1, + 'sealed' => 1, + 'unsafe' => 1, + 'static' => 1 ); + +# +# hash: functionModifiers +# An existence hash of all the acceptable function modifiers. Also applies to properties. Also encompasses those for operators +# and indexers, but have more than are valid for them. The keys are in all lowercase. +# +my %functionModifiers = ( 'new' => 1, + 'public' => 1, + 'protected' => 1, + 'internal' => 1, + 'private' => 1, + 'static' => 1, + 'virtual' => 1, + 'sealed' => 1, + 'override' => 1, + 'abstract' => 1, + 'extern' => 1, + 'unsafe' => 1 ); + +# +# hash: variableModifiers +# An existence hash of all the acceptable variable modifiers. The keys are in all lowercase. +# +my %variableModifiers = ( 'new' => 1, + 'public' => 1, + 'protected' => 1, + 'internal' => 1, + 'private' => 1, + 'static' => 1, + 'readonly' => 1, + 'volatile' => 1, + 'unsafe' => 1 ); + +# +# hash: enumTypes +# An existence hash of all the possible enum types. The keys are in all lowercase. +# +my %enumTypes = ( 'sbyte' => 1, + 'byte' => 1, + 'short' => 1, + 'ushort' => 1, + 'int' => 1, + 'uint' => 1, + 'long' => 1, + 'ulong' => 1 ); + +# +# hash: impossibleTypeWords +# An existence hash of all the reserved words that cannot be in a type. This includes 'enum' and all modifiers. The keys are in +# all lowercase. +# +my %impossibleTypeWords = ( 'abstract' => 1, 'as' => 1, 'base' => 1, 'break' => 1, 'case' => 1, 'catch' => 1, + 'checked' => 1, 'class' => 1, 'const' => 1, 'continue' => 1, 'default' => 1, 'delegate' => 1, + 'do' => 1, 'else' => 1, 'enum' => 1, 'event' => 1, 'explicit' => 1, 'extern' => 1, + 'false' => 1, 'finally' => 1, 'fixed' => 1, 'for' => 1, 'foreach' => 1, 'goto' => 1, 'if' => 1, + 'implicit' => 1, 'in' => 1, 'interface' => 1, 'internal' => 1, 'is' => 1, 'lock' => 1, + 'namespace' => 1, 'new' => 1, 'null' => 1, 'operator' => 1, 'out' => 1, 'override' => 1, + 'params' => 1, 'private' => 1, 'protected' => 1, 'public' => 1, 'readonly' => 1, 'ref' => 1, + 'return' => 1, 'sealed' => 1, 'sizeof' => 1, 'stackalloc' => 1, 'static' => 1, + 'struct' => 1, 'switch' => 1, 'this' => 1, 'throw' => 1, 'true' => 1, 'try' => 1, 'typeof' => 1, + 'unchecked' => 1, 'unsafe' => 1, 'using' => 1, 'virtual' => 1, 'volatile' => 1, 'while' => 1 ); +# Deleted from the list: object, string, bool, decimal, sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, void + + + +############################################################################### +# Group: Interface Functions + + +# +# Function: PackageSeparator +# Returns the package separator symbol. +# +sub PackageSeparator + { return '.'; }; + + +# +# Function: EnumValues +# Returns the <EnumValuesType> that describes how the language handles enums. +# +sub EnumValues + { return ::ENUM_UNDER_TYPE(); }; + + +# +# Function: ParseFile +# +# Parses the passed source file, sending comments acceptable for documentation to <NaturalDocs::Parser->OnComment()>. +# +# Parameters: +# +# sourceFile - The <FileName> to parse. +# topicList - A reference to the list of <NaturalDocs::Parser::ParsedTopics> being built by the file. +# +# Returns: +# +# The array ( autoTopics, scopeRecord ). +# +# autoTopics - An arrayref of automatically generated topics from the file, or undef if none. +# scopeRecord - An arrayref of <NaturalDocs::Languages::Advanced::ScopeChanges>, or undef if none. +# +sub ParseFile #(sourceFile, topicsList) + { + my ($self, $sourceFile, $topicsList) = @_; + + $self->ParseForCommentsAndTokens($sourceFile, [ '//' ], [ '/*', '*/' ], [ '///' ], [ '/**', '*/' ] ); + + my $tokens = $self->Tokens(); + my $index = 0; + my $lineNumber = 1; + + while ($index < scalar @$tokens) + { + if ($self->TryToSkipWhitespace(\$index, \$lineNumber) || + $self->TryToGetNamespace(\$index, \$lineNumber) || + $self->TryToGetUsing(\$index, \$lineNumber) || + $self->TryToGetClass(\$index, \$lineNumber) || + $self->TryToGetFunction(\$index, \$lineNumber) || + $self->TryToGetOverloadedOperator(\$index, \$lineNumber) || + $self->TryToGetVariable(\$index, \$lineNumber) || + $self->TryToGetEnum(\$index, \$lineNumber) ) + { + # The functions above will handle everything. + } + + elsif ($tokens->[$index] eq '{') + { + $self->StartScope('}', $lineNumber, undef, undef, undef); + $index++; + } + + elsif ($tokens->[$index] eq '}') + { + if ($self->ClosingScopeSymbol() eq '}') + { $self->EndScope($lineNumber); }; + + $index++; + } + + else + { + $self->SkipRestOfStatement(\$index, \$lineNumber); + }; + }; + + + # Don't need to keep these around. + $self->ClearTokens(); + + + my $autoTopics = $self->AutoTopics(); + + my $scopeRecord = $self->ScopeRecord(); + if (defined $scopeRecord && !scalar @$scopeRecord) + { $scopeRecord = undef; }; + + return ( $autoTopics, $scopeRecord ); + }; + + + +############################################################################### +# Group: Statement Parsing Functions +# All functions here assume that the current position is at the beginning of a statement. +# +# Note for developers: I am well aware that the code in these functions do not check if we're past the end of the tokens as +# often as it should. We're making use of the fact that Perl will always return undef in these cases to keep the code simpler. + + +# +# Function: TryToGetNamespace +# +# Determines whether the position is at a namespace declaration statement, and if so, adjusts the scope, skips it, and returns +# true. +# +# Why no topic?: +# +# The main reason we don't create a Natural Docs topic for a namespace is because in order to declare class A.B.C in C#, +# you must do this: +# +# > namespace A.B +# > { +# > class C +# > { ... } +# > } +# +# That would result in a namespace topic whose only purpose is really to qualify C. It would take the default page title, and +# thus the default menu title. So if you have files for A.B.X, A.B.Y, and A.B.Z, they all will appear as A.B on the menu. +# +# If something actually appears in the namespace besides a class, it will be handled by +# <NaturalDocs::Parser->AddPackageDelineators()>. That function will add a package topic to correct the scope. +# +# If the user actually documented it, it will still appear because of the manual topic. +# +sub TryToGetNamespace #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + if (lc($tokens->[$$indexRef]) ne 'namespace') + { return undef; }; + + my $index = $$indexRef + 1; + my $lineNumber = $$lineNumberRef; + + if (!$self->TryToSkipWhitespace(\$index, \$lineNumber)) + { return undef; }; + + my $name; + + while ($tokens->[$index] =~ /^[a-z_\.\@]/i) + { + $name .= $tokens->[$index]; + $index++; + }; + + if (!defined $name) + { return undef; }; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + if ($tokens->[$index] ne '{') + { return undef; }; + + $index++; + + + # We found a valid one if we made it this far. + + my $autoTopic = NaturalDocs::Parser::ParsedTopic->New(::TOPIC_CLASS(), $name, + $self->CurrentScope(), $self->CurrentUsing(), + undef, + undef, undef, $$lineNumberRef); + + # We don't add an auto-topic for namespaces. See the function documentation above. + + NaturalDocs::Parser->OnClass($autoTopic->Package()); + + $self->StartScope('}', $lineNumber, $autoTopic->Package()); + + $$indexRef = $index; + $$lineNumberRef = $lineNumber; + + return 1; + }; + + +# +# Function: TryToGetClass +# +# Determines whether the position is at a class declaration statement, and if so, generates a topic for it, skips it, and +# returns true. +# +# Supported Syntaxes: +# +# - Classes +# - Structs +# - Interfaces +# +sub TryToGetClass #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $index = $$indexRef; + my $lineNumber = $$lineNumberRef; + + my $startIndex = $index; + my $startLine = $lineNumber; + my $needsPrototype = 0; + + if ($self->TryToSkipAttributes(\$index, \$lineNumber)) + { $self->TryToSkipWhitespace(\$index, \$lineNumber); } + + my @modifiers; + + while ($tokens->[$index] =~ /^[a-z]/i && + !exists $classKeywords{lc($tokens->[$index])} && + exists $classModifiers{lc($tokens->[$index])} ) + { + push @modifiers, lc($tokens->[$index]); + $index++; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + }; + + if (!exists $classKeywords{lc($tokens->[$index])}) + { return undef; }; + + my $lcClassKeyword = lc($tokens->[$index]); + + $index++; + + if (!$self->TryToSkipWhitespace(\$index, \$lineNumber)) + { return undef; }; + + my $name; + + while ($tokens->[$index] =~ /^[a-z_\@]/i) + { + $name .= $tokens->[$index]; + $index++; + }; + + if (!defined $name) + { return undef; }; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + if ($tokens->[$index] eq '<') + { + # XXX: This is half-assed. + $index++; + $needsPrototype = 1; + + while ($index < scalar @$tokens && $tokens->[$index] ne '>') + { + $index++; + } + + if ($index < scalar @$tokens) + { + $index++; + } + else + { return undef; } + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + } + + my @parents; + + if ($tokens->[$index] eq ':') + { + do + { + $index++; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + my $parentName; + + while ($tokens->[$index] =~ /^[a-z_\.\@]/i) + { + $parentName .= $tokens->[$index]; + $index++; + }; + + if ($tokens->[$index] eq '<') + { + # XXX: This is still half-assed. + $index++; + $needsPrototype = 1; + + while ($index < scalar @$tokens && $tokens->[$index] ne '>') + { + $index++; + } + + if ($index < scalar @$tokens) + { + $index++; + } + else + { return undef; } + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + } + + if (!defined $parentName) + { return undef; }; + + push @parents, NaturalDocs::SymbolString->FromText($parentName); + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + } + while ($tokens->[$index] eq ',') + }; + + if (lc($tokens->[$index]) eq 'where') + { + # XXX: This is also half-assed + $index++; + + while ($index < scalar @$tokens && $tokens->[$index] ne '{') + { + $index++; + } + } + + if ($tokens->[$index] ne '{') + { return undef; }; + + + # If we made it this far, we have a valid class declaration. + + my @scopeIdentifiers = NaturalDocs::SymbolString->IdentifiersOf($self->CurrentScope()); + $name = join('.', @scopeIdentifiers, $name); + + my $topicType; + + if ($lcClassKeyword eq 'interface') + { $topicType = ::TOPIC_INTERFACE(); } + else + { $topicType = ::TOPIC_CLASS(); }; + + my $prototype; + + if ($needsPrototype) + { + $prototype = $self->CreateString($startIndex, $index); + } + + my $autoTopic = NaturalDocs::Parser::ParsedTopic->New($topicType, $name, + undef, $self->CurrentUsing(), + $prototype, + undef, undef, $$lineNumberRef); + + $self->AddAutoTopic($autoTopic); + NaturalDocs::Parser->OnClass($autoTopic->Package()); + + foreach my $parent (@parents) + { + NaturalDocs::Parser->OnClassParent($autoTopic->Package(), $parent, $self->CurrentScope(), undef, + ::RESOLVE_RELATIVE()); + }; + + $self->StartScope('}', $lineNumber, $autoTopic->Package()); + + $index++; + + $$indexRef = $index; + $$lineNumberRef = $lineNumber; + + return 1; + }; + + +# +# Function: TryToGetUsing +# +# Determines whether the position is at a using statement, and if so, adds it to the current scope, skips it, and returns +# true. +# +# Supported: +# +# - Using +# +# Unsupported: +# +# - Using with alias +# +sub TryToGetUsing #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $index = $$indexRef; + my $lineNumber = $$lineNumberRef; + + if (lc($tokens->[$index]) ne 'using') + { return undef; }; + + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + my $name; + + while ($tokens->[$index] =~ /^[a-z_\@\.]/i) + { + $name .= $tokens->[$index]; + $index++; + }; + + if ($tokens->[$index] ne ';' || + !defined $name) + { return undef; }; + + $index++; + + + $self->AddUsing( NaturalDocs::SymbolString->FromText($name) ); + + $$indexRef = $index; + $$lineNumberRef = $lineNumber; + + return 1; + }; + + + +# +# Function: TryToGetFunction +# +# Determines if the position is on a function declaration, and if so, generates a topic for it, skips it, and returns true. +# +# Supported Syntaxes: +# +# - Functions +# - Constructors +# - Destructors +# - Properties +# - Indexers +# - Delegates +# - Events +# +sub TryToGetFunction #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $index = $$indexRef; + my $lineNumber = $$lineNumberRef; + + if ($self->TryToSkipAttributes(\$index, \$lineNumber)) + { $self->TryToSkipWhitespace(\$index, \$lineNumber); }; + + my $startIndex = $index; + my $startLine = $lineNumber; + + my @modifiers; + + while ($tokens->[$index] =~ /^[a-z]/i && + exists $functionModifiers{lc($tokens->[$index])} ) + { + push @modifiers, lc($tokens->[$index]); + $index++; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + }; + + my $isDelegate; + my $isEvent; + + if (lc($tokens->[$index]) eq 'delegate') + { + $isDelegate = 1; + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + } + elsif (lc($tokens->[$index]) eq 'event') + { + $isEvent = 1; + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + }; + + my $returnType = $self->TryToGetType(\$index, \$lineNumber); + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + my $name; + my $lastNameWord; + + while ($tokens->[$index] =~ /^[a-z\_\@\.\~\<]/i) + { + $name .= $tokens->[$index]; + + # Ugly hack, but what else is new? For explicit generic interface definitions, such as: + # IDObjectType System.Collections.Generic.IEnumerator<IDObjectType>.Current + + if ($tokens->[$index] eq '<') + { + do + { + $index++; + $name .= $tokens->[$index]; + } + while ($index < @$tokens && $tokens->[$index] ne '>'); + } + + $lastNameWord = $tokens->[$index]; + $index++; + }; + + if (!defined $name) + { + # Constructors and destructors don't have return types. It's possible their names were mistaken for the return type. + if (defined $returnType) + { + $name = $returnType; + $returnType = undef; + + $name =~ /([a-z0-9_]+)$/i; + $lastNameWord = $1; + } + else + { return undef; }; + }; + + # If there's no return type, make sure it's a constructor or destructor. + if (!defined $returnType) + { + my @identifiers = NaturalDocs::SymbolString->IdentifiersOf( $self->CurrentScope() ); + + if ($lastNameWord ne $identifiers[-1]) + { return undef; }; + }; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + + # Skip the brackets on indexers. + if ($tokens->[$index] eq '[' && lc($lastNameWord) eq 'this') + { + # This should jump the brackets completely. + $self->GenericSkip(\$index, \$lineNumber); + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + $name .= '[]'; + }; + + + # Properties, indexers, events with braces + + if ($tokens->[$index] eq '{') + { + my $prototype = $self->CreateString($startIndex, $index); + + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + my ($aWord, $bWord, $hasA, $hasB); + + if ($isEvent) + { + $aWord = 'add'; + $bWord = 'remove'; + } + else + { + $aWord = 'get'; + $bWord = 'set'; + }; + + while ($index < scalar @$tokens) + { + if ($self->TryToSkipAttributes(\$index, \$lineNumber)) + { $self->TryToSkipWhitespace(\$index, \$lineNumber); }; + + if (lc($tokens->[$index]) eq $aWord) + { $hasA = 1; } + elsif (lc($tokens->[$index]) eq $bWord) + { $hasB = 1; } + elsif ($tokens->[$index] eq '}') + { + $index++; + last; + }; + + $self->SkipRestOfStatement(\$index, \$lineNumber); + $self->TryToSkipWhitespace(\$index, \$lineNumber); + }; + + if ($hasA && $hasB) + { $prototype .= ' { ' . $aWord . ', ' . $bWord . ' }'; } + elsif ($hasA) + { $prototype .= ' { ' . $aWord . ' }'; } + elsif ($hasB) + { $prototype .= ' { ' . $bWord . ' }'; }; + + $prototype = $self->NormalizePrototype($prototype); + + my $topicType = ( $isEvent ? ::TOPIC_EVENT() : ::TOPIC_PROPERTY() ); + + $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New($topicType, $name, + $self->CurrentScope(), $self->CurrentUsing(), + $prototype, + undef, undef, $startLine)); + } + + + # Functions, constructors, destructors, delegates. + + elsif ($tokens->[$index] eq '(') + { + # This should jump the parenthesis completely. + $self->GenericSkip(\$index, \$lineNumber); + + my $topicType = ( $isDelegate ? ::TOPIC_DELEGATE() : ::TOPIC_FUNCTION() ); + my $prototype = $self->NormalizePrototype( $self->CreateString($startIndex, $index) ); + + $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New($topicType, $name, + $self->CurrentScope(), $self->CurrentUsing(), + $prototype, + undef, undef, $startLine)); + + $self->SkipRestOfStatement(\$index, \$lineNumber); + } + + + # Events without braces + + elsif ($isEvent && $tokens->[$index] eq ';') + { + my $prototype = $self->NormalizePrototype( $self->CreateString($startIndex, $index) ); + + $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New(::TOPIC_EVENT(), $name, + $self->CurrentScope(), $self->CurrentUsing(), + $prototype, + undef, undef, $startLine)); + $index++; + } + + else + { return undef; }; + + + # We succeeded if we got this far. + + $$indexRef = $index; + $$lineNumberRef = $lineNumber; + + return 1; + }; + + +# +# Function: TryToGetOverloadedOperator +# +# Determines if the position is on an operator overload declaration, and if so, generates a topic for it, skips it, and returns true. +# +sub TryToGetOverloadedOperator #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $index = $$indexRef; + my $lineNumber = $$lineNumberRef; + + if ($self->TryToSkipAttributes(\$index, \$lineNumber)) + { $self->TryToSkipWhitespace(\$index, \$lineNumber); }; + + my $startIndex = $index; + my $startLine = $lineNumber; + + my @modifiers; + + while ($tokens->[$index] =~ /^[a-z]/i && + exists $functionModifiers{lc($tokens->[$index])} ) + { + push @modifiers, lc($tokens->[$index]); + $index++; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + }; + + + my $name; + + + # Casting operators. + + if (lc($tokens->[$index]) eq 'implicit' || lc($tokens->[$index]) eq 'explicit') + { + $index++; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + if (lc($tokens->[$index]) ne 'operator') + { return undef; }; + + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + $name = $self->TryToGetType(\$index, \$lineNumber); + + if (!defined $name) + { return undef; }; + } + + + # Symbol operators. + + else + { + if (!$self->TryToGetType(\$index, \$lineNumber)) + { return undef; }; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + if (lc($tokens->[$index]) ne 'operator') + { return undef; }; + + $index++; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + if (lc($tokens->[$index]) eq 'true' || lc($tokens->[$index]) eq 'false') + { + $name = $tokens->[$index]; + $index++; + } + else + { + while ($tokens->[$index] =~ /^[\+\-\!\~\*\/\%\&\|\^\<\>\=]$/) + { + $name .= $tokens->[$index]; + $index++; + }; + }; + }; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + if ($tokens->[$index] ne '(') + { return undef; }; + + # This should skip the parenthesis completely. + $self->GenericSkip(\$index, \$lineNumber); + + my $prototype = $self->NormalizePrototype( $self->CreateString($startIndex, $index) ); + + $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New(::TOPIC_FUNCTION(), 'operator ' . $name, + $self->CurrentScope(), $self->CurrentUsing(), + $prototype, + undef, undef, $startLine)); + + $self->SkipRestOfStatement(\$index, \$lineNumber); + + + # We succeeded if we got this far. + + $$indexRef = $index; + $$lineNumberRef = $lineNumber; + + return 1; + }; + + +# +# Function: TryToGetVariable +# +# Determines if the position is on a variable declaration statement, and if so, generates a topic for each variable, skips the +# statement, and returns true. +# +# Supported Syntaxes: +# +# - Variables +# - Constants +# +sub TryToGetVariable #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $index = $$indexRef; + my $lineNumber = $$lineNumberRef; + + if ($self->TryToSkipAttributes(\$index, \$lineNumber)) + { $self->TryToSkipWhitespace(\$index, \$lineNumber); }; + + my $startIndex = $index; + my $startLine = $lineNumber; + + my @modifiers; + + while ($tokens->[$index] =~ /^[a-z]/i && + exists $variableModifiers{lc($tokens->[$index])} ) + { + push @modifiers, lc($tokens->[$index]); + $index++; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + }; + + my $type; + if (lc($tokens->[$index]) eq 'const') + { + $type = ::TOPIC_CONSTANT(); + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + } + else + { + $type = ::TOPIC_VARIABLE(); + }; + + if (!$self->TryToGetType(\$index, \$lineNumber)) + { return undef; }; + + my $endTypeIndex = $index; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + my @names; + + for (;;) + { + my $name; + + while ($tokens->[$index] =~ /^[a-z\@\_]/i) + { + $name .= $tokens->[$index]; + $index++; + }; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + if ($tokens->[$index] eq '=') + { + do + { + $self->GenericSkip(\$index, \$lineNumber); + } + while ($tokens->[$index] ne ',' && $tokens->[$index] ne ';'); + }; + + push @names, $name; + + if ($tokens->[$index] eq ';') + { + $index++; + last; + } + elsif ($tokens->[$index] eq ',') + { + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + } + else + { return undef; }; + }; + + + # We succeeded if we got this far. + + my $prototypePrefix = $self->CreateString($startIndex, $endTypeIndex); + + foreach my $name (@names) + { + my $prototype = $self->NormalizePrototype( $prototypePrefix . ' ' . $name ); + + $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New($type, $name, + $self->CurrentScope(), $self->CurrentUsing(), + $prototype, + undef, undef, $startLine)); + }; + + $$indexRef = $index; + $$lineNumberRef = $lineNumber; + + return 1; + }; + + +# +# Function: TryToGetEnum +# +# Determines if the position is on an enum declaration statement, and if so, generates a topic for it. +# +# Supported Syntaxes: +# +# - Enums +# - Enums with declared types +# +# Unsupported: +# +# - Documenting the members automatically +# +sub TryToGetEnum #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $index = $$indexRef; + my $lineNumber = $$lineNumberRef; + + if ($self->TryToSkipAttributes(\$index, \$lineNumber)) + { $self->TryToSkipWhitespace(\$index, \$lineNumber); }; + + my $startIndex = $index; + my $startLine = $lineNumber; + + my @modifiers; + + while ($tokens->[$index] =~ /^[a-z]/i && + exists $variableModifiers{lc($tokens->[$index])} ) + { + push @modifiers, lc($tokens->[$index]); + $index++; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + }; + + if (lc($tokens->[$index]) ne 'enum') + { return undef; } + + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + my $name; + + while ($tokens->[$index] =~ /^[a-z\@\_]/i) + { + $name .= $tokens->[$index]; + $index++; + }; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + if ($tokens->[$index] eq ':') + { + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + if (!exists $enumTypes{ lc($tokens->[$index]) }) + { return undef; } + + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + } + + if ($tokens->[$index] ne '{') + { return undef; } + + # We succeeded if we got this far. + + my $prototype = $self->CreateString($startIndex, $index); + $prototype = $self->NormalizePrototype( $prototype ); + + $self->SkipRestOfStatement(\$index, \$lineNumber); + + $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New(::TOPIC_ENUMERATION(), $name, + $self->CurrentScope(), $self->CurrentUsing(), + $prototype, + undef, undef, $startLine)); + + $$indexRef = $index; + $$lineNumberRef = $lineNumber; + + return 1; + }; + + +# +# Function: TryToGetType +# +# Determines if the position is on a type identifier, and if so, skips it and returns it as a string. This function does _not_ allow +# modifiers. You must take care of those beforehand. +# +sub TryToGetType #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $name; + my $index = $$indexRef; + my $lineNumber = $$lineNumberRef; + + while ($tokens->[$index] =~ /^[a-z\@\.\_]/i) + { + if (exists $impossibleTypeWords{ lc($tokens->[$index]) } && $name !~ /\@$/) + { return undef; }; + + $name .= $tokens->[$index]; + $index++; + }; + + if (!defined $name) + { return undef; }; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + if ($tokens->[$index] eq '?') + { + $name .= '?'; + $index++; + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + } + + if ($tokens->[$index] eq '<') + { + # XXX: This is half-assed. + $name .= '<'; + $index++; + + while ($index < scalar @$tokens && $tokens->[$index] ne '>') + { + $name .= $tokens->[$index]; + $index++; + } + + if ($index < scalar @$tokens) + { + $name .= '>'; + $index++; + } + else + { return undef; } + + $self->TryToSkipWhitespace(\$index, \$lineNumber); + } + + while ($tokens->[$index] eq '[') + { + $name .= '['; + $index++; + + while ($tokens->[$index] eq ',') + { + $name .= ','; + $index++; + }; + + if ($tokens->[$index] eq ']') + { + $name .= ']'; + $index++; + } + else + { return undef; } + }; + + $$indexRef = $index; + $$lineNumberRef = $lineNumber; + + return $name; + }; + + + +############################################################################### +# Group: Low Level Parsing Functions + + +# +# Function: GenericSkip +# +# Advances the position one place through general code. +# +# - If the position is on a string, it will skip it completely. +# - If the position is on an opening symbol, it will skip until the past the closing symbol. +# - If the position is on whitespace (including comments and preprocessing directives), it will skip it completely. +# - Otherwise it skips one token. +# +# Parameters: +# +# indexRef - A reference to the current index. +# lineNumberRef - A reference to the current line number. +# +sub GenericSkip #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + # We can ignore the scope stack because we're just skipping everything without parsing, and we need recursion anyway. + if ($tokens->[$$indexRef] eq '{') + { + $$indexRef++; + $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}'); + } + elsif ($tokens->[$$indexRef] eq '(') + { + $$indexRef++; + $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ')'); + } + elsif ($tokens->[$$indexRef] eq '[') + { + $$indexRef++; + $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ']'); + } + + elsif ($self->TryToSkipWhitespace($indexRef, $lineNumberRef) || + $self->TryToSkipString($indexRef, $lineNumberRef)) + { + } + + else + { $$indexRef++; }; + }; + + +# +# Function: GenericSkipUntilAfter +# +# Advances the position via <GenericSkip()> until a specific token is reached and passed. +# +sub GenericSkipUntilAfter #(indexRef, lineNumberRef, token) + { + my ($self, $indexRef, $lineNumberRef, $token) = @_; + my $tokens = $self->Tokens(); + + while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] ne $token) + { $self->GenericSkip($indexRef, $lineNumberRef); }; + + if ($tokens->[$$indexRef] eq "\n") + { $$lineNumberRef++; }; + $$indexRef++; + }; + + +# +# Function: SkipRestOfStatement +# +# Advances the position via <GenericSkip()> until after the end of the current statement, which is defined as a semicolon or +# a brace group. Of course, either of those appearing inside parenthesis, a nested brace group, etc. don't count. +# +sub SkipRestOfStatement #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + while ($$indexRef < scalar @$tokens && + $tokens->[$$indexRef] ne ';' && + $tokens->[$$indexRef] ne '{') + { + $self->GenericSkip($indexRef, $lineNumberRef); + }; + + if ($tokens->[$$indexRef] eq ';') + { $$indexRef++; } + elsif ($tokens->[$$indexRef] eq '{') + { $self->GenericSkip($indexRef, $lineNumberRef); }; + }; + + +# +# Function: TryToSkipString +# If the current position is on a string delimiter, skip past the string and return true. +# +# Parameters: +# +# indexRef - A reference to the index of the position to start at. +# lineNumberRef - A reference to the line number of the position. +# +# Returns: +# +# Whether the position was at a string. +# +# Syntax Support: +# +# - Supports quotes, apostrophes, and at-quotes. +# +sub TryToSkipString #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + # The three string delimiters. All three are Perl variables when preceded by a dollar sign. + if ($self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '\'') || + $self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '"') ) + { + return 1; + } + elsif ($tokens->[$$indexRef] eq '@' && $tokens->[$$indexRef+1] eq '"') + { + $$indexRef += 2; + + # We need to do at-strings manually because backslash characters are accepted as regular characters, and two consecutive + # quotes are accepted as well. + + while ($$indexRef < scalar @$tokens && !($tokens->[$$indexRef] eq '"' && $tokens->[$$indexRef+1] ne '"') ) + { + if ($tokens->[$$indexRef] eq '"') + { + # This is safe because the while condition will only let through quote pairs. + $$indexRef += 2; + } + elsif ($tokens->[$$indexRef] eq "\n") + { + $$indexRef++; + $$lineNumberRef++; + } + else + { + $$indexRef++; + }; + }; + + # Skip the closing quote. + if ($$indexRef < scalar @$tokens) + { $$indexRef++; }; + + return 1; + } + else + { return undef; }; + }; + + +# +# Function: TryToSkipAttributes +# If the current position is on an attribute section, skip it and return true. Skips multiple attribute sections if they appear +# consecutively. +# +sub TryToSkipAttributes #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $success; + + while ($tokens->[$$indexRef] eq '[') + { + $success = 1; + $$indexRef++; + $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ']'); + $self->TryToSkipWhitespace($indexRef, $lineNumberRef); + }; + + return $success; + }; + + +# +# Function: TryToSkipWhitespace +# If the current position is on a whitespace token, a line break token, a comment, or a preprocessing directive, it skips them +# and returns true. If there are a number of these in a row, it skips them all. +# +sub TryToSkipWhitespace #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $result; + + while ($$indexRef < scalar @$tokens) + { + if ($tokens->[$$indexRef] =~ /^[ \t]/) + { + $$indexRef++; + $result = 1; + } + elsif ($tokens->[$$indexRef] eq "\n") + { + $$indexRef++; + $$lineNumberRef++; + $result = 1; + } + elsif ($self->TryToSkipComment($indexRef, $lineNumberRef) || + $self->TryToSkipPreprocessingDirective($indexRef, $lineNumberRef)) + { + $result = 1; + } + else + { last; }; + }; + + return $result; + }; + + +# +# Function: TryToSkipComment +# If the current position is on a comment, skip past it and return true. +# +sub TryToSkipComment #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + + return ( $self->TryToSkipLineComment($indexRef, $lineNumberRef) || + $self->TryToSkipMultilineComment($indexRef, $lineNumberRef) ); + }; + + +# +# Function: TryToSkipLineComment +# If the current position is on a line comment symbol, skip past it and return true. +# +sub TryToSkipLineComment #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + if ($tokens->[$$indexRef] eq '/' && $tokens->[$$indexRef+1] eq '/') + { + $self->SkipRestOfLine($indexRef, $lineNumberRef); + return 1; + } + else + { return undef; }; + }; + + +# +# Function: TryToSkipMultilineComment +# If the current position is on an opening comment symbol, skip past it and return true. +# +sub TryToSkipMultilineComment #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + if ($tokens->[$$indexRef] eq '/' && $tokens->[$$indexRef+1] eq '*') + { + $self->SkipUntilAfter($indexRef, $lineNumberRef, '*', '/'); + return 1; + } + else + { return undef; }; + }; + + +# +# Function: TryToSkipPreprocessingDirective +# If the current position is on a preprocessing directive, skip past it and return true. +# +sub TryToSkipPreprocessingDirective #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + if ($tokens->[$$indexRef] eq '#' && $self->IsFirstLineToken($$indexRef)) + { + $self->SkipRestOfLine($indexRef, $lineNumberRef); + return 1; + } + else + { return undef; }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Languages/PLSQL.pm b/docs/tool/Modules/NaturalDocs/Languages/PLSQL.pm new file mode 100644 index 00000000..4c3df998 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Languages/PLSQL.pm @@ -0,0 +1,319 @@ +############################################################################### +# +# Class: NaturalDocs::Languages::PLSQL +# +############################################################################### +# +# A subclass to handle the language variations of PL/SQL. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Languages::PLSQL; + +use base 'NaturalDocs::Languages::Simple'; + + +# +# Function: OnPrototypeEnd +# +# Microsoft's SQL specifies parameters as shown below. +# +# > CREATE PROCEDURE Test @as int, @foo int AS ... +# +# Having a parameter @is or @as is perfectly valid even though those words are also used to end the prototype. We need to +# ignore text-based enders preceded by an at sign. Also note that it does not have parenthesis for parameter lists. We need to +# skip all commas if the prototype doesn't have parenthesis but does have @ characters. +# +# Identifiers such as function names may contain the characters $, #, and _, so if "as" or "is" appears directly after one of them +# we need to ignore the ender there as well. +# +# > FUNCTION Something_is_something ... +# +# Parameters: +# +# type - The <TopicType> of the prototype. +# prototypeRef - A reference to the prototype so far, minus the ender in dispute. +# ender - The ender symbol. +# +# Returns: +# +# ENDER_ACCEPT - The ender is accepted and the prototype is finished. +# ENDER_IGNORE - The ender is rejected and parsing should continue. Note that the prototype will be rejected as a whole +# if all enders are ignored before reaching the end of the code. +# ENDER_ACCEPT_AND_CONTINUE - The ender is accepted so the prototype may stand as is. However, the prototype might +# also continue on so continue parsing. If there is no accepted ender between here and +# the end of the code this version will be accepted instead. +# ENDER_REVERT_TO_ACCEPTED - The expedition from ENDER_ACCEPT_AND_CONTINUE failed. Use the last accepted +# version and end parsing. +# +sub OnPrototypeEnd #(type, prototypeRef, ender) + { + my ($self, $type, $prototypeRef, $ender) = @_; + + # _ should be handled already. + if ($ender =~ /^[a-z]+$/i && substr($$prototypeRef, -1) =~ /^[\@\$\#]$/) + { return ::ENDER_IGNORE(); } + + elsif ($type eq ::TOPIC_FUNCTION() && $ender eq ',') + { + if ($$prototypeRef =~ /^[^\(]*\@/) + { return ::ENDER_IGNORE(); } + else + { return ::ENDER_ACCEPT(); }; + } + + else + { return ::ENDER_ACCEPT(); }; + }; + + +# +# Function: ParsePrototype +# +# Overridden to handle Microsoft's parenthesisless version. Otherwise just throws to the parent. +# +# Parameters: +# +# type - The <TopicType>. +# prototype - The text prototype. +# +# Returns: +# +# A <NaturalDocs::Languages::Prototype> object. +# +sub ParsePrototype #(type, prototype) + { + my ($self, $type, $prototype) = @_; + + my $noParenthesisParameters = ($type eq ::TOPIC_FUNCTION() && $prototype =~ /^[^\(]*\@/); + + if ($prototype !~ /\(.*[^ ].*\)/ && !$noParenthesisParameters) + { return $self->SUPER::ParsePrototype($type, $prototype); }; + + + + my ($beforeParameters, $afterParameters, $isAfterParameters); + + if ($noParenthesisParameters) + { + ($beforeParameters, $prototype) = split(/\@/, $prototype, 2); + $prototype = '@' . $prototype; + }; + + my @tokens = $prototype =~ /([^\(\)\[\]\{\}\<\>\'\"\,]+|.)/g; + + my $parameter; + my @parameterLines; + + my @symbolStack; + + foreach my $token (@tokens) + { + if ($isAfterParameters) + { $afterParameters .= $token; } + + elsif ($symbolStack[-1] eq '\'' || $symbolStack[-1] eq '"') + { + if ($noParenthesisParameters || $symbolStack[0] eq '(') + { $parameter .= $token; } + else + { $beforeParameters .= $token; }; + + if ($token eq $symbolStack[-1]) + { pop @symbolStack; }; + } + + elsif ($token =~ /^[\(\[\{\<\'\"]$/) + { + if ($noParenthesisParameters || $symbolStack[0] eq '(') + { $parameter .= $token; } + else + { $beforeParameters .= $token; }; + + push @symbolStack, $token; + } + + elsif ( ($token eq ')' && $symbolStack[-1] eq '(') || + ($token eq ']' && $symbolStack[-1] eq '[') || + ($token eq '}' && $symbolStack[-1] eq '{') || + ($token eq '>' && $symbolStack[-1] eq '<') ) + { + if (!$noParenthesisParameters && $token eq ')' && scalar @symbolStack == 1 && $symbolStack[0] eq '(') + { + $afterParameters .= $token; + $isAfterParameters = 1; + } + else + { $parameter .= $token; }; + + pop @symbolStack; + } + + elsif ($token eq ',') + { + if (!scalar @symbolStack) + { + if ($noParenthesisParameters) + { + push @parameterLines, $parameter . $token; + $parameter = undef; + } + else + { + $beforeParameters .= $token; + }; + } + else + { + if (scalar @symbolStack == 1 && $symbolStack[0] eq '(' && !$noParenthesisParameters) + { + push @parameterLines, $parameter . $token; + $parameter = undef; + } + else + { + $parameter .= $token; + }; + }; + } + + else + { + if ($noParenthesisParameters || $symbolStack[0] eq '(') + { $parameter .= $token; } + else + { $beforeParameters .= $token; }; + }; + }; + + push @parameterLines, $parameter; + + foreach my $item (\$beforeParameters, \$afterParameters) + { + $$item =~ s/^ //; + $$item =~ s/ $//; + } + + my $prototypeObject = NaturalDocs::Languages::Prototype->New($beforeParameters, $afterParameters); + + + # Parse the actual parameters. + + foreach my $parameterLine (@parameterLines) + { + $prototypeObject->AddParameter( $self->ParseParameterLine($parameterLine) ); + }; + + return $prototypeObject; + }; + + +# +# Function: ParseParameterLine +# +# Parses a prototype parameter line and returns it as a <NaturalDocs::Languages::Prototype::Parameter> object. +# +sub ParseParameterLine #(line) + { + my ($self, $line) = @_; + + $line =~ s/^ //; + $line =~ s/ $//; + + my @tokens = $line =~ /([^\(\)\[\]\{\}\<\>\'\"\:\=\ ]+|\:\=|.)/g; + + my ($name, $type, $defaultValue, $defaultValuePrefix, $inType, $inDefaultValue); + + + my @symbolStack; + + foreach my $token (@tokens) + { + if ($inDefaultValue) + { $defaultValue .= $token; } + + elsif ($symbolStack[-1] eq '\'' || $symbolStack[-1] eq '"') + { + if ($inType) + { $type .= $token; } + else + { $name .= $token; }; + + if ($token eq $symbolStack[-1]) + { pop @symbolStack; }; + } + + elsif ($token =~ /^[\(\[\{\<\'\"]$/) + { + if ($inType) + { $type .= $token; } + else + { $name .= $token; }; + + push @symbolStack, $token; + } + + elsif ( ($token eq ')' && $symbolStack[-1] eq '(') || + ($token eq ']' && $symbolStack[-1] eq '[') || + ($token eq '}' && $symbolStack[-1] eq '{') || + ($token eq '>' && $symbolStack[-1] eq '<') ) + { + if ($inType) + { $type .= $token; } + else + { $name .= $token; }; + + pop @symbolStack; + } + + elsif ($token eq ' ') + { + if ($inType) + { $type .= $token; } + elsif (!scalar @symbolStack) + { $inType = 1; } + else + { $name .= $token; }; + } + + elsif ($token eq ':=' || $token eq '=') + { + if (!scalar @symbolStack) + { + $defaultValuePrefix = $token; + $inDefaultValue = 1; + } + elsif ($inType) + { $type .= $token; } + else + { $name .= $token; }; + } + + else + { + if ($inType) + { $type .= $token; } + else + { $name .= $token; }; + }; + }; + + foreach my $part (\$type, \$defaultValue) + { + $$part =~ s/ $//; + }; + + return NaturalDocs::Languages::Prototype::Parameter->New($type, undef, $name, undef, $defaultValue, $defaultValuePrefix); + }; + + +sub TypeBeforeParameter + { return 0; }; + +1; diff --git a/docs/tool/Modules/NaturalDocs/Languages/Pascal.pm b/docs/tool/Modules/NaturalDocs/Languages/Pascal.pm new file mode 100644 index 00000000..e0242dec --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Languages/Pascal.pm @@ -0,0 +1,143 @@ +############################################################################### +# +# Class: NaturalDocs::Languages::Pascal +# +############################################################################### +# +# A subclass to handle the language variations of Pascal and Delphi. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Languages::Pascal; + +use base 'NaturalDocs::Languages::Simple'; + + +# +# hash: prototypeDirectives +# +# An existence hash of all the directives that can appear after a function prototype and will be included. The keys are the all +# lowercase keywords. +# +my %prototypeDirectives = ( 'overload' => 1, + 'override' => 1, + 'virtual' => 1, + 'abstract' => 1, + 'reintroduce' => 1, + 'export' => 1, + 'public' => 1, + 'interrupt' => 1, + 'register' => 1, + 'pascal' => 1, + 'cdecl' => 1, + 'stdcall' => 1, + 'popstack' => 1, + 'saveregisters' => 1, + 'inline' => 1, + 'safecall' => 1 ); + +# +# hash: longPrototypeDirectives +# +# An existence hash of all the directives with parameters that can appear after a function prototype and will be included. The +# keys are the all lowercase keywords. +# +my %longPrototypeDirectives = ( 'alias' => 1, + 'external' => 1 ); + +# +# bool: checkingForDirectives +# +# Set after the first function semicolon, which means we're in directives mode. +# +my $checkingForDirectives; + + +# +# Function: OnCode +# +# Just overridden to reset <checkingForDirectives>. +# +sub OnCode #(...) + { + my ($self, @parameters) = @_; + + $checkingForDirectives = 0; + + return $self->SUPER::OnCode(@parameters); + }; + + +# +# Function: OnPrototypeEnd +# +# Pascal's syntax has directives after the prototype that should be included. +# +# > function MyFunction ( param1: type ); virtual; abstract; +# +# Parameters: +# +# type - The <TopicType> of the prototype. +# prototypeRef - A reference to the prototype so far, minus the ender in dispute. +# ender - The ender symbol. +# +# Returns: +# +# ENDER_ACCEPT - The ender is accepted and the prototype is finished. +# ENDER_IGNORE - The ender is rejected and parsing should continue. Note that the prototype will be rejected as a whole +# if all enders are ignored before reaching the end of the code. +# ENDER_ACCEPT_AND_CONTINUE - The ender is accepted so the prototype may stand as is. However, the prototype might +# also continue on so continue parsing. If there is no accepted ender between here and +# the end of the code this version will be accepted instead. +# ENDER_REVERT_TO_ACCEPTED - The expedition from ENDER_ACCEPT_AND_CONTINUE failed. Use the last accepted +# version and end parsing. +# +sub OnPrototypeEnd #(type, prototypeRef, ender) + { + my ($self, $type, $prototypeRef, $ender) = @_; + + if ($type eq ::TOPIC_FUNCTION() && $ender eq ';') + { + if (!$checkingForDirectives) + { + $checkingForDirectives = 1; + return ::ENDER_ACCEPT_AND_CONTINUE(); + } + elsif ($$prototypeRef =~ /;[ \t]*([a-z]+)([^;]*)$/i) + { + my ($lastDirective, $extra) = (lc($1), $2); + + if (exists $prototypeDirectives{$lastDirective} && $extra =~ /^[ \t]*$/) + { return ::ENDER_ACCEPT_AND_CONTINUE(); } + elsif (exists $longPrototypeDirectives{$lastDirective}) + { return ::ENDER_ACCEPT_AND_CONTINUE(); } + else + { return ::ENDER_REVERT_TO_ACCEPTED(); }; + } + else + { return ::ENDER_REVERT_TO_ACCEPTED(); }; + } + else + { return ::ENDER_ACCEPT(); }; + }; + + +sub ParseParameterLine #(...) + { + my ($self, @params) = @_; + return $self->SUPER::ParsePascalParameterLine(@params); + }; + +sub TypeBeforeParameter + { + return 0; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Languages/Perl.pm b/docs/tool/Modules/NaturalDocs/Languages/Perl.pm new file mode 100644 index 00000000..8817aadc --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Languages/Perl.pm @@ -0,0 +1,1370 @@ +############################################################################### +# +# Class: NaturalDocs::Languages::Perl +# +############################################################################### +# +# A subclass to handle the language variations of Perl. +# +# +# Topic: Language Support +# +# Supported: +# +# - Packages +# - Inheritance via "use base" and "@ISA =". +# - Functions +# - Variables +# +# Not supported yet: +# +# - Constants +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Languages::Perl; + +use base 'NaturalDocs::Languages::Advanced'; + + +# +# array: hereDocTerminators +# An array of active Here Doc terminators, or an empty array if not active. Each entry is an arrayref of tokens. The entries +# must appear in the order they must appear in the source. +# +my @hereDocTerminators; + + + +############################################################################### +# Group: Interface Functions + + +# +# Function: PackageSeparator +# Returns the package separator symbol. +# +sub PackageSeparator + { return '::'; }; + +# +# Function: EnumValues +# Returns the <EnumValuesType> that describes how the language handles enums. +# +sub EnumValues + { return ::ENUM_GLOBAL(); }; + + +# +# Function: ParseFile +# +# Parses the passed source file, sending comments acceptable for documentation to <NaturalDocs::Parser->OnComment()>. +# +# Parameters: +# +# sourceFile - The name of the source file to parse. +# topicList - A reference to the list of <NaturalDocs::Parser::ParsedTopics> being built by the file. +# +# Returns: +# +# The array ( autoTopics, scopeRecord ). +# +# autoTopics - An arrayref of automatically generated topics from the file, or undef if none. +# scopeRecord - An arrayref of <NaturalDocs::Languages::Advanced::ScopeChanges>, or undef if none. +# +sub ParseFile #(sourceFile, topicsList) + { + my ($self, $sourceFile, $topicsList) = @_; + + @hereDocTerminators = ( ); + + # The regular block comment symbols are undef because they're all potentially JavaDoc comments. PreprocessFile() will + # handle translating things like =begin naturaldocs and =begin javadoc to =begin nd. + $self->ParseForCommentsAndTokens($sourceFile, [ '#' ], undef, [ '##' ], [ '=begin nd', '=end nd' ]); + + my $tokens = $self->Tokens(); + my $index = 0; + my $lineNumber = 1; + + while ($index < scalar @$tokens) + { + if ($self->TryToSkipWhitespace(\$index, \$lineNumber) || + $self->TryToGetPackage(\$index, \$lineNumber) || + $self->TryToGetBase(\$index, \$lineNumber) || + $self->TryToGetFunction(\$index, \$lineNumber) || + $self->TryToGetVariable(\$index, \$lineNumber) ) + { + # The functions above will handle everything. + } + + elsif ($tokens->[$index] eq '{') + { + $self->StartScope('}', $lineNumber, undef); + $index++; + } + + elsif ($tokens->[$index] eq '}') + { + if ($self->ClosingScopeSymbol() eq '}') + { $self->EndScope($lineNumber); }; + + $index++; + } + + elsif (lc($tokens->[$index]) eq 'eval') + { + # We want to skip the token in this case instead of letting it fall to SkipRestOfStatement. This allows evals with braces + # to be treated like normal floating braces. + $index++; + } + + else + { + $self->SkipRestOfStatement(\$index, \$lineNumber); + }; + }; + + + # Don't need to keep these around. + $self->ClearTokens(); + + return ( $self->AutoTopics(), $self->ScopeRecord() ); + }; + + +# +# Function: PreprocessFile +# +# Overridden to support "=begin nd" and similar. +# +# - "=begin [nd|naturaldocs|natural docs|jd|javadoc|java doc]" all translate to "=begin nd". +# - "=[nd|naturaldocs|natural docs]" also translate to "=begin nd". +# - "=end [nd|naturaldocs|natural docs|jd|javadoc]" all translate to "=end nd". +# - "=cut" from a ND block translates into "=end nd", but the next line will be altered to begin with "(NDPODBREAK)". This is +# so if there is POD leading into ND which ends with a cut, the parser can still end the original POD because the end ND line +# would have been removed. Remember, <NaturalDocs::Languages::Advanced->ParseForCommentsAndTokens()> removes +# Natural Docs-worthy comments to save parsing time. +# - "=pod begin nd" and "=pod end nd" are supported for compatibility with ND 1.32 and earlier, even though the syntax is a +# mistake. +# - It also supports the wrong plural forms, so naturaldoc/natural doc/javadocs/java docs will work. +# +sub PreprocessFile #(lines) + { + my ($self, $lines) = @_; + + my $inNDPOD = 0; + my $mustBreakPOD = 0; + + for (my $i = 0; $i < scalar @$lines; $i++) + { + if ($lines->[$i] =~ /^\=(?:(?:pod[ \t]+)?begin[ \t]+)?(?:nd|natural[ \t]*docs?|jd|java[ \t]*docs?)[ \t]*$/i) + { + $lines->[$i] = '=begin nd'; + $inNDPOD = 1; + $mustBreakPOD = 0; + } + elsif ($lines->[$i] =~ /^\=(?:pod[ \t]+)end[ \t]+(?:nd|natural[ \t]*docs?|jd|javadocs?)[ \t]*$/i) + { + $lines->[$i] = '=end nd'; + $inNDPOD = 0; + $mustBreakPOD = 0; + } + elsif ($lines->[$i] =~ /^\=cut[ \t]*$/i) + { + if ($inNDPOD) + { + $lines->[$i] = '=end nd'; + $inNDPOD = 0; + $mustBreakPOD = 1; + }; + } + elsif ($mustBreakPOD) + { + $lines->[$i] = '(NDPODBREAK)' . $lines->[$i]; + $mustBreakPOD = 0; + }; + }; + }; + + + +############################################################################### +# Group: Statement Parsing Functions +# All functions here assume that the current position is at the beginning of a statement. +# +# Note for developers: I am well aware that the code in these functions do not check if we're past the end of the tokens as +# often as it should. We're making use of the fact that Perl will always return undef in these cases to keep the code simpler. + + +# +# Function: TryToGetPackage +# +# Determines whether the position is at a package declaration statement, and if so, generates a topic for it, skips it, and +# returns true. +# +sub TryToGetPackage #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + if (lc($tokens->[$$indexRef]) eq 'package') + { + my $index = $$indexRef + 1; + my $lineNumber = $$lineNumberRef; + + if (!$self->TryToSkipWhitespace(\$index, \$lineNumber)) + { return undef; }; + + my $name; + + while ($tokens->[$index] =~ /^[a-z_\:]/i) + { + $name .= $tokens->[$index]; + $index++; + }; + + if (!defined $name) + { return undef; }; + + my $autoTopic = NaturalDocs::Parser::ParsedTopic->New(::TOPIC_CLASS(), $name, + undef, undef, + undef, + undef, undef, $$lineNumberRef); + $self->AddAutoTopic($autoTopic); + + NaturalDocs::Parser->OnClass($autoTopic->Symbol()); + + $self->SetPackage($autoTopic->Symbol(), $$lineNumberRef); + + $$indexRef = $index; + $$lineNumberRef = $lineNumber; + $self->SkipRestOfStatement($indexRef, $lineNumberRef); + + return 1; + }; + + return undef; + }; + + +# +# Function: TryToGetBase +# +# Determines whether the position is at a package base declaration statement, and if so, calls +# <NaturalDocs::Parser->OnClassParent()>. +# +# Supported Syntaxes: +# +# > use base [list of strings] +# > @ISA = [list of strings] +# > @[package]::ISA = [list of strings] +# > our @ISA = [list of strings] +# +sub TryToGetBase #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my ($index, $lineNumber, $class, $parents); + + if (lc($tokens->[$$indexRef]) eq 'use') + { + $index = $$indexRef + 1; + $lineNumber = $$lineNumberRef; + + if (!$self->TryToSkipWhitespace(\$index, \$lineNumber) || + lc($tokens->[$index]) ne 'base') + { return undef; } + + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + $parents = $self->TryToGetListOfStrings(\$index, \$lineNumber); + } + + else + { + $index = $$indexRef; + $lineNumber = $$lineNumberRef; + + if (lc($tokens->[$index]) eq 'our') + { + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + }; + + if ($tokens->[$index] eq '@') + { + $index++; + + while ($index < scalar @$tokens) + { + if ($tokens->[$index] eq 'ISA') + { + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + if ($tokens->[$index] eq '=') + { + $index++; + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + $parents = $self->TryToGetListOfStrings(\$index, \$lineNumber); + } + else + { last; }; + } + + # If token isn't ISA... + elsif ($tokens->[$index] =~ /^[a-z0-9_:]/i) + { + $class .= $tokens->[$index]; + $index++; + } + else + { last; }; + }; + }; + }; + + if (defined $parents) + { + if (defined $class) + { + $class =~ s/::$//; + my @classIdentifiers = split(/::/, $class); + $class = NaturalDocs::SymbolString->Join(@classIdentifiers); + } + else + { $class = $self->CurrentScope(); }; + + foreach my $parent (@$parents) + { + my @parentIdentifiers = split(/::/, $parent); + my $parentSymbol = NaturalDocs::SymbolString->Join(@parentIdentifiers); + + NaturalDocs::Parser->OnClassParent($class, $parentSymbol, undef, undef, ::RESOLVE_ABSOLUTE()); + }; + + $$indexRef = $index; + $$lineNumberRef = $lineNumber; + $self->SkipRestOfStatement($indexRef, $lineNumberRef); + + return 1; + } + else + { return undef; }; + }; + + +# +# Function: TryToGetFunction +# +# Determines whether the position is at a function declaration statement, and if so, generates a topic for it, skips it, and +# returns true. +# +sub TryToGetFunction #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + if ( lc($tokens->[$$indexRef]) eq 'sub') + { + my $prototypeStart = $$indexRef; + my $prototypeStartLine = $$lineNumberRef; + my $prototypeEnd = $$indexRef + 1; + my $prototypeEndLine = $$lineNumberRef; + + if ( !$self->TryToSkipWhitespace(\$prototypeEnd, \$prototypeEndLine) || + $tokens->[$prototypeEnd] !~ /^[a-z_]/i ) + { return undef; }; + + my $name = $tokens->[$prototypeEnd]; + $prototypeEnd++; + + # We parsed 'sub [name]'. Now keep going until we find a semicolon or a brace. + + for (;;) + { + if ($prototypeEnd >= scalar @$tokens) + { return undef; } + + # End if we find a semicolon, since it means we found a predeclaration rather than an actual function. + elsif ($tokens->[$prototypeEnd] eq ';') + { return undef; } + + elsif ($tokens->[$prototypeEnd] eq '{') + { + # Found it! + + my $prototype = $self->NormalizePrototype( $self->CreateString($prototypeStart, $prototypeEnd) ); + + $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New(::TOPIC_FUNCTION(), $name, + $self->CurrentScope(), undef, + $prototype, + undef, undef, $prototypeStartLine)); + + $$indexRef = $prototypeEnd; + $$lineNumberRef = $prototypeEndLine; + + $self->SkipRestOfStatement($indexRef, $lineNumberRef); + + return 1; + } + + else + { $self->GenericSkip(\$prototypeEnd, \$prototypeEndLine, 0, 1); }; + }; + } + else + { return undef; }; + }; + + +# +# Function: TryToGetVariable +# +# Determines if the position is at a variable declaration statement, and if so, generates a topic for it, skips it, and returns +# true. +# +# Supported Syntaxes: +# +# - Supports variables declared with "my", "our", and "local". +# - Supports multiple declarations in one statement, such as "my ($x, $y);". +# - Supports types and attributes. +# +sub TryToGetVariable #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $firstToken = lc( $tokens->[$$indexRef] ); + + if ($firstToken eq 'my' || $firstToken eq 'our' || $firstToken eq 'local') + { + my $prototypeStart = $$indexRef; + my $prototypeStartLine = $$lineNumberRef; + my $prototypeEnd = $$indexRef + 1; + my $prototypeEndLine = $$lineNumberRef; + + $self->TryToSkipWhitespace(\$prototypeEnd, \$prototypeEndLine); + + + # Get the type if present. + + my $type; + + if ($tokens->[$prototypeEnd] =~ /^[a-z\:]/i) + { + do + { + $type .= $tokens->[$prototypeEnd]; + $prototypeEnd++; + } + while ($tokens->[$prototypeEnd] =~ /^[a-z\:]/i); + + if (!$self->TryToSkipWhitespace(\$prototypeEnd, \$prototypeEndLine)) + { return undef; }; + }; + + + # Get the name, or possibly names. + + if ($tokens->[$prototypeEnd] eq '(') + { + # If there's multiple variables, we'll need to build a custom prototype for each one. $firstToken already has the + # declaring word. We're going to store each name in @names, and we're going to use $prototypeStart and + # $prototypeEnd to capture any properties appearing after the list. + + my $name; + my @names; + my $hasComma = 0; + + $prototypeStart = $prototypeEnd + 1; + $prototypeStartLine = $prototypeEndLine; + + for (;;) + { + $self->TryToSkipWhitespace(\$prototypeStart, \$prototypeStartLine); + + $name = $self->TryToGetVariableName(\$prototypeStart, \$prototypeStartLine); + + if (!defined $name) + { return undef; }; + + push @names, $name; + + $self->TryToSkipWhitespace(\$prototypeStart, \$prototypeStartLine); + + # We can have multiple commas in a row. We can also have trailing commas. However, the parenthesis must + # not start with a comma or be empty, hence this logic does not appear earlier. + while ($tokens->[$prototypeStart] eq ',') + { + $prototypeStart++; + $self->TryToSkipWhitespace(\$prototypeStart, \$prototypeStartLine); + + $hasComma = 1; + } + + if ($tokens->[$prototypeStart] eq ')') + { + $prototypeStart++; + last; + } + elsif (!$hasComma) + { return undef; }; + }; + + + # Now find the end of the prototype. + + $prototypeEnd = $prototypeStart; + $prototypeEndLine = $prototypeStartLine; + + while ($prototypeEnd < scalar @$tokens && + $tokens->[$prototypeEnd] !~ /^[\;\=]/) + { + $prototypeEnd++; + }; + + + my $prototypePrefix = $firstToken . ' '; + if (defined $type) + { $prototypePrefix .= $type . ' '; }; + + my $prototypeSuffix = ' ' . $self->CreateString($prototypeStart, $prototypeEnd); + + foreach $name (@names) + { + my $prototype = $self->NormalizePrototype( $prototypePrefix . $name . $prototypeSuffix ); + + $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New(::TOPIC_VARIABLE(), $name, + $self->CurrentScope(), undef, + $prototype, + undef, undef, $prototypeStartLine)); + }; + + $self->SkipRestOfStatement(\$prototypeEnd, \$prototypeEndLine); + + $$indexRef = $prototypeEnd; + $$lineNumberRef = $prototypeEndLine; + } + + else # no parenthesis + { + my $name = $self->TryToGetVariableName(\$prototypeEnd, \$prototypeEndLine); + + if (!defined $name) + { return undef; }; + + while ($prototypeEnd < scalar @$tokens && + $tokens->[$prototypeEnd] !~ /^[\;\=]/) + { + $prototypeEnd++; + }; + + my $prototype = $self->NormalizePrototype( $self->CreateString($prototypeStart, $prototypeEnd) ); + + $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New(::TOPIC_VARIABLE(), $name, + $self->CurrentScope(), undef, + $prototype, + undef, undef, $prototypeStartLine)); + + $self->SkipRestOfStatement(\$prototypeEnd, \$prototypeEndLine); + + $$indexRef = $prototypeEnd; + $$lineNumberRef = $prototypeEndLine; + }; + + return 1; + } + else + { return undef; }; + }; + + +# +# Function: TryToGetVariableName +# +# Determines if the position is at a variable name, and if so, skips it and returns the name. +# +sub TryToGetVariableName #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $name; + + if ($tokens->[$$indexRef] =~ /^[\$\@\%\*]/) + { + $name .= $tokens->[$$indexRef]; + $$indexRef++; + + $self->TryToSkipWhitespace($indexRef, $lineNumberRef); + + if ($tokens->[$$indexRef] =~ /^[a-z_]/i) + { + $name .= $tokens->[$$indexRef]; + $$indexRef++; + } + else + { return undef; }; + }; + + return $name; + }; + + +# +# Function: TryToGetListOfStrings +# +# Attempts to retrieve a list of strings from the current position. Returns an arrayref of them if any are found, or undef if none. +# It stops the moment it reaches a non-string, so "string1, variable, string2" will only return string1. +# +# Supported Syntaxes: +# +# - Supports parenthesis. +# - Supports all string forms supported by <TryToSkipString()>. +# - Supports qw() string arrays. +# +sub TryToGetListOfStrings #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $parenthesis = 0; + my $strings; + + while ($$indexRef < scalar @$tokens) + { + # We'll tolerate parenthesis. + if ($tokens->[$$indexRef] eq '(') + { + $$indexRef++; + $parenthesis++; + } + elsif ($tokens->[$$indexRef] eq ')') + { + if ($parenthesis == 0) + { last; }; + + $$indexRef++; + $parenthesis--; + } + elsif ($tokens->[$$indexRef] eq ',') + { + $$indexRef++; + } + else + { + my ($startContent, $endContent); + my $symbolIndex = $$indexRef; + + if ($self->TryToSkipString($indexRef, $lineNumberRef, \$startContent, \$endContent)) + { + my $content = $self->CreateString($startContent, $endContent); + + if (!defined $strings) + { $strings = [ ]; }; + + if (lc($tokens->[$symbolIndex]) eq 'qw') + { + $content =~ tr/ \t\n/ /s; + $content =~ s/^ //; + + my @qwStrings = split(/ /, $content); + + push @$strings, @qwStrings; + } + else + { + push @$strings, $content; + }; + } + else + { last; }; + }; + + $self->TryToSkipWhitespace($indexRef, $lineNumberRef); + }; + + return $strings; + }; + + +############################################################################### +# Group: Low Level Parsing Functions + + +# +# Function: GenericSkip +# +# Advances the position one place through general code. +# +# - If the position is on a comment or string, it will skip it completely. +# - If the position is on an opening symbol, it will skip until the past the closing symbol. +# - If the position is on a regexp or quote-like operator, it will skip it completely. +# - If the position is on a backslash, it will skip it and the following token. +# - If the position is on whitespace (including comments), it will skip it completely. +# - Otherwise it skips one token. +# +# Parameters: +# +# indexRef - A reference to the current index. +# lineNumberRef - A reference to the current line number. +# noRegExps - If set, does not test for regular expressions. +# +sub GenericSkip #(indexRef, lineNumberRef, noRegExps) + { + my ($self, $indexRef, $lineNumberRef, $noRegExps, $allowStringedClosingParens) = @_; + my $tokens = $self->Tokens(); + + if ($tokens->[$$indexRef] eq "\\" && $$indexRef + 1 < scalar @$tokens && $tokens->[$$indexRef+1] ne "\n") + { $$indexRef += 2; } + + # Note that we don't want to count backslashed ()[]{} since they could be in regexps. Also, ()[] are valid variable names + # when preceded by a string. + + # We can ignore the scope stack because we're just skipping everything without parsing, and we need recursion anyway. + elsif ($tokens->[$$indexRef] eq '{' && !$self->IsBackslashed($$indexRef)) + { + $$indexRef++; + $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}', $noRegExps, $allowStringedClosingParens); + } + elsif ($tokens->[$$indexRef] eq '(' && !$self->IsBackslashed($$indexRef) && !$self->IsStringed($$indexRef)) + { + # Temporarily allow stringed closing parenthesis if it looks like we're in an anonymous function declaration with Perl's + # cheap version of prototypes, such as "my $_declare = sub($) {}". + my $tempAllowStringedClosingParens = $allowStringedClosingParens; + if (!$allowStringedClosingParens) + { + my $tempIndex = $$indexRef - 1; + if ($tempIndex >= 0 && $tokens->[$tempIndex] =~ /^[ \t]/) + { $tempIndex--; } + if ($tempIndex >= 0 && $tokens->[$tempIndex] eq 'sub') + { $tempAllowStringedClosingParens = 1; } + } + + $$indexRef++; + + do + { $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ')', $noRegExps, $tempAllowStringedClosingParens); } + while ($$indexRef < scalar @$tokens && $self->IsStringed($$indexRef - 1) && !$tempAllowStringedClosingParens); + } + elsif ($tokens->[$$indexRef] eq '[' && !$self->IsBackslashed($$indexRef) && !$self->IsStringed($$indexRef)) + { + $$indexRef++; + + do + { $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ']', $noRegExps, $allowStringedClosingParens); } + while ($$indexRef < scalar @$tokens && $self->IsStringed($$indexRef - 1)); + } + + elsif ($self->TryToSkipWhitespace($indexRef, $lineNumberRef) || + $self->TryToSkipString($indexRef, $lineNumberRef) || + $self->TryToSkipHereDocDeclaration($indexRef, $lineNumberRef) || + (!$noRegExps && $self->TryToSkipRegexp($indexRef, $lineNumberRef) ) ) + { + } + + else + { $$indexRef++; }; + }; + + +# +# Function: GenericSkipUntilAfter +# +# Advances the position via <GenericSkip()> until a specific token is reached and passed. +# +sub GenericSkipUntilAfter #(indexRef, lineNumberRef, token, noRegExps, allowStringedClosingParens) + { + my ($self, $indexRef, $lineNumberRef, $token, $noRegExps, $allowStringedClosingParens) = @_; + my $tokens = $self->Tokens(); + + while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] ne $token) + { $self->GenericSkip($indexRef, $lineNumberRef, $noRegExps, $allowStringedClosingParens); }; + + if ($tokens->[$$indexRef] eq "\n") + { $$lineNumberRef++; }; + $$indexRef++; + }; + + +# +# Function: GenericRegexpSkip +# +# Advances the position one place through regexp code. +# +# - If the position is on an opening symbol, it will skip until the past the closing symbol. +# - If the position is on a backslash, it will skip it and the following token. +# - If the position is on whitespace (not including comments), it will skip it completely. +# - Otherwise it skips one token. +# +# Also differs from <GenericSkip()> in that the parenthesis in $( and $) do count against the scope, where they wouldn't +# normally. +# +# Parameters: +# +# indexRef - A reference to the current index. +# lineNumberRef - A reference to the current line number. +# inBrackets - Whether we're in brackets or not. If true, we don't care about matching braces and parenthesis. +# +sub GenericRegexpSkip #(indexRef, lineNumberRef, inBrackets) + { + my ($self, $indexRef, $lineNumberRef, $inBrackets) = @_; + my $tokens = $self->Tokens(); + + if ($tokens->[$$indexRef] eq "\\" && $$indexRef + 1 < scalar @$tokens && $tokens->[$$indexRef+1] ne "\n") + { $$indexRef += 2; } + + # We can ignore the scope stack because we're just skipping everything without parsing, and we need recursion anyway. + elsif ($tokens->[$$indexRef] eq '{' && !$self->IsBackslashed($$indexRef) && !$inBrackets) + { + $$indexRef++; + $self->GenericRegexpSkipUntilAfter($indexRef, $lineNumberRef, '}'); + } + elsif ($tokens->[$$indexRef] eq '(' && !$self->IsBackslashed($$indexRef) && !$inBrackets) + { + $$indexRef++; + $self->GenericRegexpSkipUntilAfter($indexRef, $lineNumberRef, ')'); + } + elsif ($tokens->[$$indexRef] eq '[' && !$self->IsBackslashed($$indexRef) && !$self->IsStringed($$indexRef)) + { + $$indexRef++; + + do + { $self->GenericRegexpSkipUntilAfter($indexRef, $lineNumberRef, ']'); } + while ($$indexRef < scalar @$tokens && $self->IsStringed($$indexRef - 1)); + } + + elsif ($tokens->[$$indexRef] eq "\n") + { + $$lineNumberRef++; + $$indexRef++; + } + + else + { $$indexRef++; }; + }; + + +# +# Function: GenericRegexpSkipUntilAfter +# +# Advances the position via <GenericRegexpSkip()> until a specific token is reached and passed. +# +sub GenericRegexpSkipUntilAfter #(indexRef, lineNumberRef, token) + { + my ($self, $indexRef, $lineNumberRef, $token) = @_; + my $tokens = $self->Tokens(); + + my $inBrackets = ( $token eq ']' ); + + while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] ne $token) + { $self->GenericRegexpSkip($indexRef, $lineNumberRef, $inBrackets); }; + + if ($tokens->[$$indexRef] eq "\n") + { $$lineNumberRef++; }; + $$indexRef++; + }; + + +# +# Function: SkipRestOfStatement +# +# Advances the position via <GenericSkip()> until after the end of the current statement, which is defined as a semicolon or +# a brace group. Of course, either of those appearing inside parenthesis, a nested brace group, etc. don't count. +# +sub SkipRestOfStatement #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + while ($$indexRef < scalar @$tokens && + $tokens->[$$indexRef] ne ';' && + !($tokens->[$$indexRef] eq '{' && !$self->IsStringed($$indexRef)) ) + { + $self->GenericSkip($indexRef, $lineNumberRef); + }; + + if ($tokens->[$$indexRef] eq ';') + { $$indexRef++; } + elsif ($tokens->[$$indexRef] eq '{') + { $self->GenericSkip($indexRef, $lineNumberRef); }; + }; + + +# +# Function: TryToSkipWhitespace +# +# If the current position is on whitespace it skips them and returns true. If there are a number of these in a row, it skips them +# all. +# +# Supported Syntax: +# +# - Whitespace +# - Line break +# - All comment forms supported by <TryToSkipComment()> +# - Here Doc content +# +sub TryToSkipWhitespace #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $result; + + while ($$indexRef < scalar @$tokens) + { + if ($self->TryToSkipHereDocContent($indexRef, $lineNumberRef) || + $self->TryToSkipComment($indexRef, $lineNumberRef)) + { + $result = 1; + } + elsif ($tokens->[$$indexRef] =~ /^[ \t]/) + { + $$indexRef++; + $result = 1; + } + elsif ($tokens->[$$indexRef] eq "\n") + { + $$indexRef++; + $$lineNumberRef++; + $result = 1; + } + else + { last; }; + }; + + return $result; + }; + + +# +# Function: TryToSkipComment +# If the current position is on a comment, skip past it and return true. +# +sub TryToSkipComment #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + + return ( $self->TryToSkipLineComment($indexRef, $lineNumberRef) || + $self->TryToSkipPODComment($indexRef, $lineNumberRef) ); + }; + + +# +# Function: TryToSkipLineComment +# If the current position is on a line comment symbol, skip past it and return true. +# +sub TryToSkipLineComment #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + # Note that $#var is not a comment. + if ($tokens->[$$indexRef] eq '#' && !$self->IsStringed($$indexRef)) + { + $self->SkipRestOfLine($indexRef, $lineNumberRef); + return 1; + } + else + { return undef; }; + }; + + +# +# Function: TryToSkipPODComment +# If the current position is on a POD comment symbol, skip past it and return true. +# +sub TryToSkipPODComment #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + # Note that whitespace is not allowed before the equals sign. It must directly start a line. + if ($tokens->[$$indexRef] eq '=' && + ( $$indexRef == 0 || $tokens->[$$indexRef - 1] eq "\n" ) && + $tokens->[$$indexRef + 1] =~ /^[a-z]/i ) + { + # Skip until =cut or (NDPODBREAK). Note that it's theoretically possible for =cut to appear without a prior POD directive. + + do + { + if ($tokens->[$$indexRef] eq '=' && lc( $tokens->[$$indexRef + 1] ) eq 'cut') + { + $self->SkipRestOfLine($indexRef, $lineNumberRef); + last; + } + elsif ($tokens->[$$indexRef] eq '(' && $$indexRef + 2 < scalar @$tokens && + $tokens->[$$indexRef+1] eq 'NDPODBREAK' && $tokens->[$$indexRef+2] eq ')') + { + $$indexRef += 3; + last; + } + else + { + $self->SkipRestOfLine($indexRef, $lineNumberRef); + }; + } + while ($$indexRef < scalar @$tokens); + + return 1; + } + + # It's also possible that (NDPODBREAK) will appear without any opening pod statement because "=begin nd" and "=cut" will + # still result in one. We need to pick off the stray (NDPODBREAK). + elsif ($tokens->[$$indexRef] eq '(' && $$indexRef + 2 < scalar @$tokens && + $tokens->[$$indexRef+1] eq 'NDPODBREAK' && $tokens->[$$indexRef+2] eq ')') + { + $$indexRef += 3; + return 1; + } + + else + { return undef; }; + }; + + +# +# Function: TryToSkipString +# If the current position is on a string delimiter, skip past the string and return true. +# +# Parameters: +# +# indexRef - A reference to the index of the position to start at. +# lineNumberRef - A reference to the line number of the position. +# startContentIndexRef - A reference to the variable in which to store the index of the first content token. May be undef. +# endContentIndexRef - A reference to the variable in which to store the index of the end of the content, which is one past +# the last content token. may be undef. +# +# Returns: +# +# Whether the position was at a string. The index, line number, and content index variabls will only be changed if true. +# +# Syntax Support: +# +# - Supports quotes, apostrophes, backticks, q(), qq(), qx(), and qw(). +# - All symbols are supported for the letter forms. +# +sub TryToSkipString #(indexRef, lineNumberRef, startContentIndexRef, endContentIndexRef) + { + my ($self, $indexRef, $lineNumberRef, $startContentIndexRef, $endContentIndexRef) = @_; + my $tokens = $self->Tokens(); + + # The three string delimiters. All three are Perl variables when preceded by a dollar sign. + if (!$self->IsStringed($$indexRef) && + ( $self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '\'', '\'', $startContentIndexRef, $endContentIndexRef) || + $self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '"', '"', $startContentIndexRef, $endContentIndexRef) || + $self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '`', '`', $startContentIndexRef, $endContentIndexRef) ) ) + { + return 1; + } + elsif ($tokens->[$$indexRef] =~ /^(?:q|qq|qx|qw)$/i && + ($$indexRef == 0 || $tokens->[$$indexRef - 1] !~ /^[\$\%\@\*]$/)) + { + $$indexRef++; + + $self->TryToSkipWhitespace($indexRef, $lineNumberRef); + + my $openingSymbol = $tokens->[$$indexRef]; + my $closingSymbol; + + if ($openingSymbol eq '{') + { $closingSymbol = '}'; } + elsif ($openingSymbol eq '(') + { $closingSymbol = ')'; } + elsif ($openingSymbol eq '[') + { $closingSymbol = ']'; } + elsif ($openingSymbol eq '<') + { $closingSymbol = '>'; } + else + { $closingSymbol = $openingSymbol; }; + + $self->SUPER::TryToSkipString($indexRef, $lineNumberRef, $openingSymbol, $closingSymbol, + $startContentIndexRef, $endContentIndexRef); + + return 1; + } + else + { return undef; }; + }; + + +# +# Function: TryToSkipHereDocDeclaration +# +# If the current position is on a Here Doc declaration, add its terminators to <hereDocTerminators> and skip it. +# +# Syntax Support: +# +# - Supports <<EOF +# - Supports << "String" with all string forms supported by <TryToSkipString()>. +# +sub TryToSkipHereDocDeclaration #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $index = $$indexRef; + my $lineNumber = $$lineNumberRef; + + if ($tokens->[$index] eq '<' && $tokens->[$index + 1] eq '<') + { + $index += 2; + my $success; + + # No whitespace allowed with the bare word. + if ($tokens->[$index] =~ /^[a-z0-9_]/i) + { + push @hereDocTerminators, [ $tokens->[$index] ]; + $index++; + $success = 1; + } + else + { + $self->TryToSkipWhitespace(\$index, \$lineNumber); + + my ($contentStart, $contentEnd); + if ($self->TryToSkipString(\$index, \$lineNumber, \$contentStart, \$contentEnd)) + { + push @hereDocTerminators, [ @{$tokens}[$contentStart..$contentEnd - 1] ]; + $success = 1; + }; + }; + + if ($success) + { + $$indexRef = $index; + $$lineNumberRef = $lineNumber; + + return 1; + }; + }; + + return 0; + }; + + +# +# Function: TryToSkipHereDocContent +# +# If the current position is at the beginning of a line and there are entries in <hereDocTerminators>, skips lines until all the +# terminators are exhausted or we reach the end of the file. +# +# Returns: +# +# Whether the position was on Here Doc content. +# +sub TryToSkipHereDocContent #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + # We don't use IsFirstLineToken() because it really needs to be the first line token. Whitespace is not allowed. + if ($$indexRef > 0 && $tokens->[$$indexRef - 1] eq "\n") + { + my $success = (scalar @hereDocTerminators > 0); + + while (scalar @hereDocTerminators && $$indexRef < scalar @$tokens) + { + my $terminatorIndex = 0; + + while ($hereDocTerminators[0]->[$terminatorIndex] eq $tokens->[$$indexRef]) + { + $terminatorIndex++; + $$indexRef++; + }; + + if ($terminatorIndex == scalar @{$hereDocTerminators[0]} && + ($tokens->[$$indexRef] eq "\n" || ($tokens->[$$indexRef] =~ /^[ \t]/ && $tokens->[$$indexRef + 1] eq "\n")) ) + { + shift @hereDocTerminators; + $$indexRef++; + $$lineNumberRef++; + } + else + { $self->SkipRestOfLine($indexRef, $lineNumberRef); }; + }; + + return $success; + } + + else + { return 0; }; + }; + + +# +# Function: TryToSkipRegexp +# If the current position is on a regular expression or a quote-like operator, skip past it and return true. +# +# Syntax Support: +# +# - Supports //, ??, m//, qr//, s///, tr///, and y///. +# - All symbols are supported for the letter forms. +# - ?? is *not* supported because it could cause problems with ?: statements. The generic parser has a good chance of +# successfully stumbling through a regex, whereas the regex code will almost certainly see the rest of the file as part of it. +# +sub TryToSkipRegexp #(indexRef, lineNumberRef) + { + my ($self, $indexRef, $lineNumberRef) = @_; + my $tokens = $self->Tokens(); + + my $isRegexp; + + # If it's a supported character sequence that's not a variable (ex $qr)... + if ($tokens->[$$indexRef] =~ /^(?:m|qr|s|tr|y)$/i && + ($$indexRef == 0 || $tokens->[$$indexRef - 1] !~ /^[\$\%\@\*\-]$/) ) + { $isRegexp = 1; } + + elsif ($tokens->[$$indexRef] eq '/' && !$self->IsStringed($$indexRef)) + { + # This is a bit of a hack. If we find a random slash, it could be a divide operator or a bare regexp. Find the first previous + # non-whitespace token and if it's text, a closing brace, or a string, assume it's a divide operator. (Strings don't make + # much pratical sense there but a regexp would be impossible.) Otherwise assume it's a regexp. + + # We make a special consideration for split() appearing without parenthesis. If the previous token is split and it's not a + # variable, assume it is a regexp even though it fails the above test. + + my $index = $$indexRef - 1; + + while ($index >= 0 && $tokens->[$index] =~ /^(?: |\t|\n)/) + { $index--; }; + + if ($index < 0 || $tokens->[$index] !~ /^[a-zA-Z0-9_\)\]\}\'\"\`]/ || + ($tokens->[$index] =~ /^split|grep$/ && $index > 0 && $tokens->[$index-1] !~ /^[\$\%\@\*]$/) ) + { $isRegexp = 1; }; + }; + + if ($isRegexp) + { + my $operator = lc($tokens->[$$indexRef]); + my $index = $$indexRef; + my $lineNumber = $$lineNumberRef; + + if ($operator =~ /^[\?\/]/) + { $operator = 'm'; } + else + { + $index++; + + # Believe it or not, s#...# is allowed. We can't pass over number signs here. + if ($tokens->[$index] ne '#') + { $self->TryToSkipWhitespace(\$index, \$lineNumber); }; + }; + + if ($tokens->[$index] =~ /^\w/) + { return undef; }; + if ($tokens->[$index] eq '=' && $tokens->[$index+1] eq '>') + { return undef; }; + + my $openingSymbol = $tokens->[$index]; + my $closingSymbol; + + if ($openingSymbol eq '{') + { $closingSymbol = '}'; } + elsif ($openingSymbol eq '(') + { $closingSymbol = ')'; } + elsif ($openingSymbol eq '[') + { $closingSymbol = ']'; } + elsif ($openingSymbol eq '<') + { $closingSymbol = '>'; } + else + { $closingSymbol = $openingSymbol; }; + + $index++; + + $self->GenericRegexpSkipUntilAfter(\$index, \$lineNumber, $closingSymbol); + + $$indexRef = $index; + $$lineNumberRef = $lineNumber; + + if ($operator =~ /^(?:s|tr|y)$/) + { + if ($openingSymbol ne $closingSymbol) + { + $self->TryToSkipWhitespace($indexRef, $lineNumberRef); + + $openingSymbol = $tokens->[$index]; + + if ($openingSymbol eq '{') + { $closingSymbol = '}'; } + elsif ($openingSymbol eq '(') + { $closingSymbol = ')'; } + elsif ($openingSymbol eq '[') + { $closingSymbol = ']'; } + elsif ($openingSymbol eq '<') + { $closingSymbol = '>'; } + else + { $closingSymbol = $openingSymbol; }; + + $$indexRef++; + }; + + if ($operator eq 's') + { + $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, $closingSymbol, 1); + } + else # ($operator eq 'tr' || $operator eq 'y') + { + while ($$indexRef < scalar @$tokens && + ($tokens->[$$indexRef] ne $closingSymbol || $self->IsBackslashed($$indexRef)) ) + { + if ($tokens->[$$indexRef] eq "\n") + { $$lineNumberRef++; }; + $$indexRef++; + }; + + $$indexRef++; + }; + }; + + # We want to skip any letters after the regexp. Otherwise something like tr/a/b/s; could have the trailing s; interpreted + # as another regexp. Whitespace is not allowed between the closing symbol and the letters. + + if ($tokens->[$$indexRef] =~ /^[a-z]/i) + { $$indexRef++; }; + + return 1; + }; + + return undef; + }; + + + +############################################################################### +# Group: Support Functions + + +# +# Function: IsStringed +# +# Returns whether the position is after a string (dollar sign) character. Returns false if it's preceded by two dollar signs so +# "if ($x == $$)" doesn't skip the closing parenthesis as stringed. +# +# Parameters: +# +# index - The index of the postition. +# +sub IsStringed #(index) + { + my ($self, $index) = @_; + my $tokens = $self->Tokens(); + + if ($index > 0 && $tokens->[$index - 1] eq '$' && !($index > 1 && $tokens->[$index - 2] eq '$')) + { return 1; } + else + { return undef; }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Languages/Prototype.pm b/docs/tool/Modules/NaturalDocs/Languages/Prototype.pm new file mode 100644 index 00000000..3a038513 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Languages/Prototype.pm @@ -0,0 +1,92 @@ +############################################################################### +# +# Class: NaturalDocs::Languages::Prototype +# +############################################################################### +# +# A data class for storing parsed prototypes. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +use NaturalDocs::Languages::Prototype::Parameter; + + +package NaturalDocs::Languages::Prototype; + +use NaturalDocs::DefineMembers 'BEFORE_PARAMETERS', 'BeforeParameters()', 'SetBeforeParameters()', + 'AFTER_PARAMETERS', 'AfterParameters()', 'SetAfterParameters()', + 'PARAMETERS', 'Parameters()'; +# Dependency: New(), constant order, no parents. + + +# +# Function: New +# +# Creates and returns a new prototype object. +# +# Parameters: +# +# beforeParameters - The part of the prototype before the parameter list. +# afterParameters - The part of the prototype after the parameter list. +# +# You cannot set the parameters from here. Use <AddParameter()>. +# +sub New #(beforeParameters, afterParameters) + { + my ($package, @params) = @_; + + # Dependency: Constant order, no parents. + + my $object = [ @params ]; + bless $object, $package; + + return $object; + }; + + +# +# Functions: Members +# +# BeforeParameters - Returns the part of the prototype before the parameter list. If there is no parameter list, this will be the +# only thing that returns content. +# SetBeforeParameters - Replaces the part of the prototype before the parameter list. +# AfterParameters - Returns the part of the prototype after the parameter list, if any. +# SetAfterParameters - Replaces the part of the prototype after the parameter list. +# Parameters - Returns the parameter list as an arrayref of <NaturalDocs::Languages::Prototype::Parameters>, or undef if none. +# + +# +# Function: AddParameter +# +# Adds a <NaturalDocs::Languages::Prototype::Parameter> to the list. +# +sub AddParameter #(parameter) + { + my ($self, $parameter) = @_; + + if (!defined $self->[PARAMETERS]) + { $self->[PARAMETERS] = [ ]; }; + + push @{$self->[PARAMETERS]}, $parameter; + }; + + +# +# Function: OnlyBeforeParameters +# +# Returns whether <BeforeParameters()> is the only thing set. +# +sub OnlyBeforeParameters + { + my $self = shift; + return (!defined $self->[PARAMETERS] && !defined $self->[AFTER_PARAMETERS]); + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Languages/Prototype/Parameter.pm b/docs/tool/Modules/NaturalDocs/Languages/Prototype/Parameter.pm new file mode 100644 index 00000000..2d8f6bec --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Languages/Prototype/Parameter.pm @@ -0,0 +1,87 @@ +############################################################################### +# +# Class: NaturalDocs::Languages::Prototype::Parameter +# +############################################################################### +# +# A data class for storing parsed prototype parameters. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Languages::Prototype::Parameter; + +use NaturalDocs::DefineMembers 'TYPE', 'Type()', 'SetType()', + 'TYPE_PREFIX', 'TypePrefix()', 'SetTypePrefix()', + 'NAME', 'Name()', 'SetName()', + 'NAME_PREFIX', 'NamePrefix()', 'SetNamePrefix()', + 'DEFAULT_VALUE', 'DefaultValue()', 'SetDefaultValue()', + 'DEFAULT_VALUE_PREFIX', 'DefaultValuePrefix()', 'SetDefaultValuePrefix()'; +# Dependency: New() depends on the order of these constants and that they don't inherit from another class. + + +# +# Constants: Members +# +# The object is implemented as a blessed arrayref, with the following constants as its indexes. +# +# TYPE - The parameter type, if any. +# TYPE_PREFIX - The parameter type prefix which should be aligned separately, if any. +# NAME - The parameter name. +# NAME_PREFIX - The parameter name prefix which should be aligned separately, if any. +# DEFAULT_VALUE - The default value expression, if any. +# DEFAULT_VALUE_PREFIX - The default value prefix which should be aligned separately, if any. +# + +# +# Function: New +# +# Creates and returns a new prototype object. +# +# Parameters: +# +# type - The parameter type, if any. +# typePrefix - The parameter type prefix which should be aligned separately, if any. +# name - The parameter name. +# namePrefix - The parameter name prefix which should be aligned separately, if any. +# defaultValue - The default value expression, if any. +# defaultValuePrefix - The default value prefix which should be aligned separately, if any. +# +sub New #(type, typePrefix, name, namePrefix, defaultValue, defaultValuePrefix) + { + my ($package, @params) = @_; + + # Dependency: This depends on the order of the parameters being the same as the order of the constants, and that the + # constants don't inherit from another class. + + my $object = [ @params ]; + bless $object, $package; + + return $object; + }; + + +# +# Functions: Members +# +# Type - The parameter type, if any. +# SetType - Replaces the parameter type. +# TypePrefix - The parameter type prefix, which should be aligned separately, if any. +# SetTypePrefix - Replaces the parameter type prefix. +# Name - The parameter name. +# SetName - Replaces the parameter name. +# NamePrefix - The parameter name prefix, which should be aligned separately, if any. +# SetNamePrefix - Replaces the parameter name prefix. +# DefaultValue - The default value expression, if any. +# SetDefaultValue - Replaces the default value expression. +# DefaultValuePrefix - The default value prefix, which should be aligned separately, if any. +# SetDefaultValuePrefix - Replaces the default value prefix. +# + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Languages/Simple.pm b/docs/tool/Modules/NaturalDocs/Languages/Simple.pm new file mode 100644 index 00000000..9d962b1c --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Languages/Simple.pm @@ -0,0 +1,503 @@ +############################################################################### +# +# Class: NaturalDocs::Languages::Simple +# +############################################################################### +# +# A class containing the characteristics of a particular programming language for basic support within Natural Docs. +# Also serves as a base class for languages that break from general conventions, such as not having parameter lists use +# parenthesis and commas. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Languages::Simple; + +use base 'NaturalDocs::Languages::Base'; +use base 'Exporter'; + +our @EXPORT = ( 'ENDER_ACCEPT', 'ENDER_IGNORE', 'ENDER_ACCEPT_AND_CONTINUE', 'ENDER_REVERT_TO_ACCEPTED' ); + + +use NaturalDocs::DefineMembers 'LINE_COMMENT_SYMBOLS', 'LineCommentSymbols()', 'SetLineCommentSymbols() duparrayref', + 'BLOCK_COMMENT_SYMBOLS', 'BlockCommentSymbols()', + 'SetBlockCommentSymbols() duparrayref', + 'PROTOTYPE_ENDERS', + 'LINE_EXTENDER', 'LineExtender()', 'SetLineExtender()', + 'PACKAGE_SEPARATOR', 'PackageSeparator()', + 'PACKAGE_SEPARATOR_WAS_SET', 'PackageSeparatorWasSet()', + 'ENUM_VALUES', 'EnumValues()', + 'ENUM_VALUES_WAS_SET', 'EnumValuesWasSet()'; + +# +# Function: New +# +# Creates and returns a new object. +# +# Parameters: +# +# name - The name of the language. +# +sub New #(name) + { + my ($selfPackage, $name) = @_; + + my $object = $selfPackage->SUPER::New($name); + + $object->[ENUM_VALUES] = ::ENUM_GLOBAL(); + $object->[PACKAGE_SEPARATOR] = '.'; + + return $object; + }; + + +# +# Functions: Members +# +# LineCommentSymbols - Returns an arrayref of symbols that start a line comment, or undef if none. +# SetLineCommentSymbols - Replaces the arrayref of symbols that start a line comment. +# BlockCommentSymbols - Returns an arrayref of start/end symbol pairs that specify a block comment, or undef if none. Pairs +# are specified with two consecutive array entries. +# SetBlockCommentSymbols - Replaces the arrayref of start/end symbol pairs that specify a block comment. Pairs are +# specified with two consecutive array entries. +# LineExtender - Returns the symbol to ignore a line break in languages where line breaks are significant. +# SetLineExtender - Replaces the symbol to ignore a line break in languages where line breaks are significant. +# PackageSeparator - Returns the package separator symbol. +# PackageSeparatorWasSet - Returns whether the package separator symbol was ever changed from the default. +# + +# +# Function: SetPackageSeparator +# Replaces the language's package separator string. +# +sub SetPackageSeparator #(separator) + { + my ($self, $separator) = @_; + $self->[PACKAGE_SEPARATOR] = $separator; + $self->[PACKAGE_SEPARATOR_WAS_SET] = 1; + }; + + +# +# Functions: Members +# +# EnumValues - Returns the <EnumValuesType> that describes how the language handles enums. +# EnumValuesWasSet - Returns whether <EnumValues> was ever changed from the default. + + +# +# Function: SetEnumValues +# Replaces the <EnumValuesType> that describes how the language handles enums. +# +sub SetEnumValues #(EnumValuesType newBehavior) + { + my ($self, $behavior) = @_; + $self->[ENUM_VALUES] = $behavior; + $self->[ENUM_VALUES_WAS_SET] = 1; + }; + + +# +# Function: PrototypeEndersFor +# +# Returns an arrayref of prototype ender symbols for the passed <TopicType>, or undef if none. +# +sub PrototypeEndersFor #(type) + { + my ($self, $type) = @_; + + if (defined $self->[PROTOTYPE_ENDERS]) + { return $self->[PROTOTYPE_ENDERS]->{$type}; } + else + { return undef; }; + }; + + +# +# Function: SetPrototypeEndersFor +# +# Replaces the arrayref of prototype ender symbols for the passed <TopicType>. +# +sub SetPrototypeEndersFor #(type, enders) + { + my ($self, $type, $enders) = @_; + + if (!defined $self->[PROTOTYPE_ENDERS]) + { $self->[PROTOTYPE_ENDERS] = { }; }; + + if (!defined $enders) + { delete $self->[PROTOTYPE_ENDERS]->{$type}; } + else + { + $self->[PROTOTYPE_ENDERS]->{$type} = [ @$enders ]; + }; + }; + + + + +############################################################################### +# Group: Parsing Functions + + +# +# Function: ParseFile +# +# Parses the passed source file, sending comments acceptable for documentation to <NaturalDocs::Parser->OnComment()> +# and all other sections to <OnCode()>. +# +# Parameters: +# +# sourceFile - The <FileName> of the source file to parse. +# topicList - A reference to the list of <NaturalDocs::Parser::ParsedTopics> being built by the file. +# +# Returns: +# +# Since this class cannot automatically document the code or generate a scope record, it always returns ( undef, undef ). +# +sub ParseFile #(sourceFile, topicsList) + { + my ($self, $sourceFile, $topicsList) = @_; + + open(SOURCEFILEHANDLE, '<' . $sourceFile) + or die "Couldn't open input file " . $sourceFile . "\n"; + + my @commentLines; + my @codeLines; + my $lastCommentTopicCount = 0; + + if ($self->Name() eq 'Text File') + { + my $line = <SOURCEFILEHANDLE>; + + # On the very first line, remove a Unicode BOM if present. Information on it available at: + # http://www.unicode.org/faq/utf_bom.html#BOM + $line =~ s/^\xEF\xBB\xBF//; + + while ($line) + { + ::XChomp(\$line); + push @commentLines, $line; + $line = <SOURCEFILEHANDLE>; + }; + + NaturalDocs::Parser->OnComment(\@commentLines, 1); + } + + else + { + my $line = <SOURCEFILEHANDLE>; + my $lineNumber = 1; + + # On the very first line, remove a Unicode BOM if present. Information on it available at: + # http://www.unicode.org/faq/utf_bom.html#BOM + $line =~ s/^\xEF\xBB\xBF//; + + while (defined $line) + { + ::XChomp(\$line); + my $originalLine = $line; + + + # Retrieve single line comments. This leaves $line at the next line. + + if ($self->StripOpeningSymbols(\$line, $self->LineCommentSymbols())) + { + do + { + push @commentLines, $line; + $line = <SOURCEFILEHANDLE>; + + if (!defined $line) + { goto EndDo; }; + + ::XChomp(\$line); + } + while ($self->StripOpeningSymbols(\$line, $self->LineCommentSymbols())); + + EndDo: # I hate Perl sometimes. + } + + + # Retrieve multiline comments. This leaves $line at the next line. + + elsif (my $closingSymbol = $self->StripOpeningBlockSymbols(\$line, $self->BlockCommentSymbols())) + { + # Note that it is possible for a multiline comment to start correctly but not end so. We want those comments to stay in + # the code. For example, look at this prototype with this splint annotation: + # + # int get_array(integer_t id, + # /*@out@*/ array_t array); + # + # The annotation starts correctly but doesn't end so because it is followed by code on the same line. + + my $lineRemainder; + + for (;;) + { + $lineRemainder = $self->StripClosingSymbol(\$line, $closingSymbol); + + push @commentLines, $line; + + # If we found an end comment symbol... + if (defined $lineRemainder) + { last; }; + + $line = <SOURCEFILEHANDLE>; + + if (!defined $line) + { last; }; + + ::XChomp(\$line); + }; + + if ($lineRemainder !~ /^[ \t]*$/) + { + # If there was something past the closing symbol this wasn't an acceptable comment, so move the lines to code. + push @codeLines, @commentLines; + @commentLines = ( ); + }; + + $line = <SOURCEFILEHANDLE>; + } + + + # Otherwise just add it to the code. + + else + { + push @codeLines, $line; + $line = <SOURCEFILEHANDLE>; + }; + + + # If there were comments, send them to Parser->OnComment(). + + if (scalar @commentLines) + { + # First process any code lines before the comment. + if (scalar @codeLines) + { + $self->OnCode(\@codeLines, $lineNumber, $topicsList, $lastCommentTopicCount); + $lineNumber += scalar @codeLines; + @codeLines = ( ); + }; + + $lastCommentTopicCount = NaturalDocs::Parser->OnComment(\@commentLines, $lineNumber); + $lineNumber += scalar @commentLines; + @commentLines = ( ); + }; + + }; # while (defined $line) + + + # Clean up any remaining code. + if (scalar @codeLines) + { + $self->OnCode(\@codeLines, $lineNumber, $topicsList, $lastCommentTopicCount); + @codeLines = ( ); + }; + + }; + + close(SOURCEFILEHANDLE); + + return ( undef, undef ); + }; + + +# +# Function: OnCode +# +# Called whenever a section of code is encountered by the parser. Is used to find the prototype of the last topic created. +# +# Parameters: +# +# codeLines - The source code as an arrayref of lines. +# codeLineNumber - The line number of the first line of code. +# topicList - A reference to the list of <NaturalDocs::Parser::ParsedTopics> being built by the file. +# lastCommentTopicCount - The number of Natural Docs topics that were created by the last comment. +# +sub OnCode #(codeLines, codeLineNumber, topicList, lastCommentTopicCount) + { + my ($self, $codeLines, $codeLineNumber, $topicList, $lastCommentTopicCount) = @_; + + if ($lastCommentTopicCount && defined $self->PrototypeEndersFor($topicList->[-1]->Type())) + { + my $lineIndex = 0; + my $prototype; + + # Skip all blank lines before a prototype. + while ($lineIndex < scalar @$codeLines && $codeLines->[$lineIndex] =~ /^[ \t]*$/) + { $lineIndex++; }; + + my @tokens; + my $tokenIndex = 0; + + my @brackets; + my $enders = $self->PrototypeEndersFor($topicList->[-1]->Type()); + + # Add prototype lines until we reach the end of the prototype or the end of the code lines. + while ($lineIndex < scalar @$codeLines) + { + my $line = $self->RemoveLineExtender($codeLines->[$lineIndex] . "\n"); + + push @tokens, $line =~ /([^\(\)\[\]\{\}\<\>]+|.)/g; + + while ($tokenIndex < scalar @tokens) + { + # If we're not inside brackets, check for ender symbols. + if (!scalar @brackets) + { + my $startingIndex = 0; + my $testPrototype; + + for (;;) + { + my ($enderIndex, $ender) = ::FindFirstSymbol($tokens[$tokenIndex], $enders, $startingIndex); + + if ($enderIndex == -1) + { last; } + else + { + # We do this here so we don't duplicate prototype for every single token. Just the first time an ender symbol + # is found in one. + if (!defined $testPrototype) + { $testPrototype = $prototype; }; + + $testPrototype .= substr($tokens[$tokenIndex], $startingIndex, $enderIndex - $startingIndex); + + my $enderResult; + + # If the ender is all text and the character preceding or following it is as well, ignore it. + if ($ender =~ /^[a-z0-9]+$/i && + ( ($enderIndex > 0 && substr($tokens[$tokenIndex], $enderIndex - 1, 1) =~ /^[a-z0-9_]$/i) || + substr($tokens[$tokenIndex], $enderIndex + length($ender), 1) =~ /^[a-z0-9_]$/i ) ) + { $enderResult = ENDER_IGNORE(); } + else + { $enderResult = $self->OnPrototypeEnd($topicList->[-1]->Type(), \$testPrototype, $ender); } + + if ($enderResult == ENDER_IGNORE()) + { + $testPrototype .= $ender; + $startingIndex = $enderIndex + length($ender); + } + elsif ($enderResult == ENDER_REVERT_TO_ACCEPTED()) + { + return; + } + else # ENDER_ACCEPT || ENDER_ACCEPT_AND_CONTINUE + { + my $titleInPrototype = $topicList->[-1]->Title(); + + # Strip parenthesis so Function(2) and Function(int, int) will still match Function(anything). + $titleInPrototype =~ s/[\t ]*\([^\(]*$//; + + if (index($testPrototype, $titleInPrototype) != -1) + { + $topicList->[-1]->SetPrototype( $self->NormalizePrototype($testPrototype) ); + }; + + if ($enderResult == ENDER_ACCEPT()) + { return; } + else # ENDER_ACCEPT_AND_CONTINUE + { + $testPrototype .= $ender; + $startingIndex = $enderIndex + length($ender); + }; + }; + }; + }; + } + + # If we are inside brackets, check for closing symbols. + elsif ( ($tokens[$tokenIndex] eq ')' && $brackets[-1] eq '(') || + ($tokens[$tokenIndex] eq ']' && $brackets[-1] eq '[') || + ($tokens[$tokenIndex] eq '}' && $brackets[-1] eq '{') || + ($tokens[$tokenIndex] eq '>' && $brackets[-1] eq '<') ) + { + pop @brackets; + }; + + # Check for opening brackets. + if ($tokens[$tokenIndex] =~ /^[\(\[\{\<]$/) + { + push @brackets, $tokens[$tokenIndex]; + }; + + $prototype .= $tokens[$tokenIndex]; + $tokenIndex++; + }; + + $lineIndex++; + }; + + # If we got out of that while loop by running out of lines, there was no prototype. + }; + }; + + +use constant ENDER_ACCEPT => 1; +use constant ENDER_IGNORE => 2; +use constant ENDER_ACCEPT_AND_CONTINUE => 3; +use constant ENDER_REVERT_TO_ACCEPTED => 4; + +# +# Function: OnPrototypeEnd +# +# Called whenever the end of a prototype is found so that there's a chance for derived classes to mark false positives. +# +# Parameters: +# +# type - The <TopicType> of the prototype. +# prototypeRef - A reference to the prototype so far, minus the ender in dispute. +# ender - The ender symbol. +# +# Returns: +# +# ENDER_ACCEPT - The ender is accepted and the prototype is finished. +# ENDER_IGNORE - The ender is rejected and parsing should continue. Note that the prototype will be rejected as a whole +# if all enders are ignored before reaching the end of the code. +# ENDER_ACCEPT_AND_CONTINUE - The ender is accepted so the prototype may stand as is. However, the prototype might +# also continue on so continue parsing. If there is no accepted ender between here and +# the end of the code this version will be accepted instead. +# ENDER_REVERT_TO_ACCEPTED - The expedition from ENDER_ACCEPT_AND_CONTINUE failed. Use the last accepted +# version and end parsing. +# +sub OnPrototypeEnd #(type, prototypeRef, ender) + { + return ENDER_ACCEPT(); + }; + + +# +# Function: RemoveLineExtender +# +# If the passed line has a line extender, returns it without the extender or the line break that follows. If it doesn't, or there are +# no line extenders defined, returns the passed line unchanged. +# +sub RemoveLineExtender #(line) + { + my ($self, $line) = @_; + + if (defined $self->LineExtender()) + { + my $lineExtenderIndex = rindex($line, $self->LineExtender()); + + if ($lineExtenderIndex != -1 && + substr($line, $lineExtenderIndex + length($self->LineExtender())) =~ /^[ \t]*\n$/) + { + $line = substr($line, 0, $lineExtenderIndex) . ' '; + }; + }; + + return $line; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Languages/Tcl.pm b/docs/tool/Modules/NaturalDocs/Languages/Tcl.pm new file mode 100644 index 00000000..bd6b5a0d --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Languages/Tcl.pm @@ -0,0 +1,219 @@ +############################################################################### +# +# Class: NaturalDocs::Languages::Tcl +# +############################################################################### +# +# A subclass to handle the language variations of Tcl. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Languages::Tcl; + +use base 'NaturalDocs::Languages::Simple'; + + +# +# bool: pastFirstBrace +# +# Whether we've past the first brace in a function prototype or not. +# +my $pastFirstBrace; + + +# +# Function: OnCode +# +# This is just overridden to reset <pastFirstBrace>. +# +sub OnCode #(...) + { + my ($self, @params) = @_; + + $pastFirstBrace = 0; + + return $self->SUPER::OnCode(@params); + }; + + +# +# Function: OnPrototypeEnd +# +# Tcl's function syntax is shown below. +# +# > proc [name] { [params] } { [code] } +# +# The opening brace is one of the prototype enders. We need to allow the first opening brace because it contains the +# parameters. +# +# Also, the parameters may have braces within them. I've seen one that used { seconds 20 } as a parameter. +# +# Parameters: +# +# type - The <TopicType> of the prototype. +# prototypeRef - A reference to the prototype so far, minus the ender in dispute. +# ender - The ender symbol. +# +# Returns: +# +# ENDER_ACCEPT - The ender is accepted and the prototype is finished. +# ENDER_IGNORE - The ender is rejected and parsing should continue. Note that the prototype will be rejected as a whole +# if all enders are ignored before reaching the end of the code. +# ENDER_ACCEPT_AND_CONTINUE - The ender is accepted so the prototype may stand as is. However, the prototype might +# also continue on so continue parsing. If there is no accepted ender between here and +# the end of the code this version will be accepted instead. +# ENDER_REVERT_TO_ACCEPTED - The expedition from ENDER_ACCEPT_AND_CONTINUE failed. Use the last accepted +# version and end parsing. +# +sub OnPrototypeEnd #(type, prototypeRef, ender) + { + my ($self, $type, $prototypeRef, $ender) = @_; + + if ($type eq ::TOPIC_FUNCTION() && $ender eq '{' && !$pastFirstBrace) + { + $pastFirstBrace = 1; + return ::ENDER_IGNORE(); + } + else + { return ::ENDER_ACCEPT(); }; + }; + + +# +# Function: ParsePrototype +# +# Parses the prototype and returns it as a <NaturalDocs::Languages::Prototype> object. +# +# Parameters: +# +# type - The <TopicType>. +# prototype - The text prototype. +# +# Returns: +# +# A <NaturalDocs::Languages::Prototype> object. +# +sub ParsePrototype #(type, prototype) + { + my ($self, $type, $prototype) = @_; + + if ($type ne ::TOPIC_FUNCTION()) + { + my $object = NaturalDocs::Languages::Prototype->New($prototype); + return $object; + }; + + + # Parse the parameters out of the prototype. + + my @tokens = $prototype =~ /([^\{\}\ ]+|.)/g; + + my $parameter; + my @parameterLines; + + my $braceLevel = 0; + + my ($beforeParameters, $afterParameters, $finishedParameters); + + foreach my $token (@tokens) + { + if ($finishedParameters) + { $afterParameters .= $token; } + + elsif ($token eq '{') + { + if ($braceLevel == 0) + { $beforeParameters .= $token; } + + else # braceLevel > 0 + { $parameter .= $token; }; + + $braceLevel++; + } + + elsif ($token eq '}') + { + if ($braceLevel == 1) + { + if ($parameter && $parameter ne ' ') + { push @parameterLines, $parameter; }; + + $finishedParameters = 1; + $afterParameters .= $token; + + $braceLevel--; + } + elsif ($braceLevel > 1) + { + $parameter .= $token; + $braceLevel--; + }; + } + + elsif ($token eq ' ') + { + if ($braceLevel == 1) + { + if ($parameter) + { push @parameterLines, $parameter; }; + + $parameter = undef; + } + elsif ($braceLevel > 1) + { + $parameter .= $token; + } + else + { + $beforeParameters .= $token; + }; + } + + else + { + if ($braceLevel > 0) + { $parameter .= $token; } + else + { $beforeParameters .= $token; }; + }; + }; + + foreach my $part (\$beforeParameters, \$afterParameters) + { + $$part =~ s/^ //; + $$part =~ s/ $//; + }; + + my $prototypeObject = NaturalDocs::Languages::Prototype->New($beforeParameters, $afterParameters); + + + # Parse the actual parameters. + + foreach my $parameterLine (@parameterLines) + { + $prototypeObject->AddParameter( $self->ParseParameterLine($parameterLine) ); + }; + + return $prototypeObject; + }; + + +# +# Function: ParseParameterLine +# +# Parses a prototype parameter line and returns it as a <NaturalDocs::Languages::Prototype::Parameter> object. +# +sub ParseParameterLine #(line) + { + my ($self, $line) = @_; + return NaturalDocs::Languages::Prototype::Parameter->New(undef, undef, $line, undef, undef, undef); + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Menu.pm b/docs/tool/Modules/NaturalDocs/Menu.pm new file mode 100644 index 00000000..e8b696a6 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Menu.pm @@ -0,0 +1,3406 @@ +############################################################################### +# +# Package: NaturalDocs::Menu +# +############################################################################### +# +# A package handling the menu's contents and state. +# +# Usage and Dependencies: +# +# - The <Event Handlers> can be called by <NaturalDocs::Project> immediately. +# +# - Prior to initialization, <NaturalDocs::Project> must be initialized, and all files that have been changed must be run +# through <NaturalDocs::Parser->ParseForInformation()>. +# +# - To initialize, call <LoadAndUpdate()>. Afterwards, all other functions are available. Also, <LoadAndUpdate()> will +# call <NaturalDocs::Settings->GenerateDirectoryNames()>. +# +# - To save the changes back to disk, call <Save()>. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use Tie::RefHash; + +use NaturalDocs::Menu::Entry; + +use strict; +use integer; + +package NaturalDocs::Menu; + + +# +# Constants: Constants +# +# MAXFILESINGROUP - The maximum number of file entries that can be present in a group before it becomes a candidate for +# sub-grouping. +# MINFILESINNEWGROUP - The minimum number of file entries that must be present in a group before it will be automatically +# created. This is *not* the number of files that must be in a group before it's deleted. +# +use constant MAXFILESINGROUP => 6; +use constant MINFILESINNEWGROUP => 3; + + +############################################################################### +# Group: Variables + + +# +# bool: hasChanged +# +# Whether the menu changed or not, regardless of why. +# +my $hasChanged; + + +# +# Object: menu +# +# The parsed menu file. Is stored as a <MENU_GROUP> <NaturalDocs::Menu::Entry> object, with the top-level entries being +# stored as the group's content. This is done because it makes a number of functions simpler to implement, plus it allows group +# flags to be set on the top-level. However, it is exposed externally via <Content()> as an arrayref. +# +# This structure will only contain objects for <MENU_FILE>, <MENU_GROUP>, <MENU_TEXT>, <MENU_LINK>, and +# <MENU_INDEX> entries. Other types, such as <MENU_TITLE>, are stored in variables such as <title>. +# +my $menu; + +# +# hash: defaultTitlesChanged +# +# An existence hash of default titles that have changed, since <OnDefaultTitleChange()> will be called before +# <LoadAndUpdate()>. Collects them to be applied later. The keys are the <FileNames>. +# +my %defaultTitlesChanged; + +# +# String: title +# +# The title of the menu. +# +my $title; + +# +# String: subTitle +# +# The sub-title of the menu. +# +my $subTitle; + +# +# String: footer +# +# The footer for the documentation. +# +my $footer; + +# +# String: timestampText +# +# The timestamp for the documentation, stored as the final output text. +# +my $timestampText; + +# +# String: timestampCode +# +# The timestamp for the documentation, storted as the symbolic code. +# +my $timestampCode; + +# +# hash: indexes +# +# An existence hash of all the defined index <TopicTypes> appearing in the menu. +# +my %indexes; + +# +# hash: previousIndexes +# +# An existence hash of all the index <TopicTypes> that appeared in the menu last time. +# +my %previousIndexes; + +# +# hash: bannedIndexes +# +# An existence hash of all the index <TopicTypes> that the user has manually deleted, and thus should not be added back to +# the menu automatically. +# +my %bannedIndexes; + + +############################################################################### +# Group: Files + +# +# File: Menu.txt +# +# The file used to generate the menu. +# +# Format: +# +# The file is plain text. Blank lines can appear anywhere and are ignored. Tags and their content must be completely +# contained on one line with the exception of Group's braces. All values in brackets below are encoded with entity characters. +# +# > # [comment] +# +# The file supports single-line comments via #. They can appear alone on a line or after content. +# +# > Format: [version] +# > Title: [title] +# > SubTitle: [subtitle] +# > Footer: [footer] +# > Timestamp: [timestamp code] +# +# The file format version, menu title, subtitle, footer, and timestamp are specified as above. Each can only be specified once, +# with subsequent ones being ignored. Subtitle is ignored if Title is not present. Format must be the first entry in the file. If +# it's not present, it's assumed the menu is from version 0.95 or earlier, since it was added with 1.0. +# +# The timestamp code is as follows. +# +# m - Single digit month, where applicable. January is "1". +# mm - Always double digit month. January is "01". +# mon - Short month word. January is "Jan". +# month - Long month word. January is "January". +# d - Single digit day, where applicable. 1 is "1". +# dd - Always double digit day. 1 is "01". +# day - Day with text extension. 1 is "1st". +# yy - Double digit year. 2006 is "06". +# yyyy - Four digit year. 2006 is "2006". +# year - Four digit year. 2006 is "2006". +# +# Anything else is left literal in the output. +# +# > File: [title] ([file name]) +# > File: [title] (auto-title, [file name]) +# > File: [title] (no auto-title, [file name]) +# +# Files are specified as above. If there is only one input directory, file names are relative. Otherwise they are absolute. +# If "no auto-title" is specified, the title on the line is used. If not, the title is ignored and the +# default file title is used instead. Auto-title defaults to on, so specifying "auto-title" is for compatibility only. +# +# > Group: [title] +# > Group: [title] { ... } +# +# Groups are specified as above. If no braces are specified, the group's content is everything that follows until the end of the +# file, the next group (braced or unbraced), or the closing brace of a parent group. Group braces are the only things in this +# file that can span multiple lines. +# +# There is no limitations on where the braces can appear. The opening brace can appear after the group tag, on its own line, +# or preceding another tag on a line. Similarly, the closing brace can appear after another tag or on its own line. Being +# bitchy here would just get in the way of quick and dirty editing; the package will clean it up automatically when it writes it +# back to disk. +# +# > Text: [text] +# +# Arbitrary text is specified as above. As with other tags, everything must be contained on the same line. +# +# > Link: [URL] +# > Link: [title] ([URL]) +# +# External links can be specified as above. If the titled form is not used, the URL is used as the title. +# +# > Index: [name] +# > [topic type name] Index: [name] +# +# Indexes are specified as above. The topic type names can be either singular or plural. General is assumed if not specified. +# +# > Don't Index: [topic type name] +# > Don't Index: [topic type name], [topic type name], ... +# +# The option above prevents indexes that exist but are not on the menu from being automatically added. +# +# > Data: [number]([obscured data]) +# +# Used to store non-user editable data. +# +# > Data: 1([obscured: [directory name]///[input directory]]) +# +# When there is more than one directory, these lines store the input directories used in the last run and their names. This +# allows menu files to be shared across machines since the names will be consistent and the directories can be used to convert +# filenames to the local machine's paths. We don't want this user-editable because they may think changing it changes the +# input directories, when it doesn't. Also, changing it without changing all the paths screws up resolving. +# +# > Data: 2([obscured: [directory name]) +# +# When there is only one directory and its name is not "default", this stores the name. +# +# +# Entities: +# +# & - Ampersand. +# &lparen; - Left parenthesis. +# &rparen; - Right parenthesis. +# { - Left brace. +# } - Right brace. +# +# +# Revisions: +# +# 1.4: +# +# - Added Timestamp property. +# - Values are now encoded with entity characters. +# +# 1.3: +# +# - File names are now relative again if there is only one input directory. +# - Data: 2(...) added. +# - Can't use synonyms like "copyright" for "footer" or "sub-title" for "subtitle". +# - "Don't Index" line now requires commas to separate them, whereas it tolerated just spaces before. +# +# 1.16: +# +# - File names are now absolute instead of relative. Prior to 1.16 only one input directory was allowed, so they could be +# relative. +# - Data keywords introduced to store input directories and their names. +# +# 1.14: +# +# - Renamed this file from NaturalDocs_Menu.txt to Menu.txt. +# +# 1.1: +# +# - Added the "don't index" line. +# +# This is also the point where indexes were automatically added and removed, so all index entries from prior revisions +# were manually added and are not guaranteed to contain anything. +# +# 1.0: +# +# - Added the format line. +# - Added the "no auto-title" attribute. +# - Changed the file entry default to auto-title. +# +# This is also the point where auto-organization and better auto-titles were introduced. All groups in prior revisions were +# manually added, with the exception of a top-level Other group where new files were automatically added if there were +# groups defined. +# +# Break in support: +# +# Releases prior to 1.0 are no longer supported. Why? +# +# - They don't have a Format: line, which is required by <NaturalDocs::ConfigFile>, although I could work around this +# if I needed to. +# - No significant number of downloads for pre-1.0 releases. +# - Code simplification. I don't have to bridge the conversion from manual-only menu organization to automatic. +# +# 0.9: +# +# - Added index entries. +# + +# +# File: PreviousMenuState.nd +# +# The file used to store the previous state of the menu so as to detect changes. +# +# +# Format: +# +# > [BINARY_FORMAT] +# > [VersionInt: app version] +# +# First is the standard <BINARY_FORMAT> <VersionInt> header. +# +# > [UInt8: 0 (end group)] +# > [UInt8: MENU_FILE] [UInt8: noAutoTitle] [AString16: title] [AString16: target] +# > [UInt8: MENU_GROUP] [AString16: title] +# > [UInt8: MENU_INDEX] [AString16: title] [AString16: topic type] +# > [UInt8: MENU_LINK] [AString16: title] [AString16: url] +# > [UInt8: MENU_TEXT] [AString16: text] +# +# The first UInt8 of each following line is either zero or one of the <Menu Entry Types>. What follows is contextual. +# +# There are no entries for title, subtitle, or footer. Only the entries present in <menu>. +# +# See Also: +# +# <File Format Conventions> +# +# Dependencies: +# +# - Because the type is represented by a UInt8, the <Menu Entry Types> must all be <= 255. +# +# Revisions: +# +# 1.3: +# +# - The topic type following the <MENU_INDEX> entries were changed from UInt8s to AString16s, since <TopicTypes> +# were switched from integer constants to strings. You can still convert the old to the new via +# <NaturalDocs::Topics->TypeFromLegacy()>. +# +# 1.16: +# +# - The file targets are now absolute. Prior to 1.16, they were relative to the input directory since only one was allowed. +# +# 1.14: +# +# - The file was renamed from NaturalDocs.m to PreviousMenuState.nd and moved into the Data subdirectory. +# +# 1.0: +# +# - The file's format was completely redone. Prior to 1.0, the file was a text file consisting of the app version and a line +# which was a tab-separated list of the indexes present in the menu. * meant the general index. +# +# Break in support: +# +# Pre-1.0 files are no longer supported. There was no significant number of downloads for pre-1.0 releases, and this +# eliminates a separate code path for them. +# +# 0.95: +# +# - Change the file version to match the app version. Prior to 0.95, the version line was 1. Test for "1" instead of "1.0" to +# distinguish. +# +# 0.9: +# +# - The file was added to the project. Prior to 0.9, it didn't exist. +# + + +############################################################################### +# Group: File Functions + +# +# Function: LoadAndUpdate +# +# Loads the menu file from disk and updates it. Will add, remove, rearrange, and remove auto-titling from entries as +# necessary. Will also call <NaturalDocs::Settings->GenerateDirectoryNames()>. +# +sub LoadAndUpdate + { + my ($self) = @_; + + my ($inputDirectoryNames, $relativeFiles, $onlyDirectoryName) = $self->LoadMenuFile(); + + my $errorCount = NaturalDocs::ConfigFile->ErrorCount(); + if ($errorCount) + { + NaturalDocs::ConfigFile->PrintErrorsAndAnnotateFile(); + NaturalDocs::Error->SoftDeath('There ' . ($errorCount == 1 ? 'is an error' : 'are ' . $errorCount . ' errors') + . ' in ' . NaturalDocs::Project->UserConfigFile('Menu.txt')); + }; + + # If the menu has a timestamp and today is a different day than the last time Natural Docs was run, we have to count it as the + # menu changing. + if (defined $timestampCode) + { + my (undef, undef, undef, $currentDay, $currentMonth, $currentYear) = localtime(); + my (undef, undef, undef, $lastDay, $lastMonth, $lastYear) = + localtime( (stat( NaturalDocs::Project->DataFile('PreviousMenuState.nd') ))[9] ); + # This should be okay if the previous menu state file doesn't exist. + + if ($currentDay != $lastDay || $currentMonth != $lastMonth || $currentYear != $lastYear) + { $hasChanged = 1; }; + }; + + + if ($relativeFiles) + { + my $inputDirectory = $self->ResolveRelativeInputDirectories($onlyDirectoryName); + + if ($onlyDirectoryName) + { $inputDirectoryNames = { $inputDirectory => $onlyDirectoryName }; }; + } + else + { $self->ResolveInputDirectories($inputDirectoryNames); }; + + NaturalDocs::Settings->GenerateDirectoryNames($inputDirectoryNames); + + my $filesInMenu = $self->FilesInMenu(); + + my ($previousMenu, $previousIndexes, $previousFiles) = $self->LoadPreviousMenuStateFile(); + + if (defined $previousIndexes) + { %previousIndexes = %$previousIndexes; }; + + if (defined $previousFiles) + { $self->LockUserTitleChanges($previousFiles); }; + + # Don't need these anymore. We keep this level of detail because it may be used more in the future. + $previousMenu = undef; + $previousFiles = undef; + $previousIndexes = undef; + + # We flag title changes instead of actually performing them at this point for two reasons. First, contents of groups are still + # subject to change, which would affect the generated titles. Second, we haven't detected the sort order yet. Changing titles + # could make groups appear unalphabetized when they were beforehand. + + my $updateAllTitles; + + # If the menu file changed, we can't be sure which groups changed and which didn't without a comparison, which really isn't + # worth the trouble. So we regenerate all the titles instead. + if (NaturalDocs::Project->UserConfigFileStatus('Menu.txt') == ::FILE_CHANGED()) + { $updateAllTitles = 1; } + else + { $self->FlagAutoTitleChanges(); }; + + # We add new files before deleting old files so their presence still affects the grouping. If we deleted old files first, it could + # throw off where to place the new ones. + + $self->AutoPlaceNewFiles($filesInMenu); + + my $numberRemoved = $self->RemoveDeadFiles(); + + $self->CheckForTrashedMenu(scalar keys %$filesInMenu, $numberRemoved); + + # Don't ban indexes if they deleted Menu.txt. They may have not deleted PreviousMenuState.nd and we don't want everything + # to be banned because of it. + if (NaturalDocs::Project->UserConfigFileStatus('Menu.txt') != ::FILE_DOESNTEXIST()) + { $self->BanAndUnbanIndexes(); }; + + # Index groups need to be detected before adding new ones. + + $self->DetectIndexGroups(); + + $self->AddAndRemoveIndexes(); + + # We wait until after new files are placed to remove dead groups because a new file may save a group. + + $self->RemoveDeadGroups(); + + $self->CreateDirectorySubGroups(); + + # We detect the sort before regenerating the titles so it doesn't get thrown off by changes. However, we do it after deleting + # dead entries and moving things into subgroups because their removal may bump it into a stronger sort category (i.e. + # SORTFILESANDGROUPS instead of just SORTFILES.) New additions don't factor into the sort. + + $self->DetectOrder($updateAllTitles); + + $self->GenerateAutoFileTitles($updateAllTitles); + + $self->ResortGroups($updateAllTitles); + + + # Don't need this anymore. + %defaultTitlesChanged = ( ); + }; + + +# +# Function: Save +# +# Writes the changes to the menu files. +# +sub Save + { + my ($self) = @_; + + if ($hasChanged) + { + $self->SaveMenuFile(); + $self->SavePreviousMenuStateFile(); + }; + }; + + +############################################################################### +# Group: Information Functions + +# +# Function: HasChanged +# +# Returns whether the menu has changed or not. +# +sub HasChanged + { return $hasChanged; }; + +# +# Function: Content +# +# Returns the parsed menu as an arrayref of <NaturalDocs::Menu::Entry> objects. Do not change the arrayref. +# +# The arrayref will only contain <MENU_FILE>, <MENU_GROUP>, <MENU_INDEX>, <MENU_TEXT>, and <MENU_LINK> +# entries. Entries such as <MENU_TITLE> are parsed out and are only accessible via functions such as <Title()>. +# +sub Content + { return $menu->GroupContent(); }; + +# +# Function: Title +# +# Returns the title of the menu, or undef if none. +# +sub Title + { return $title; }; + +# +# Function: SubTitle +# +# Returns the sub-title of the menu, or undef if none. +# +sub SubTitle + { return $subTitle; }; + +# +# Function: Footer +# +# Returns the footer of the documentation, or undef if none. +# +sub Footer + { return $footer; }; + +# +# Function: TimeStamp +# +# Returns the timestamp text of the documentation, or undef if none. +# +sub TimeStamp + { return $timestampText; }; + +# +# Function: Indexes +# +# Returns an existence hashref of all the index <TopicTypes> appearing in the menu. Do not change the hashref. +# +sub Indexes + { return \%indexes; }; + +# +# Function: PreviousIndexes +# +# Returns an existence hashref of all the index <TopicTypes> that previously appeared in the menu. Do not change the +# hashref. +# +sub PreviousIndexes + { return \%previousIndexes; }; + + +# +# Function: FilesInMenu +# +# Returns a hashref of all the files present in the menu. The keys are the <FileNames>, and the values are references to their +# <NaturalDocs::Menu::Entry> objects. +# +sub FilesInMenu + { + my ($self) = @_; + + my @groupStack = ( $menu ); + my $filesInMenu = { }; + + while (scalar @groupStack) + { + my $currentGroup = pop @groupStack; + my $currentGroupContent = $currentGroup->GroupContent(); + + foreach my $entry (@$currentGroupContent) + { + if ($entry->Type() == ::MENU_GROUP()) + { push @groupStack, $entry; } + elsif ($entry->Type() == ::MENU_FILE()) + { $filesInMenu->{ $entry->Target() } = $entry; }; + }; + }; + + return $filesInMenu; + }; + + + +############################################################################### +# Group: Event Handlers +# +# These functions are called by <NaturalDocs::Project> only. You don't need to worry about calling them. For example, when +# changing the default menu title of a file, you only need to call <NaturalDocs::Project->SetDefaultMenuTitle()>. That function +# will handle calling <OnDefaultTitleChange()>. + + +# +# Function: OnDefaultTitleChange +# +# Called by <NaturalDocs::Project> if the default menu title of a source file has changed. +# +# Parameters: +# +# file - The source <FileName> that had its default menu title changed. +# +sub OnDefaultTitleChange #(file) + { + my ($self, $file) = @_; + + # Collect them for later. We'll deal with them in LoadAndUpdate(). + + $defaultTitlesChanged{$file} = 1; + }; + + + +############################################################################### +# Group: Support Functions + + +# +# Function: LoadMenuFile +# +# Loads and parses the menu file <Menu.txt>. This will fill <menu>, <title>, <subTitle>, <footer>, <timestampText>, +# <timestampCode>, <indexes>, and <bannedIndexes>. If there are any errors in the file, they will be recorded with +# <NaturalDocs::ConfigFile->AddError()>. +# +# Returns: +# +# The array ( inputDirectories, relativeFiles, onlyDirectoryName ) or an empty array if the file doesn't exist. +# +# inputDirectories - A hashref of all the input directories and their names stored in the menu file. The keys are the +# directories and the values are their names. Undef if none. +# relativeFiles - Whether the menu uses relative file names. +# onlyDirectoryName - The name of the input directory if there is only one. +# +sub LoadMenuFile + { + my ($self) = @_; + + my $inputDirectories = { }; + my $relativeFiles; + my $onlyDirectoryName; + + # A stack of Menu::Entry object references as we move through the groups. + my @groupStack; + + $menu = NaturalDocs::Menu::Entry->New(::MENU_GROUP(), undef, undef, undef); + my $currentGroup = $menu; + + # Whether we're currently in a braceless group, since we'd have to find the implied end rather than an explicit one. + my $inBracelessGroup; + + # Whether we're right after a group token, which is the only place there can be an opening brace. + my $afterGroupToken; + + my $version; + + if ($version = NaturalDocs::ConfigFile->Open(NaturalDocs::Project->UserConfigFile('Menu.txt'), 1)) + { + # We don't check if the menu file is from a future version because we can't just throw it out and regenerate it like we can + # with other data files. So we just keep going regardless. Any syntactic differences will show up as errors. + + while (my ($keyword, $value, $comment) = NaturalDocs::ConfigFile->GetLine()) + { + # Check for an opening brace after a group token. This has to be separate from the rest of the code because the flag + # needs to be reset after every line. + if ($afterGroupToken) + { + $afterGroupToken = undef; + + if ($keyword eq '{') + { + $inBracelessGroup = undef; + next; + } + else + { $inBracelessGroup = 1; }; + }; + + + # Now on to the real code. + + if ($keyword eq 'file') + { + my $flags = 0; + + if ($value =~ /^(.+)\(([^\(]+)\)$/) + { + my ($title, $file) = ($1, $2); + + $title =~ s/ +$//; + + # Check for auto-title modifier. + if ($file =~ /^((?:no )?auto-title, ?)(.+)$/i) + { + my $modifier; + ($modifier, $file) = ($1, $2); + + if ($modifier =~ /^no/i) + { $flags |= ::MENU_FILE_NOAUTOTITLE(); }; + }; + + my $entry = NaturalDocs::Menu::Entry->New(::MENU_FILE(), $self->RestoreAmpChars($title), + $self->RestoreAmpChars($file), $flags); + + $currentGroup->PushToGroup($entry); + } + else + { NaturalDocs::ConfigFile->AddError('File lines must be in the format "File: [title] ([location])"'); }; + } + + + elsif ($keyword eq 'group') + { + # End a braceless group, if we were in one. + if ($inBracelessGroup) + { + $currentGroup = pop @groupStack; + $inBracelessGroup = undef; + }; + + my $entry = NaturalDocs::Menu::Entry->New(::MENU_GROUP(), $self->RestoreAmpChars($value), undef, undef); + + $currentGroup->PushToGroup($entry); + + push @groupStack, $currentGroup; + $currentGroup = $entry; + + $afterGroupToken = 1; + } + + + elsif ($keyword eq '{') + { + NaturalDocs::ConfigFile->AddError('Opening braces are only allowed after Group tags.'); + } + + + elsif ($keyword eq '}') + { + # End a braceless group, if we were in one. + if ($inBracelessGroup) + { + $currentGroup = pop @groupStack; + $inBracelessGroup = undef; + }; + + # End a braced group too. + if (scalar @groupStack) + { $currentGroup = pop @groupStack; } + else + { NaturalDocs::ConfigFile->AddError('Unmatched closing brace.'); }; + } + + + elsif ($keyword eq 'title') + { + if (!defined $title) + { $title = $self->RestoreAmpChars($value); } + else + { NaturalDocs::ConfigFile->AddError('Title can only be defined once.'); }; + } + + + elsif ($keyword eq 'subtitle') + { + if (defined $title) + { + if (!defined $subTitle) + { $subTitle = $self->RestoreAmpChars($value); } + else + { NaturalDocs::ConfigFile->AddError('SubTitle can only be defined once.'); }; + } + else + { NaturalDocs::ConfigFile->AddError('Title must be defined before SubTitle.'); }; + } + + + elsif ($keyword eq 'footer') + { + if (!defined $footer) + { $footer = $self->RestoreAmpChars($value); } + else + { NaturalDocs::ConfigFile->AddError('Footer can only be defined once.'); }; + } + + + elsif ($keyword eq 'timestamp') + { + if (!defined $timestampCode) + { + $timestampCode = $self->RestoreAmpChars($value); + $self->GenerateTimestampText(); + } + else + { NaturalDocs::ConfigFile->AddError('Timestamp can only be defined once.'); }; + } + + + elsif ($keyword eq 'text') + { + $currentGroup->PushToGroup( NaturalDocs::Menu::Entry->New(::MENU_TEXT(), $self->RestoreAmpChars($value), + undef, undef) ); + } + + + elsif ($keyword eq 'link') + { + my ($title, $url); + + if ($value =~ /^([^\(\)]+?) ?\(([^\)]+)\)$/) + { + ($title, $url) = ($1, $2); + } + elsif (defined $comment) + { + $value .= $comment; + + if ($value =~ /^([^\(\)]+?) ?\(([^\)]+)\) ?(?:#.*)?$/) + { + ($title, $url) = ($1, $2); + }; + }; + + if ($title) + { + $currentGroup->PushToGroup( NaturalDocs::Menu::Entry->New(::MENU_LINK(), $self->RestoreAmpChars($title), + $self->RestoreAmpChars($url), undef) ); + } + else + { NaturalDocs::ConfigFile->AddError('Link lines must be in the format "Link: [title] ([url])"'); }; + } + + + elsif ($keyword eq 'data') + { + $value =~ /^(\d)\((.*)\)$/; + my ($number, $data) = ($1, $2); + + $data = NaturalDocs::ConfigFile->Unobscure($data); + + # The input directory naming convention changed with version 1.32, but NaturalDocs::Settings will handle that + # automatically. + + if ($number == 1) + { + my ($dirName, $inputDir) = split(/\/\/\//, $data, 2); + $inputDirectories->{$inputDir} = $dirName; + } + elsif ($number == 2) + { $onlyDirectoryName = $data; }; + # Ignore other numbers because it may be from a future format and we don't want to make the user delete it + # manually. + } + + elsif ($keyword eq "don't index") + { + my @indexes = split(/, ?/, $value); + + foreach my $index (@indexes) + { + my $indexType = NaturalDocs::Topics->TypeFromName( $self->RestoreAmpChars($index) ); + + if (defined $indexType) + { $bannedIndexes{$indexType} = 1; }; + }; + } + + elsif ($keyword eq 'index') + { + my $entry = NaturalDocs::Menu::Entry->New(::MENU_INDEX(), $self->RestoreAmpChars($value), + ::TOPIC_GENERAL(), undef); + $currentGroup->PushToGroup($entry); + + $indexes{::TOPIC_GENERAL()} = 1; + } + + elsif (substr($keyword, -6) eq ' index') + { + my $index = substr($keyword, 0, -6); + my ($indexType, $indexInfo) = NaturalDocs::Topics->NameInfo( $self->RestoreAmpChars($index) ); + + if (defined $indexType) + { + if ($indexInfo->Index()) + { + $indexes{$indexType} = 1; + $currentGroup->PushToGroup( + NaturalDocs::Menu::Entry->New(::MENU_INDEX(), $self->RestoreAmpChars($value), $indexType, undef) ); + } + else + { + # If it's on the menu but isn't indexable, the topic setting may have changed out from under it. + $hasChanged = 1; + }; + } + else + { + NaturalDocs::ConfigFile->AddError($index . ' is not a valid index type.'); + }; + } + + else + { + NaturalDocs::ConfigFile->AddError(ucfirst($keyword) . ' is not a valid keyword.'); + }; + }; + + + # End a braceless group, if we were in one. + if ($inBracelessGroup) + { + $currentGroup = pop @groupStack; + $inBracelessGroup = undef; + }; + + # Close up all open groups. + my $openGroups = 0; + while (scalar @groupStack) + { + $currentGroup = pop @groupStack; + $openGroups++; + }; + + if ($openGroups == 1) + { NaturalDocs::ConfigFile->AddError('There is an unclosed group.'); } + elsif ($openGroups > 1) + { NaturalDocs::ConfigFile->AddError('There are ' . $openGroups . ' unclosed groups.'); }; + + + if (!scalar keys %$inputDirectories) + { + $inputDirectories = undef; + $relativeFiles = 1; + }; + + NaturalDocs::ConfigFile->Close(); + + return ($inputDirectories, $relativeFiles, $onlyDirectoryName); + } + + else + { return ( ); }; + }; + + +# +# Function: SaveMenuFile +# +# Saves the current menu to <Menu.txt>. +# +sub SaveMenuFile + { + my ($self) = @_; + + open(MENUFILEHANDLE, '>' . NaturalDocs::Project->UserConfigFile('Menu.txt')) + or die "Couldn't save menu file " . NaturalDocs::Project->UserConfigFile('Menu.txt') . "\n"; + + + print MENUFILEHANDLE + "Format: " . NaturalDocs::Settings->TextAppVersion() . "\n\n\n"; + + my $inputDirs = NaturalDocs::Settings->InputDirectories(); + + + if (defined $title) + { + print MENUFILEHANDLE 'Title: ' . $self->ConvertAmpChars($title) . "\n"; + + if (defined $subTitle) + { + print MENUFILEHANDLE 'SubTitle: ' . $self->ConvertAmpChars($subTitle) . "\n"; + } + else + { + print MENUFILEHANDLE + "\n" + . "# You can also add a sub-title to your menu like this:\n" + . "# SubTitle: [subtitle]\n"; + }; + } + else + { + print MENUFILEHANDLE + "# You can add a title and sub-title to your menu like this:\n" + . "# Title: [project name]\n" + . "# SubTitle: [subtitle]\n"; + }; + + print MENUFILEHANDLE "\n"; + + if (defined $footer) + { + print MENUFILEHANDLE 'Footer: ' . $self->ConvertAmpChars($footer) . "\n"; + } + else + { + print MENUFILEHANDLE + "# You can add a footer to your documentation like this:\n" + . "# Footer: [text]\n" + . "# If you want to add a copyright notice, this would be the place to do it.\n"; + }; + + if (defined $timestampCode) + { + print MENUFILEHANDLE 'Timestamp: ' . $self->ConvertAmpChars($timestampCode) . "\n"; + } + else + { + print MENUFILEHANDLE + "\n" + . "# You can add a timestamp to your documentation like one of these:\n" + . "# Timestamp: Generated on month day, year\n" + . "# Timestamp: Updated mm/dd/yyyy\n" + . "# Timestamp: Last updated mon day\n" + . "#\n"; + }; + + print MENUFILEHANDLE + qq{# m - One or two digit month. January is "1"\n} + . qq{# mm - Always two digit month. January is "01"\n} + . qq{# mon - Short month word. January is "Jan"\n} + . qq{# month - Long month word. January is "January"\n} + . qq{# d - One or two digit day. 1 is "1"\n} + . qq{# dd - Always two digit day. 1 is "01"\n} + . qq{# day - Day with letter extension. 1 is "1st"\n} + . qq{# yy - Two digit year. 2006 is "06"\n} + . qq{# yyyy - Four digit year. 2006 is "2006"\n} + . qq{# year - Four digit year. 2006 is "2006"\n} + + . "\n"; + + if (scalar keys %bannedIndexes) + { + print MENUFILEHANDLE + + "# These are indexes you deleted, so Natural Docs will not add them again\n" + . "# unless you remove them from this line.\n" + . "\n" + . "Don't Index: "; + + my $first = 1; + + foreach my $index (keys %bannedIndexes) + { + if (!$first) + { print MENUFILEHANDLE ', '; } + else + { $first = undef; }; + + print MENUFILEHANDLE $self->ConvertAmpChars( NaturalDocs::Topics->NameOfType($index, 1), CONVERT_COMMAS() ); + }; + + print MENUFILEHANDLE "\n\n"; + }; + + + # Remember to keep lines below eighty characters. + + print MENUFILEHANDLE + "\n" + . "# --------------------------------------------------------------------------\n" + . "# \n" + . "# Cut and paste the lines below to change the order in which your files\n" + . "# appear on the menu. Don't worry about adding or removing files, Natural\n" + . "# Docs will take care of that.\n" + . "# \n" + . "# You can further organize the menu by grouping the entries. Add a\n" + . "# \"Group: [name] {\" line to start a group, and add a \"}\" to end it.\n" + . "# \n" + . "# You can add text and web links to the menu by adding \"Text: [text]\" and\n" + . "# \"Link: [name] ([URL])\" lines, respectively.\n" + . "# \n" + . "# The formatting and comments are auto-generated, so don't worry about\n" + . "# neatness when editing the file. Natural Docs will clean it up the next\n" + . "# time it is run. When working with groups, just deal with the braces and\n" + . "# forget about the indentation and comments.\n" + . "# \n"; + + if (scalar @$inputDirs > 1) + { + print MENUFILEHANDLE + "# You can use this file on other computers even if they use different\n" + . "# directories. As long as the command line points to the same source files,\n" + . "# Natural Docs will be able to correct the locations automatically.\n" + . "# \n"; + }; + + print MENUFILEHANDLE + "# --------------------------------------------------------------------------\n" + + . "\n\n"; + + + $self->WriteMenuEntries($menu->GroupContent(), \*MENUFILEHANDLE, undef, (scalar @$inputDirs == 1)); + + + if (scalar @$inputDirs > 1) + { + print MENUFILEHANDLE + "\n\n##### Do not change or remove these lines. #####\n"; + + foreach my $inputDir (@$inputDirs) + { + print MENUFILEHANDLE + 'Data: 1(' . NaturalDocs::ConfigFile->Obscure( NaturalDocs::Settings->InputDirectoryNameOf($inputDir) + . '///' . $inputDir ) . ")\n"; + }; + } + elsif (lc(NaturalDocs::Settings->InputDirectoryNameOf($inputDirs->[0])) != 1) + { + print MENUFILEHANDLE + "\n\n##### Do not change or remove this line. #####\n" + . 'Data: 2(' . NaturalDocs::ConfigFile->Obscure( NaturalDocs::Settings->InputDirectoryNameOf($inputDirs->[0]) ) . ")\n"; + } + + close(MENUFILEHANDLE); + }; + + +# +# Function: WriteMenuEntries +# +# A recursive function to write the contents of an arrayref of <NaturalDocs::Menu::Entry> objects to disk. +# +# Parameters: +# +# entries - The arrayref of menu entries to write. +# fileHandle - The handle to the output file. +# indentChars - The indentation _characters_ to add before each line. It is not the number of characters, it is the characters +# themselves. Use undef for none. +# relativeFiles - Whether to use relative file names. +# +sub WriteMenuEntries #(entries, fileHandle, indentChars, relativeFiles) + { + my ($self, $entries, $fileHandle, $indentChars, $relativeFiles) = @_; + my $lastEntryType; + + foreach my $entry (@$entries) + { + if ($entry->Type() == ::MENU_FILE()) + { + my $fileName; + + if ($relativeFiles) + { $fileName = (NaturalDocs::Settings->SplitFromInputDirectory($entry->Target()))[1]; } + else + { $fileName = $entry->Target(); }; + + print $fileHandle $indentChars . 'File: ' . $self->ConvertAmpChars( $entry->Title(), CONVERT_PARENTHESIS() ) + . ' (' . ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE() ? 'no auto-title, ' : '') + . $self->ConvertAmpChars($fileName) . ")\n"; + } + elsif ($entry->Type() == ::MENU_GROUP()) + { + if (defined $lastEntryType && $lastEntryType != ::MENU_GROUP()) + { print $fileHandle "\n"; }; + + print $fileHandle $indentChars . 'Group: ' . $self->ConvertAmpChars( $entry->Title() ) . " {\n\n"; + $self->WriteMenuEntries($entry->GroupContent(), $fileHandle, ' ' . $indentChars, $relativeFiles); + print $fileHandle ' ' . $indentChars . '} # Group: ' . $self->ConvertAmpChars( $entry->Title() ) . "\n\n"; + } + elsif ($entry->Type() == ::MENU_TEXT()) + { + print $fileHandle $indentChars . 'Text: ' . $self->ConvertAmpChars( $entry->Title() ) . "\n"; + } + elsif ($entry->Type() == ::MENU_LINK()) + { + print $fileHandle $indentChars . 'Link: ' . $self->ConvertAmpChars( $entry->Title() ) . ' ' + . '(' . $self->ConvertAmpChars( $entry->Target(), CONVERT_PARENTHESIS() ) . ')' . "\n"; + } + elsif ($entry->Type() == ::MENU_INDEX()) + { + my $type; + if ($entry->Target() ne ::TOPIC_GENERAL()) + { + $type = NaturalDocs::Topics->NameOfType($entry->Target()) . ' '; + }; + + print $fileHandle $indentChars . $self->ConvertAmpChars($type, CONVERT_COLONS()) . 'Index: ' + . $self->ConvertAmpChars( $entry->Title() ) . "\n"; + }; + + $lastEntryType = $entry->Type(); + }; + }; + + +# +# Function: LoadPreviousMenuStateFile +# +# Loads and parses the previous menu state file. +# +# Returns: +# +# The array ( previousMenu, previousIndexes, previousFiles ) or an empty array if there was a problem with the file. +# +# previousMenu - A <MENU_GROUP> <NaturalDocs::Menu::Entry> object, similar to <menu>, which contains the entire +# previous menu. +# previousIndexes - An existence hashref of the index <TopicTypes> present in the previous menu. +# previousFiles - A hashref of the files present in the previous menu. The keys are the <FileNames>, and the entries are +# references to its object in previousMenu. +# +sub LoadPreviousMenuStateFile + { + my ($self) = @_; + + my $fileIsOkay; + my $version; + my $previousStateFileName = NaturalDocs::Project->DataFile('PreviousMenuState.nd'); + + if (open(PREVIOUSSTATEFILEHANDLE, '<' . $previousStateFileName)) + { + # See if it's binary. + binmode(PREVIOUSSTATEFILEHANDLE); + + my $firstChar; + read(PREVIOUSSTATEFILEHANDLE, $firstChar, 1); + + if ($firstChar == ::BINARY_FORMAT()) + { + $version = NaturalDocs::Version->FromBinaryFile(\*PREVIOUSSTATEFILEHANDLE); + + # Only the topic type format has changed since switching to binary, and we support both methods. + + if (NaturalDocs::Version->CheckFileFormat($version)) + { $fileIsOkay = 1; } + else + { close(PREVIOUSSTATEFILEHANDLE); }; + } + + else # it's not in binary + { close(PREVIOUSSTATEFILEHANDLE); }; + }; + + if ($fileIsOkay) + { + if (NaturalDocs::Project->UserConfigFileStatus('Menu.txt') == ::FILE_CHANGED()) + { $hasChanged = 1; }; + + + my $menu = NaturalDocs::Menu::Entry->New(::MENU_GROUP(), undef, undef, undef); + my $indexes = { }; + my $files = { }; + + my @groupStack; + my $currentGroup = $menu; + my $raw; + + # [UInt8: type or 0 for end group] + + while (read(PREVIOUSSTATEFILEHANDLE, $raw, 1)) + { + my ($type, $flags, $title, $titleLength, $target, $targetLength); + $type = unpack('C', $raw); + + if ($type == 0) + { $currentGroup = pop @groupStack; } + + elsif ($type == ::MENU_FILE()) + { + # [UInt8: noAutoTitle] [AString16: title] [AString16: target] + + read(PREVIOUSSTATEFILEHANDLE, $raw, 3); + (my $noAutoTitle, $titleLength) = unpack('Cn', $raw); + + if ($noAutoTitle) + { $flags = ::MENU_FILE_NOAUTOTITLE(); }; + + read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength); + read(PREVIOUSSTATEFILEHANDLE, $raw, 2); + + $targetLength = unpack('n', $raw); + + read(PREVIOUSSTATEFILEHANDLE, $target, $targetLength); + } + + elsif ($type == ::MENU_GROUP()) + { + # [AString16: title] + + read(PREVIOUSSTATEFILEHANDLE, $raw, 2); + $titleLength = unpack('n', $raw); + + read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength); + } + + elsif ($type == ::MENU_INDEX()) + { + # [AString16: title] + + read(PREVIOUSSTATEFILEHANDLE, $raw, 2); + $titleLength = unpack('n', $raw); + + read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength); + + if ($version >= NaturalDocs::Version->FromString('1.3')) + { + # [AString16: topic type] + read(PREVIOUSSTATEFILEHANDLE, $raw, 2); + $targetLength = unpack('n', $raw); + + read(PREVIOUSSTATEFILEHANDLE, $target, $targetLength); + } + else + { + # [UInt8: topic type (0 for general)] + read(PREVIOUSSTATEFILEHANDLE, $raw, 1); + $target = unpack('C', $raw); + + $target = NaturalDocs::Topics->TypeFromLegacy($target); + }; + } + + elsif ($type == ::MENU_LINK()) + { + # [AString16: title] [AString16: url] + + read(PREVIOUSSTATEFILEHANDLE, $raw, 2); + $titleLength = unpack('n', $raw); + + read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength); + read(PREVIOUSSTATEFILEHANDLE, $raw, 2); + $targetLength = unpack('n', $raw); + + read(PREVIOUSSTATEFILEHANDLE, $target, $targetLength); + } + + elsif ($type == ::MENU_TEXT()) + { + # [AString16: text] + + read(PREVIOUSSTATEFILEHANDLE, $raw, 2); + $titleLength = unpack('n', $raw); + + read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength); + }; + + + # The topic type of the index may have been removed. + + if ( !($type == ::MENU_INDEX() && !NaturalDocs::Topics->IsValidType($target)) ) + { + my $entry = NaturalDocs::Menu::Entry->New($type, $title, $target, ($flags || 0)); + $currentGroup->PushToGroup($entry); + + if ($type == ::MENU_FILE()) + { + $files->{$target} = $entry; + } + elsif ($type == ::MENU_GROUP()) + { + push @groupStack, $currentGroup; + $currentGroup = $entry; + } + elsif ($type == ::MENU_INDEX()) + { + $indexes->{$target} = 1; + }; + }; + + }; + + close(PREVIOUSSTATEFILEHANDLE); + + return ($menu, $indexes, $files); + } + else + { + $hasChanged = 1; + return ( ); + }; + }; + + +# +# Function: SavePreviousMenuStateFile +# +# Saves changes to <PreviousMenuState.nd>. +# +sub SavePreviousMenuStateFile + { + my ($self) = @_; + + open (PREVIOUSSTATEFILEHANDLE, '>' . NaturalDocs::Project->DataFile('PreviousMenuState.nd')) + or die "Couldn't save " . NaturalDocs::Project->DataFile('PreviousMenuState.nd') . ".\n"; + + binmode(PREVIOUSSTATEFILEHANDLE); + + print PREVIOUSSTATEFILEHANDLE '' . ::BINARY_FORMAT(); + + NaturalDocs::Version->ToBinaryFile(\*PREVIOUSSTATEFILEHANDLE, NaturalDocs::Settings->AppVersion()); + + $self->WritePreviousMenuStateEntries($menu->GroupContent(), \*PREVIOUSSTATEFILEHANDLE); + + close(PREVIOUSSTATEFILEHANDLE); + }; + + +# +# Function: WritePreviousMenuStateEntries +# +# A recursive function to write the contents of an arrayref of <NaturalDocs::Menu::Entry> objects to disk. +# +# Parameters: +# +# entries - The arrayref of menu entries to write. +# fileHandle - The handle to the output file. +# +sub WritePreviousMenuStateEntries #(entries, fileHandle) + { + my ($self, $entries, $fileHandle) = @_; + + foreach my $entry (@$entries) + { + if ($entry->Type() == ::MENU_FILE()) + { + # We need to do length manually instead of using n/A in the template because it's not supported in earlier versions + # of Perl. + + # [UInt8: MENU_FILE] [UInt8: noAutoTitle] [AString16: title] [AString16: target] + print $fileHandle pack('CCnA*nA*', ::MENU_FILE(), ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE() ? 1 : 0), + length($entry->Title()), $entry->Title(), + length($entry->Target()), $entry->Target()); + } + + elsif ($entry->Type() == ::MENU_GROUP()) + { + # [UInt8: MENU_GROUP] [AString16: title] + print $fileHandle pack('CnA*', ::MENU_GROUP(), length($entry->Title()), $entry->Title()); + $self->WritePreviousMenuStateEntries($entry->GroupContent(), $fileHandle); + print $fileHandle pack('C', 0); + } + + elsif ($entry->Type() == ::MENU_INDEX()) + { + # [UInt8: MENU_INDEX] [AString16: title] [AString16: topic type] + print $fileHandle pack('CnA*nA*', ::MENU_INDEX(), length($entry->Title()), $entry->Title(), + length($entry->Target()), $entry->Target()); + } + + elsif ($entry->Type() == ::MENU_LINK()) + { + # [UInt8: MENU_LINK] [AString16: title] [AString16: url] + print $fileHandle pack('CnA*nA*', ::MENU_LINK(), length($entry->Title()), $entry->Title(), + length($entry->Target()), $entry->Target()); + } + + elsif ($entry->Type() == ::MENU_TEXT()) + { + # [UInt8: MENU_TEXT] [AString16: hext] + print $fileHandle pack('CnA*', ::MENU_TEXT(), length($entry->Title()), $entry->Title()); + }; + }; + + }; + + +# +# Function: CheckForTrashedMenu +# +# Checks the menu to see if a significant number of file entries didn't resolve to actual files, and if so, saves a backup of the +# menu and issues a warning. +# +# Parameters: +# +# numberOriginallyInMenu - A count of how many file entries were in the menu orignally. +# numberRemoved - A count of how many file entries were removed from the menu. +# +sub CheckForTrashedMenu #(numberOriginallyInMenu, numberRemoved) + { + my ($self, $numberOriginallyInMenu, $numberRemoved) = @_; + + no integer; + + if ( ($numberOriginallyInMenu >= 6 && $numberRemoved == $numberOriginallyInMenu) || + ($numberOriginallyInMenu >= 12 && ($numberRemoved / $numberOriginallyInMenu) >= 0.4) || + ($numberRemoved >= 15) ) + { + my $backupFile = NaturalDocs::Project->UserConfigFile('Menu_Backup.txt'); + my $backupFileNumber = 1; + + while (-e $backupFile) + { + $backupFileNumber++; + $backupFile = NaturalDocs::Project->UserConfigFile('Menu_Backup_' . $backupFileNumber . '.txt'); + }; + + NaturalDocs::File->Copy( NaturalDocs::Project->UserConfigFile('Menu.txt'), $backupFile ); + + print STDERR + "\n" + # GNU format. See http://www.gnu.org/prep/standards_15.html + . "NaturalDocs: warning: possible trashed menu\n" + . "\n" + . " Natural Docs has detected that a significant number file entries in the\n" + . " menu did not resolve to actual files. A backup of your original menu file\n" + . " has been saved as\n" + . "\n" + . " " . $backupFile . "\n" + . "\n" + . " - If you recently deleted a lot of files from your project, you can safely\n" + . " ignore this message. They have been deleted from the menu as well.\n" + . " - If you recently rearranged your source tree, you may want to restore your\n" + . " menu from the backup and do a search and replace to preserve your layout.\n" + . " Otherwise the position of any moved files will be reset.\n" + . " - If neither of these is the case, you may have gotten the -i parameter\n" + . " wrong in the command line. You should definitely restore the backup and\n" + . " try again, because otherwise every file in your menu will be reset.\n" + . "\n"; + }; + + use integer; + }; + + +# +# Function: GenerateTimestampText +# +# Generates <timestampText> from <timestampCode> with the current date. +# +sub GenerateTimestampText + { + my $self = shift; + + my @longMonths = ( 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' ); + my @shortMonths = ( 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec' ); + + my (undef, undef, undef, $day, $month, $year) = localtime(); + $year += 1900; + + my $longDay; + if ($day % 10 == 1 && $day != 11) + { $longDay = $day . 'st'; } + elsif ($day % 10 == 2 && $day != 12) + { $longDay = $day . 'nd'; } + elsif ($day % 10 == 3 && $day != 13) + { $longDay = $day . 'rd'; } + else + { $longDay = $day . 'th'; }; + + + $timestampText = $timestampCode; + + $timestampText =~ s/(?<![a-z])month(?![a-z])/$longMonths[$month]/i; + $timestampText =~ s/(?<![a-z])mon(?![a-z])/$shortMonths[$month]/i; + $timestampText =~ s/(?<![a-z])mm(?![a-z])/sprintf('%02d', $month + 1)/ie; + $timestampText =~ s/(?<![a-z])m(?![a-z])/$month + 1/ie; + + $timestampText =~ s/(?<![a-z])day(?![a-z])/$longDay/i; + $timestampText =~ s/(?<![a-z])dd(?![a-z])/sprintf('%02d', $day)/ie; + $timestampText =~ s/(?<![a-z])d(?![a-z])/$day/i; + + $timestampText =~ s/(?<![a-z])(?:year|yyyy)(?![a-z])/$year/i; + $timestampText =~ s/(?<![a-z])(?:year|yyyy)(?![a-z])/$year/i; #XXX + $timestampText =~ s/(?<![a-z])yy(?![a-z])/sprintf('%02d', $year % 100)/ie; + }; + + +use constant CONVERT_PARENTHESIS => 0x01; +use constant CONVERT_COMMAS => 0x02; +use constant CONVERT_COLONS => 0x04; + +# +# Function: ConvertAmpChars +# Replaces certain characters in the string with their entities and returns it. +# +# Parameters: +# +# text - The text to convert. +# flags - The flags of any additional characters to convert. +# +# Flags: +# +# - CONVERT_PARENTHESIS +# - CONVERT_COMMAS +# - CONVERT_COLONS +# +# Returns: +# +# The string with the amp chars converted. +# +sub ConvertAmpChars #(string text, int flags) => string + { + my ($self, $text, $flags) = @_; + + $text =~ s/&/&/g; + $text =~ s/\{/{/g; + $text =~ s/\}/}/g; + + if ($flags & CONVERT_PARENTHESIS()) + { + $text =~ s/\(/&lparen;/g; + $text =~ s/\)/&rparen;/g; + }; + if ($flags & CONVERT_COMMAS()) + { + $text =~ s/\,/,/g; + }; + if ($flags & CONVERT_COLONS()) + { + $text =~ s/\:/:/g; + }; + + return $text; + }; + + +# +# Function: RestoreAmpChars +# Replaces entity characters in the string with their original characters and returns it. This will restore all amp chars regardless +# of the flags passed to <ConvertAmpChars()>. +# +sub RestoreAmpChars #(string text) => string + { + my ($self, $text) = @_; + + $text =~ s/&lparen;/(/gi; + $text =~ s/&rparen;/)/gi; + $text =~ s/{/{/gi; + $text =~ s/}/}/gi; + $text =~ s/,/,/gi; + $text =~ s/&/&/gi; + $text =~ s/:/:/gi; + + return $text; + }; + + + +############################################################################### +# Group: Auto-Adjustment Functions + + +# +# Function: ResolveInputDirectories +# +# Detects if the input directories in the menu file match those in the command line, and if not, tries to resolve them. This allows +# menu files to work across machines, since the absolute paths won't be the same but the relative ones should be. +# +# Parameters: +# +# inputDirectoryNames - A hashref of the input directories appearing in the menu file, or undef if none. The keys are the +# directories, and the values are their names. May be undef. +# +sub ResolveInputDirectories #(inputDirectoryNames) + { + my ($self, $menuDirectoryNames) = @_; + + + # Determine which directories don't match the command line, if any. + + my $inputDirectories = NaturalDocs::Settings->InputDirectories(); + my @unresolvedMenuDirectories; + + foreach my $menuDirectory (keys %$menuDirectoryNames) + { + my $found; + + foreach my $inputDirectory (@$inputDirectories) + { + if ($menuDirectory eq $inputDirectory) + { + $found = 1; + last; + }; + }; + + if (!$found) + { push @unresolvedMenuDirectories, $menuDirectory; }; + }; + + # Quit if everything matches up, which should be the most common case. + if (!scalar @unresolvedMenuDirectories) + { return; }; + + # Poop. See which input directories are still available. + + my @unresolvedInputDirectories; + + foreach my $inputDirectory (@$inputDirectories) + { + if (!exists $menuDirectoryNames->{$inputDirectory}) + { push @unresolvedInputDirectories, $inputDirectory; }; + }; + + # Quit if there are none. This means an input directory is in the menu that isn't in the command line. Natural Docs should + # proceed normally and let the files be deleted. + if (!scalar @unresolvedInputDirectories) + { + $hasChanged = 1; + return; + }; + + # The index into menuDirectoryScores is the same as in unresolvedMenuDirectories. The index into each arrayref within it is + # the same as in unresolvedInputDirectories. + my @menuDirectoryScores; + for (my $i = 0; $i < scalar @unresolvedMenuDirectories; $i++) + { push @menuDirectoryScores, [ ]; }; + + + # Now plow through the menu, looking for files that have an unresolved base. + + my @menuGroups = ( $menu ); + + while (scalar @menuGroups) + { + my $currentGroup = pop @menuGroups; + my $currentGroupContent = $currentGroup->GroupContent(); + + foreach my $entry (@$currentGroupContent) + { + if ($entry->Type() == ::MENU_GROUP()) + { + push @menuGroups, $entry; + } + elsif ($entry->Type() == ::MENU_FILE()) + { + # Check if it uses an unresolved base. + for (my $i = 0; $i < scalar @unresolvedMenuDirectories; $i++) + { + if (NaturalDocs::File->IsSubPathOf($unresolvedMenuDirectories[$i], $entry->Target())) + { + my $relativePath = NaturalDocs::File->MakeRelativePath($unresolvedMenuDirectories[$i], $entry->Target()); + $self->ResolveFile($relativePath, \@unresolvedInputDirectories, $menuDirectoryScores[$i]); + last; + }; + }; + }; + }; + }; + + + # Now, create an array of score objects. Each score object is the three value arrayref [ from, to, score ]. From and To are the + # conversion options and are the indexes into unresolvedInput/MenuDirectories. We'll sort this array by score to get the best + # possible conversions. Yes, really. + my @scores; + + for (my $menuIndex = 0; $menuIndex < scalar @unresolvedMenuDirectories; $menuIndex++) + { + for (my $inputIndex = 0; $inputIndex < scalar @unresolvedInputDirectories; $inputIndex++) + { + if ($menuDirectoryScores[$menuIndex]->[$inputIndex]) + { + push @scores, [ $menuIndex, $inputIndex, $menuDirectoryScores[$menuIndex]->[$inputIndex] ]; + }; + }; + }; + + @scores = sort { $b->[2] <=> $a->[2] } @scores; + + + # Now we determine what goes where. + my @menuDirectoryConversions; + + foreach my $scoreObject (@scores) + { + if (!defined $menuDirectoryConversions[ $scoreObject->[0] ]) + { + $menuDirectoryConversions[ $scoreObject->[0] ] = $unresolvedInputDirectories[ $scoreObject->[1] ]; + }; + }; + + + # Now, FINALLY, we do the conversion. Note that not every menu directory may have a conversion defined. + + @menuGroups = ( $menu ); + + while (scalar @menuGroups) + { + my $currentGroup = pop @menuGroups; + my $currentGroupContent = $currentGroup->GroupContent(); + + foreach my $entry (@$currentGroupContent) + { + if ($entry->Type() == ::MENU_GROUP()) + { + push @menuGroups, $entry; + } + elsif ($entry->Type() == ::MENU_FILE()) + { + # Check if it uses an unresolved base. + for (my $i = 0; $i < scalar @unresolvedMenuDirectories; $i++) + { + if (NaturalDocs::File->IsSubPathOf($unresolvedMenuDirectories[$i], $entry->Target()) && + defined $menuDirectoryConversions[$i]) + { + my $relativePath = NaturalDocs::File->MakeRelativePath($unresolvedMenuDirectories[$i], $entry->Target()); + $entry->SetTarget( NaturalDocs::File->JoinPaths($menuDirectoryConversions[$i], $relativePath) ); + last; + }; + }; + }; + }; + }; + + + # Whew. + + $hasChanged = 1; + }; + + +# +# Function: ResolveRelativeInputDirectories +# +# Resolves relative input directories to the input directories available. +# +sub ResolveRelativeInputDirectories + { + my ($self) = @_; + + my $inputDirectories = NaturalDocs::Settings->InputDirectories(); + my $resolvedInputDirectory; + + if (scalar @$inputDirectories == 1) + { $resolvedInputDirectory = $inputDirectories->[0]; } + else + { + my @score; + + # Plow through the menu, looking for files and scoring them. + + my @menuGroups = ( $menu ); + + while (scalar @menuGroups) + { + my $currentGroup = pop @menuGroups; + my $currentGroupContent = $currentGroup->GroupContent(); + + foreach my $entry (@$currentGroupContent) + { + if ($entry->Type() == ::MENU_GROUP()) + { + push @menuGroups, $entry; + } + elsif ($entry->Type() == ::MENU_FILE()) + { + $self->ResolveFile($entry->Target(), $inputDirectories, \@score); + }; + }; + }; + + # Determine the best match. + + my $bestScore = 0; + my $bestIndex = 0; + + for (my $i = 0; $i < scalar @$inputDirectories; $i++) + { + if ($score[$i] > $bestScore) + { + $bestScore = $score[$i]; + $bestIndex = $i; + }; + }; + + $resolvedInputDirectory = $inputDirectories->[$bestIndex]; + }; + + + # Okay, now that we have our resolved directory, update everything. + + my @menuGroups = ( $menu ); + + while (scalar @menuGroups) + { + my $currentGroup = pop @menuGroups; + my $currentGroupContent = $currentGroup->GroupContent(); + + foreach my $entry (@$currentGroupContent) + { + if ($entry->Type() == ::MENU_GROUP()) + { push @menuGroups, $entry; } + elsif ($entry->Type() == ::MENU_FILE()) + { + $entry->SetTarget( NaturalDocs::File->JoinPaths($resolvedInputDirectory, $entry->Target()) ); + }; + }; + }; + + if (scalar @$inputDirectories > 1) + { $hasChanged = 1; }; + + return $resolvedInputDirectory; + }; + + +# +# Function: ResolveFile +# +# Tests a relative path against a list of directories. Adds one to the score of each base where there is a match. +# +# Parameters: +# +# relativePath - The relative file name to test. +# possibleBases - An arrayref of bases to test it against. +# possibleBaseScores - An arrayref of scores to adjust. The score indexes should correspond to the base indexes. +# +sub ResolveFile #(relativePath, possibleBases, possibleBaseScores) + { + my ($self, $relativePath, $possibleBases, $possibleBaseScores) = @_; + + for (my $i = 0; $i < scalar @$possibleBases; $i++) + { + if (-e NaturalDocs::File->JoinPaths($possibleBases->[$i], $relativePath)) + { $possibleBaseScores->[$i]++; }; + }; + }; + + +# +# Function: LockUserTitleChanges +# +# Detects if the user manually changed any file titles, and if so, automatically locks them with <MENU_FILE_NOAUTOTITLE>. +# +# Parameters: +# +# previousMenuFiles - A hashref of the files from the previous menu state. The keys are the <FileNames>, and the values are +# references to their <NaturalDocs::Menu::Entry> objects. +# +sub LockUserTitleChanges #(previousMenuFiles) + { + my ($self, $previousMenuFiles) = @_; + + my @groupStack = ( $menu ); + my $groupEntry; + + while (scalar @groupStack) + { + $groupEntry = pop @groupStack; + + foreach my $entry (@{$groupEntry->GroupContent()}) + { + + # If it's an unlocked file entry + if ($entry->Type() == ::MENU_FILE() && ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0) + { + my $previousEntry = $previousMenuFiles->{$entry->Target()}; + + # If the previous entry was also unlocked and the titles are different, the user changed the title. Automatically lock it. + if (defined $previousEntry && ($previousEntry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0 && + $entry->Title() ne $previousEntry->Title()) + { + $entry->SetFlags($entry->Flags() | ::MENU_FILE_NOAUTOTITLE()); + $hasChanged = 1; + }; + } + + elsif ($entry->Type() == ::MENU_GROUP()) + { + push @groupStack, $entry; + }; + + }; + }; + }; + + +# +# Function: FlagAutoTitleChanges +# +# Finds which files have auto-titles that changed and flags their groups for updating with <MENU_GROUP_UPDATETITLES> and +# <MENU_GROUP_UPDATEORDER>. +# +sub FlagAutoTitleChanges + { + my ($self) = @_; + + my @groupStack = ( $menu ); + my $groupEntry; + + while (scalar @groupStack) + { + $groupEntry = pop @groupStack; + + foreach my $entry (@{$groupEntry->GroupContent()}) + { + if ($entry->Type() == ::MENU_FILE() && ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0 && + exists $defaultTitlesChanged{$entry->Target()}) + { + $groupEntry->SetFlags($groupEntry->Flags() | ::MENU_GROUP_UPDATETITLES() | ::MENU_GROUP_UPDATEORDER()); + $hasChanged = 1; + } + elsif ($entry->Type() == ::MENU_GROUP()) + { + push @groupStack, $entry; + }; + }; + }; + }; + + +# +# Function: AutoPlaceNewFiles +# +# Adds files to the menu that aren't already on it, attempting to guess where they belong. +# +# New files are placed after a dummy <MENU_ENDOFORIGINAL> entry so that they don't affect the detected order. Also, the +# groups they're placed in get <MENU_GROUP_UPDATETITLES>, <MENU_GROUP_UPDATESTRUCTURE>, and +# <MENU_GROUP_UPDATEORDER> flags. +# +# Parameters: +# +# filesInMenu - An existence hash of all the <FileNames> present in the menu. +# +sub AutoPlaceNewFiles #(fileInMenu) + { + my ($self, $filesInMenu) = @_; + + my $files = NaturalDocs::Project->FilesWithContent(); + + my $directories; + + foreach my $file (keys %$files) + { + if (!exists $filesInMenu->{$file}) + { + # This is done on demand because new files shouldn't be added very often, so this will save time. + if (!defined $directories) + { $directories = $self->MatchDirectoriesAndGroups(); }; + + my $targetGroup; + my $fileDirectoryString = (NaturalDocs::File->SplitPath($file))[1]; + + $targetGroup = $directories->{$fileDirectoryString}; + + if (!defined $targetGroup) + { + # Okay, if there's no exact match, work our way down. + + my @fileDirectories = NaturalDocs::File->SplitDirectories($fileDirectoryString); + + do + { + pop @fileDirectories; + $targetGroup = $directories->{ NaturalDocs::File->JoinDirectories(@fileDirectories) }; + } + while (!defined $targetGroup && scalar @fileDirectories); + + if (!defined $targetGroup) + { $targetGroup = $menu; }; + }; + + $targetGroup->MarkEndOfOriginal(); + $targetGroup->PushToGroup( NaturalDocs::Menu::Entry->New(::MENU_FILE(), undef, $file, undef) ); + + $targetGroup->SetFlags( $targetGroup->Flags() | ::MENU_GROUP_UPDATETITLES() | + ::MENU_GROUP_UPDATESTRUCTURE() | ::MENU_GROUP_UPDATEORDER() ); + + $hasChanged = 1; + }; + }; + }; + + +# +# Function: MatchDirectoriesAndGroups +# +# Determines which groups files in certain directories should be placed in. +# +# Returns: +# +# A hashref. The keys are the directory names, and the values are references to the group objects they should be placed in. +# +# This only repreesents directories that currently have files on the menu, so it shouldn't be assumed that every possible +# directory will exist. To match, you should first try to match the directory, and then strip the deepest directories one by +# one until there's a match or there's none left. If there's none left, use the root group <menu>. +# +sub MatchDirectoriesAndGroups + { + my ($self) = @_; + + # The keys are the directory names, and the values are hashrefs. For the hashrefs, the keys are the group objects, and the + # values are the number of files in them from that directory. In other words, + # $directories{$directory}->{$groupEntry} = $count; + my %directories; + # Note that we need to use Tie::RefHash to use references as keys. Won't work otherwise. Also, not every Perl distro comes + # with Tie::RefHash::Nestable, so we can't rely on that. + + # We're using an index instead of pushing and popping because we want to save a list of the groups in the order they appear + # to break ties. + my @groups = ( $menu ); + my $groupIndex = 0; + + + # Count the number of files in each group that appear in each directory. + + while ($groupIndex < scalar @groups) + { + my $groupEntry = $groups[$groupIndex]; + + foreach my $entry (@{$groupEntry->GroupContent()}) + { + if ($entry->Type() == ::MENU_GROUP()) + { + push @groups, $entry; + } + elsif ($entry->Type() == ::MENU_FILE()) + { + my $directory = (NaturalDocs::File->SplitPath($entry->Target()))[1]; + + if (!exists $directories{$directory}) + { + my $subHash = { }; + tie %$subHash, 'Tie::RefHash'; + $directories{$directory} = $subHash; + }; + + if (!exists $directories{$directory}->{$groupEntry}) + { $directories{$directory}->{$groupEntry} = 1; } + else + { $directories{$directory}->{$groupEntry}++; }; + }; + }; + + $groupIndex++; + }; + + + # Determine which group goes with which directory, breaking ties by using whichever group appears first. + + my $finalDirectories = { }; + + while (my ($directory, $directoryGroups) = each %directories) + { + my $bestGroup; + my $bestCount = 0; + my %tiedGroups; # Existence hash + + while (my ($group, $count) = each %$directoryGroups) + { + if ($count > $bestCount) + { + $bestGroup = $group; + $bestCount = $count; + %tiedGroups = ( ); + } + elsif ($count == $bestCount) + { + $tiedGroups{$group} = 1; + }; + }; + + # Break ties. + if (scalar keys %tiedGroups) + { + $tiedGroups{$bestGroup} = 1; + + foreach my $group (@groups) + { + if (exists $tiedGroups{$group}) + { + $bestGroup = $group; + last; + }; + }; + }; + + + $finalDirectories->{$directory} = $bestGroup; + }; + + + return $finalDirectories; + }; + + +# +# Function: RemoveDeadFiles +# +# Removes files from the menu that no longer exist or no longer have Natural Docs content. +# +# Returns: +# +# The number of file entries removed. +# +sub RemoveDeadFiles + { + my ($self) = @_; + + my @groupStack = ( $menu ); + my $numberRemoved = 0; + + my $filesWithContent = NaturalDocs::Project->FilesWithContent(); + + while (scalar @groupStack) + { + my $groupEntry = pop @groupStack; + my $groupContent = $groupEntry->GroupContent(); + + my $index = 0; + while ($index < scalar @$groupContent) + { + if ($groupContent->[$index]->Type() == ::MENU_FILE() && + !exists $filesWithContent->{ $groupContent->[$index]->Target() } ) + { + $groupEntry->DeleteFromGroup($index); + + $groupEntry->SetFlags( $groupEntry->Flags() | ::MENU_GROUP_UPDATETITLES() | + ::MENU_GROUP_UPDATESTRUCTURE() ); + $numberRemoved++; + $hasChanged = 1; + } + + elsif ($groupContent->[$index]->Type() == ::MENU_GROUP()) + { + push @groupStack, $groupContent->[$index]; + $index++; + } + + else + { $index++; }; + }; + }; + + return $numberRemoved; + }; + + +# +# Function: BanAndUnbanIndexes +# +# Adjusts the indexes that are banned depending on if the user added or deleted any. +# +sub BanAndUnbanIndexes + { + my ($self) = @_; + + # Unban any indexes that are present, meaning the user added them back manually without deleting the ban. + foreach my $index (keys %indexes) + { delete $bannedIndexes{$index}; }; + + # Ban any indexes that were in the previous menu but not the current, meaning the user manually deleted them. However, + # don't do this if the topic isn't indexable, meaning they changed the topic type rather than the menu. + foreach my $index (keys %previousIndexes) + { + if (!exists $indexes{$index} && NaturalDocs::Topics->TypeInfo($index)->Index()) + { $bannedIndexes{$index} = 1; }; + }; + }; + + +# +# Function: AddAndRemoveIndexes +# +# Automatically adds and removes index entries on the menu as necessary. <DetectIndexGroups()> should be called +# beforehand. +# +sub AddAndRemoveIndexes + { + my ($self) = @_; + + my %validIndexes; + my @allIndexes = NaturalDocs::Topics->AllIndexableTypes(); + + foreach my $index (@allIndexes) + { + # Strip the banned indexes first so it's potentially less work for SymbolTable. + if (!exists $bannedIndexes{$index}) + { $validIndexes{$index} = 1; }; + }; + + %validIndexes = %{NaturalDocs::SymbolTable->HasIndexes(\%validIndexes)}; + + + # Delete dead indexes and find the best index group. + + my @groupStack = ( $menu ); + + my $bestIndexGroup; + my $bestIndexCount = 0; + + while (scalar @groupStack) + { + my $currentGroup = pop @groupStack; + my $index = 0; + + my $currentIndexCount = 0; + + while ($index < scalar @{$currentGroup->GroupContent()}) + { + my $entry = $currentGroup->GroupContent()->[$index]; + + if ($entry->Type() == ::MENU_INDEX()) + { + $currentIndexCount++; + + if ($currentIndexCount > $bestIndexCount) + { + $bestIndexCount = $currentIndexCount; + $bestIndexGroup = $currentGroup; + }; + + # Remove it if it's dead. + + if (!exists $validIndexes{ $entry->Target() }) + { + $currentGroup->DeleteFromGroup($index); + delete $indexes{ $entry->Target() }; + $hasChanged = 1; + } + else + { $index++; }; + } + + else + { + if ($entry->Type() == ::MENU_GROUP()) + { push @groupStack, $entry; }; + + $index++; + }; + }; + }; + + + # Now add the new indexes. + + foreach my $index (keys %indexes) + { delete $validIndexes{$index}; }; + + if (scalar keys %validIndexes) + { + # Add a group if there are no indexes at all. + + if ($bestIndexCount == 0) + { + $menu->MarkEndOfOriginal(); + + my $newIndexGroup = NaturalDocs::Menu::Entry->New(::MENU_GROUP(), 'Index', undef, + ::MENU_GROUP_ISINDEXGROUP()); + $menu->PushToGroup($newIndexGroup); + + $bestIndexGroup = $newIndexGroup; + $menu->SetFlags( $menu->Flags() | ::MENU_GROUP_UPDATEORDER() | ::MENU_GROUP_UPDATESTRUCTURE() ); + }; + + # Add the new indexes. + + $bestIndexGroup->MarkEndOfOriginal(); + my $isIndexGroup = $bestIndexGroup->Flags() & ::MENU_GROUP_ISINDEXGROUP(); + + foreach my $index (keys %validIndexes) + { + my $title; + + if ($isIndexGroup) + { + if ($index eq ::TOPIC_GENERAL()) + { $title = 'Everything'; } + else + { $title = NaturalDocs::Topics->NameOfType($index, 1); }; + } + else + { + $title = NaturalDocs::Topics->NameOfType($index) . ' Index'; + }; + + my $newEntry = NaturalDocs::Menu::Entry->New(::MENU_INDEX(), $title, $index, undef); + $bestIndexGroup->PushToGroup($newEntry); + + $indexes{$index} = 1; + }; + + $bestIndexGroup->SetFlags( $bestIndexGroup->Flags() | + ::MENU_GROUP_UPDATEORDER() | ::MENU_GROUP_UPDATESTRUCTURE() ); + $hasChanged = 1; + }; + }; + + +# +# Function: RemoveDeadGroups +# +# Removes groups with less than two entries. It will always remove empty groups, and it will remove groups with one entry if it +# has the <MENU_GROUP_UPDATESTRUCTURE> flag. +# +sub RemoveDeadGroups + { + my ($self) = @_; + + my $index = 0; + + while ($index < scalar @{$menu->GroupContent()}) + { + my $entry = $menu->GroupContent()->[$index]; + + if ($entry->Type() == ::MENU_GROUP()) + { + my $removed = $self->RemoveIfDead($entry, $menu, $index); + + if (!$removed) + { $index++; }; + } + else + { $index++; }; + }; + }; + + +# +# Function: RemoveIfDead +# +# Checks a group and all its sub-groups for life and remove any that are dead. Empty groups are removed, and groups with one +# entry and the <MENU_GROUP_UPDATESTRUCTURE> flag have their entry moved to the parent group. +# +# Parameters: +# +# groupEntry - The group to check for possible deletion. +# parentGroupEntry - The parent group to move the single entry to if necessary. +# parentGroupIndex - The index of the group in its parent. +# +# Returns: +# +# Whether the group was removed or not. +# +sub RemoveIfDead #(groupEntry, parentGroupEntry, parentGroupIndex) + { + my ($self, $groupEntry, $parentGroupEntry, $parentGroupIndex) = @_; + + + # Do all sub-groups first, since their deletions will affect our UPDATESTRUCTURE flag and content count. + + my $index = 0; + while ($index < scalar @{$groupEntry->GroupContent()}) + { + my $entry = $groupEntry->GroupContent()->[$index]; + + if ($entry->Type() == ::MENU_GROUP()) + { + my $removed = $self->RemoveIfDead($entry, $groupEntry, $index); + + if (!$removed) + { $index++; }; + } + else + { $index++; }; + }; + + + # Now check ourself. + + my $count = scalar @{$groupEntry->GroupContent()}; + if ($groupEntry->Flags() & ::MENU_GROUP_HASENDOFORIGINAL()) + { $count--; }; + + if ($count == 0) + { + $parentGroupEntry->DeleteFromGroup($parentGroupIndex); + + $parentGroupEntry->SetFlags( $parentGroupEntry->Flags() | ::MENU_GROUP_UPDATESTRUCTURE() ); + + $hasChanged = 1; + return 1; + } + elsif ($count == 1 && ($groupEntry->Flags() & ::MENU_GROUP_UPDATESTRUCTURE()) ) + { + my $onlyEntry = $groupEntry->GroupContent()->[0]; + if ($onlyEntry->Type() == ::MENU_ENDOFORIGINAL()) + { $onlyEntry = $groupEntry->GroupContent()->[1]; }; + + $parentGroupEntry->DeleteFromGroup($parentGroupIndex); + + $parentGroupEntry->MarkEndOfOriginal(); + $parentGroupEntry->PushToGroup($onlyEntry); + + $parentGroupEntry->SetFlags( $parentGroupEntry->Flags() | ::MENU_GROUP_UPDATETITLES() | + ::MENU_GROUP_UPDATEORDER() | ::MENU_GROUP_UPDATESTRUCTURE() ); + + $hasChanged = 1; + return 1; + } + else + { return undef; }; + }; + + +# +# Function: DetectIndexGroups +# +# Finds groups that are primarily used for indexes and gives them the <MENU_GROUP_ISINDEXGROUP> flag. +# +sub DetectIndexGroups + { + my ($self) = @_; + + my @groupStack = ( $menu ); + + while (scalar @groupStack) + { + my $groupEntry = pop @groupStack; + + my $isIndexGroup = -1; # -1: Can't tell yet. 0: Can't be an index group. 1: Is an index group so far. + + foreach my $entry (@{$groupEntry->GroupContent()}) + { + if ($entry->Type() == ::MENU_INDEX()) + { + if ($isIndexGroup == -1) + { $isIndexGroup = 1; }; + } + + # Text is tolerated, but it still needs at least one index entry. + elsif ($entry->Type() != ::MENU_TEXT()) + { + $isIndexGroup = 0; + + if ($entry->Type() == ::MENU_GROUP()) + { push @groupStack, $entry; }; + }; + }; + + if ($isIndexGroup == 1) + { + $groupEntry->SetFlags( $groupEntry->Flags() | ::MENU_GROUP_ISINDEXGROUP() ); + }; + }; + }; + + +# +# Function: CreateDirectorySubGroups +# +# Where possible, creates sub-groups based on directories for any long groups that have <MENU_GROUP_UPDATESTRUCTURE> +# set. Clears the flag afterwards on groups that are short enough to not need any more sub-groups, but leaves it for the rest. +# +sub CreateDirectorySubGroups + { + my ($self) = @_; + + my @groupStack = ( $menu ); + + foreach my $groupEntry (@groupStack) + { + if ($groupEntry->Flags() & ::MENU_GROUP_UPDATESTRUCTURE()) + { + # Count the number of files. + + my $fileCount = 0; + + foreach my $entry (@{$groupEntry->GroupContent()}) + { + if ($entry->Type() == ::MENU_FILE()) + { $fileCount++; }; + }; + + + if ($fileCount > MAXFILESINGROUP) + { + my @sharedDirectories = $self->SharedDirectoriesOf($groupEntry); + my $unsharedIndex = scalar @sharedDirectories; + + # The keys are the first directory entries after the shared ones, and the values are the number of files that are in + # that directory. Files that don't have subdirectories after the shared directories aren't included because they shouldn't + # be put in a subgroup. + my %directoryCounts; + + foreach my $entry (@{$groupEntry->GroupContent()}) + { + if ($entry->Type() == ::MENU_FILE()) + { + my @entryDirectories = NaturalDocs::File->SplitDirectories( (NaturalDocs::File->SplitPath($entry->Target()))[1] ); + + if (scalar @entryDirectories > $unsharedIndex) + { + my $unsharedDirectory = $entryDirectories[$unsharedIndex]; + + if (!exists $directoryCounts{$unsharedDirectory}) + { $directoryCounts{$unsharedDirectory} = 1; } + else + { $directoryCounts{$unsharedDirectory}++; }; + }; + }; + }; + + + # Now create the subgroups. + + # The keys are the first directory entries after the shared ones, and the values are the groups for those files to be + # put in. There will only be entries for the groups with at least MINFILESINNEWGROUP files. + my %directoryGroups; + + while (my ($directory, $count) = each %directoryCounts) + { + if ($count >= MINFILESINNEWGROUP) + { + my $newGroup = NaturalDocs::Menu::Entry->New( ::MENU_GROUP(), ucfirst($directory), undef, + ::MENU_GROUP_UPDATETITLES() | + ::MENU_GROUP_UPDATEORDER() ); + + if ($count > MAXFILESINGROUP) + { $newGroup->SetFlags( $newGroup->Flags() | ::MENU_GROUP_UPDATESTRUCTURE()); }; + + $groupEntry->MarkEndOfOriginal(); + push @{$groupEntry->GroupContent()}, $newGroup; + + $directoryGroups{$directory} = $newGroup; + $fileCount -= $count; + }; + }; + + + # Now fill the subgroups. + + if (scalar keys %directoryGroups) + { + my $afterOriginal; + my $index = 0; + + while ($index < scalar @{$groupEntry->GroupContent()}) + { + my $entry = $groupEntry->GroupContent()->[$index]; + + if ($entry->Type() == ::MENU_FILE()) + { + my @entryDirectories = + NaturalDocs::File->SplitDirectories( (NaturalDocs::File->SplitPath($entry->Target()))[1] ); + + my $unsharedDirectory = $entryDirectories[$unsharedIndex]; + + if (exists $directoryGroups{$unsharedDirectory}) + { + my $targetGroup = $directoryGroups{$unsharedDirectory}; + + if ($afterOriginal) + { $targetGroup->MarkEndOfOriginal(); }; + $targetGroup->PushToGroup($entry); + + $groupEntry->DeleteFromGroup($index); + } + else + { $index++; }; + } + + elsif ($entry->Type() == ::MENU_ENDOFORIGINAL()) + { + $afterOriginal = 1; + $index++; + } + + elsif ($entry->Type() == ::MENU_GROUP()) + { + # See if we need to relocate this group. + + my @groupDirectories = $self->SharedDirectoriesOf($entry); + + # The group's shared directories must be at least two levels deeper than the current. If the first level deeper + # is a new group, move it there because it's a subdirectory of that one. + if (scalar @groupDirectories - scalar @sharedDirectories >= 2) + { + my $unsharedDirectory = $groupDirectories[$unsharedIndex]; + + if (exists $directoryGroups{$unsharedDirectory} && + $directoryGroups{$unsharedDirectory} != $entry) + { + my $targetGroup = $directoryGroups{$unsharedDirectory}; + + if ($afterOriginal) + { $targetGroup->MarkEndOfOriginal(); }; + $targetGroup->PushToGroup($entry); + + $groupEntry->DeleteFromGroup($index); + + # We need to retitle the group if it has the name of the unshared directory. + + my $oldTitle = $entry->Title(); + $oldTitle =~ s/ +//g; + $unsharedDirectory =~ s/ +//g; + + if (lc($oldTitle) eq lc($unsharedDirectory)) + { + $entry->SetTitle($groupDirectories[$unsharedIndex + 1]); + }; + } + else + { $index++; }; + } + else + { $index++; }; + } + + else + { $index++; }; + }; + + $hasChanged = 1; + + if ($fileCount <= MAXFILESINGROUP) + { $groupEntry->SetFlags( $groupEntry->Flags() & ~::MENU_GROUP_UPDATESTRUCTURE() ); }; + + $groupEntry->SetFlags( $groupEntry->Flags() | ::MENU_GROUP_UPDATETITLES() | + ::MENU_GROUP_UPDATEORDER() ); + }; + + }; # If group has >MAXFILESINGROUP files + }; # If group has UPDATESTRUCTURE + + + # Okay, now go through all the subgroups. We do this after the above so that newly created groups can get subgrouped + # further. + + foreach my $entry (@{$groupEntry->GroupContent()}) + { + if ($entry->Type() == ::MENU_GROUP()) + { push @groupStack, $entry; }; + }; + + }; # For each group entry + }; + + +# +# Function: DetectOrder +# +# Detects the order of the entries in all groups that have the <MENU_GROUP_UPDATEORDER> flag set. Will set one of the +# <MENU_GROUP_FILESSORTED>, <MENU_GROUP_FILESANDGROUPSSORTED>, <MENU_GROUP_EVERYTHINGSORTED>, or +# <MENU_GROUP_UNSORTED> flags. It will always go for the most comprehensive sort possible, so if a group only has one +# entry, it will be flagged as <MENU_GROUP_EVERYTHINGSORTED>. +# +# <DetectIndexGroups()> should be called beforehand, as the <MENU_GROUP_ISINDEXGROUP> flag affects how the order is +# detected. +# +# The sort detection stops if it reaches a <MENU_ENDOFORIGINAL> entry, so new entries can be added to the end while still +# allowing the original sort to be detected. +# +# Parameters: +# +# forceAll - If set, the order will be detected for all groups regardless of whether <MENU_GROUP_UPDATEORDER> is set. +# +sub DetectOrder #(forceAll) + { + my ($self, $forceAll) = @_; + my @groupStack = ( $menu ); + + while (scalar @groupStack) + { + my $groupEntry = pop @groupStack; + my $index = 0; + + + # First detect the sort. + + if ($forceAll || ($groupEntry->Flags() & ::MENU_GROUP_UPDATEORDER()) ) + { + my $order = ::MENU_GROUP_EVERYTHINGSORTED(); + + my $lastFile; + my $lastFileOrGroup; + + while ($index < scalar @{$groupEntry->GroupContent()} && + $groupEntry->GroupContent()->[$index]->Type() != ::MENU_ENDOFORIGINAL() && + $order != ::MENU_GROUP_UNSORTED()) + { + my $entry = $groupEntry->GroupContent()->[$index]; + + + # Ignore the last entry if it's an index group. We don't want it to affect the sort. + + if ($index + 1 == scalar @{$groupEntry->GroupContent()} && + $entry->Type() == ::MENU_GROUP() && ($entry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) ) + { + # Ignore. + + # This is an awkward code construct, basically working towards an else instead of using an if, but the code just gets + # too hard to read otherwise. The compiled code should work out to roughly the same thing anyway. + } + + + # Ignore the first entry if it's the general index in an index group. We don't want it to affect the sort. + + elsif ($index == 0 && ($groupEntry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) && + $entry->Type() == ::MENU_INDEX() && $entry->Target() eq ::TOPIC_GENERAL() ) + { + # Ignore. + } + + + # Degenerate the sort. + + else + { + + if ($order == ::MENU_GROUP_EVERYTHINGSORTED() && $index > 0 && + ::StringCompare($entry->Title(), $groupEntry->GroupContent()->[$index - 1]->Title()) < 0) + { $order = ::MENU_GROUP_FILESANDGROUPSSORTED(); }; + + if ($order == ::MENU_GROUP_FILESANDGROUPSSORTED() && + ($entry->Type() == ::MENU_FILE() || $entry->Type() == ::MENU_GROUP()) && + defined $lastFileOrGroup && ::StringCompare($entry->Title(), $lastFileOrGroup->Title()) < 0) + { $order = ::MENU_GROUP_FILESSORTED(); }; + + if ($order == ::MENU_GROUP_FILESSORTED() && + $entry->Type() == ::MENU_FILE() && defined $lastFile && + ::StringCompare($entry->Title(), $lastFile->Title()) < 0) + { $order = ::MENU_GROUP_UNSORTED(); }; + + }; + + + # Set the lastX parameters for comparison and add sub-groups to the stack. + + if ($entry->Type() == ::MENU_FILE()) + { + $lastFile = $entry; + $lastFileOrGroup = $entry; + } + elsif ($entry->Type() == ::MENU_GROUP()) + { + $lastFileOrGroup = $entry; + push @groupStack, $entry; + }; + + $index++; + }; + + $groupEntry->SetFlags($groupEntry->Flags() | $order); + }; + + + # Find any subgroups in the remaining entries. + + while ($index < scalar @{$groupEntry->GroupContent()}) + { + my $entry = $groupEntry->GroupContent()->[$index]; + + if ($entry->Type() == ::MENU_GROUP()) + { push @groupStack, $entry; }; + + $index++; + }; + }; + }; + + +# +# Function: GenerateAutoFileTitles +# +# Creates titles for the unlocked file entries in all groups that have the <MENU_GROUP_UPDATETITLES> flag set. It clears the +# flag afterwards so it can be used efficiently for multiple sweeps. +# +# Parameters: +# +# forceAll - If set, forces all the unlocked file titles to update regardless of whether the group has the +# <MENU_GROUP_UPDATETITLES> flag set. +# +sub GenerateAutoFileTitles #(forceAll) + { + my ($self, $forceAll) = @_; + + my @groupStack = ( $menu ); + + while (scalar @groupStack) + { + my $groupEntry = pop @groupStack; + + if ($forceAll || ($groupEntry->Flags() & ::MENU_GROUP_UPDATETITLES()) ) + { + # Find common prefixes and paths to strip from the default menu titles. + + my @sharedDirectories = $self->SharedDirectoriesOf($groupEntry); + my $noSharedDirectories = (scalar @sharedDirectories == 0); + + my @sharedPrefixes; + my $noSharedPrefixes; + + foreach my $entry (@{$groupEntry->GroupContent()}) + { + if ($entry->Type() == ::MENU_FILE()) + { + # Find the common prefixes among all file entries that are unlocked and don't use the file name as their default title. + + my $defaultTitle = NaturalDocs::Project->DefaultMenuTitleOf($entry->Target()); + + if (!$noSharedPrefixes && ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0 && + $defaultTitle ne $entry->Target()) + { + # If the filename is part of the title, separate it off so no part of it gets included as a common prefix. This would + # happen if there's a group with only one file in it (Project.h => h) or only files that differ by extension + # (Project.h, Project.cpp => h, cpp) and people labeled them manually (// File: Project.h). + my $filename = (NaturalDocs::File->SplitPath($entry->Target()))[2]; + my $filenamePart; + + if ( length $defaultTitle >= length $filename && + lc(substr($defaultTitle, 0 - length($filename))) eq lc($filename) ) + { + $filenamePart = substr($defaultTitle, 0 - length($filename)); + $defaultTitle = substr($defaultTitle, 0, 0 - length($filename)); + }; + + + my @entryPrefixes = split(/(\.|::|->)/, $defaultTitle); + + # Remove potential leading undef/empty string. + if (!length $entryPrefixes[0]) + { shift @entryPrefixes; }; + + # Remove last entry. Something has to exist for the title. If we already separated off the filename, that will be + # it instead. + if (!$filenamePart) + { pop @entryPrefixes; }; + + if (!scalar @entryPrefixes) + { $noSharedPrefixes = 1; } + elsif (!scalar @sharedPrefixes) + { @sharedPrefixes = @entryPrefixes; } + elsif ($entryPrefixes[0] ne $sharedPrefixes[0]) + { $noSharedPrefixes = 1; } + + # If both arrays have entries, and the first is shared... + else + { + my $index = 1; + + while ($index < scalar @sharedPrefixes && $entryPrefixes[$index] eq $sharedPrefixes[$index]) + { $index++; }; + + if ($index < scalar @sharedPrefixes) + { splice(@sharedPrefixes, $index); }; + }; + }; + + }; # if entry is MENU_FILE + }; # foreach entry in group content. + + + if (!scalar @sharedPrefixes) + { $noSharedPrefixes = 1; }; + + + # Update all the menu titles of unlocked file entries. + + foreach my $entry (@{$groupEntry->GroupContent()}) + { + if ($entry->Type() == ::MENU_FILE() && ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0) + { + my $title = NaturalDocs::Project->DefaultMenuTitleOf($entry->Target()); + + if ($title eq $entry->Target()) + { + my ($volume, $directoryString, $file) = NaturalDocs::File->SplitPath($entry->Target()); + my @directories = NaturalDocs::File->SplitDirectories($directoryString); + + if (!$noSharedDirectories) + { splice(@directories, 0, scalar @sharedDirectories); }; + + # directory\...\directory\file.ext + + if (scalar @directories > 2) + { @directories = ( $directories[0], '...', $directories[-1] ); }; + + $directoryString = NaturalDocs::File->JoinDirectories(@directories); + $title = NaturalDocs::File->JoinPaths($directoryString, $file); + } + + else + { + my $filename = (NaturalDocs::File->SplitPath($entry->Target()))[2]; + my $filenamePart; + + if ( length $title >= length $filename && + lc(substr($title, 0 - length($filename))) eq lc($filename) ) + { + $filenamePart = substr($title, 0 - length($filename)); + $title = substr($title, 0, 0 - length($filename)); + }; + + my @segments = split(/(::|\.|->)/, $title); + if (!length $segments[0]) + { shift @segments; }; + + if ($filenamePart) + { push @segments, $filenamePart; }; + + if (!$noSharedPrefixes) + { splice(@segments, 0, scalar @sharedPrefixes); }; + + # package...package::target + + if (scalar @segments > 5) + { splice(@segments, 1, scalar @segments - 4, '...'); }; + + $title = join('', @segments); + }; + + $entry->SetTitle($title); + }; # If entry is an unlocked file + }; # Foreach entry + + $groupEntry->SetFlags( $groupEntry->Flags() & ~::MENU_GROUP_UPDATETITLES() ); + + }; # If updating group titles + + # Now find any subgroups. + foreach my $entry (@{$groupEntry->GroupContent()}) + { + if ($entry->Type() == ::MENU_GROUP()) + { push @groupStack, $entry; }; + }; + }; + + }; + + +# +# Function: ResortGroups +# +# Resorts all groups that have <MENU_GROUP_UPDATEORDER> set. Assumes <DetectOrder()> and <GenerateAutoFileTitles()> +# have already been called. Will clear the flag and any <MENU_ENDOFORIGINAL> entries on reordered groups. +# +# Parameters: +# +# forceAll - If set, resorts all groups regardless of whether <MENU_GROUP_UPDATEORDER> is set. +# +sub ResortGroups #(forceAll) + { + my ($self, $forceAll) = @_; + my @groupStack = ( $menu ); + + while (scalar @groupStack) + { + my $groupEntry = pop @groupStack; + + if ($forceAll || ($groupEntry->Flags() & ::MENU_GROUP_UPDATEORDER()) ) + { + my $newEntriesIndex; + + + # Strip the ENDOFORIGINAL. + + if ($groupEntry->Flags() & ::MENU_GROUP_HASENDOFORIGINAL()) + { + $newEntriesIndex = 0; + + while ($newEntriesIndex < scalar @{$groupEntry->GroupContent()} && + $groupEntry->GroupContent()->[$newEntriesIndex]->Type() != ::MENU_ENDOFORIGINAL() ) + { $newEntriesIndex++; }; + + $groupEntry->DeleteFromGroup($newEntriesIndex); + + $groupEntry->SetFlags( $groupEntry->Flags() & ~::MENU_GROUP_HASENDOFORIGINAL() ); + } + else + { $newEntriesIndex = -1; }; + + + # Strip the exceptions. + + my $trailingIndexGroup; + my $leadingGeneralIndex; + + if ( ($groupEntry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) && + $groupEntry->GroupContent()->[0]->Type() == ::MENU_INDEX() && + $groupEntry->GroupContent()->[0]->Target() eq ::TOPIC_GENERAL() ) + { + $leadingGeneralIndex = shift @{$groupEntry->GroupContent()}; + if ($newEntriesIndex != -1) + { $newEntriesIndex--; }; + } + + elsif (scalar @{$groupEntry->GroupContent()} && $newEntriesIndex != 0) + { + my $lastIndex; + + if ($newEntriesIndex != -1) + { $lastIndex = $newEntriesIndex - 1; } + else + { $lastIndex = scalar @{$groupEntry->GroupContent()} - 1; }; + + if ($groupEntry->GroupContent()->[$lastIndex]->Type() == ::MENU_GROUP() && + ( $groupEntry->GroupContent()->[$lastIndex]->Flags() & ::MENU_GROUP_ISINDEXGROUP() ) ) + { + $trailingIndexGroup = $groupEntry->GroupContent()->[$lastIndex]; + $groupEntry->DeleteFromGroup($lastIndex); + + if ($newEntriesIndex != -1) + { $newEntriesIndex++; }; + }; + }; + + + # If there weren't already exceptions, strip them from the new entries. + + if ( (!defined $trailingIndexGroup || !defined $leadingGeneralIndex) && $newEntriesIndex != -1) + { + my $index = $newEntriesIndex; + + while ($index < scalar @{$groupEntry->GroupContent()}) + { + my $entry = $groupEntry->GroupContent()->[$index]; + + if (!defined $trailingIndexGroup && + $entry->Type() == ::MENU_GROUP() && ($entry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) ) + { + $trailingIndexGroup = $entry; + $groupEntry->DeleteFromGroup($index); + } + elsif (!defined $leadingGeneralIndex && ($groupEntry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) && + $entry->Type() == ::MENU_INDEX() && !defined $entry->Target()) + { + $leadingGeneralIndex = $entry; + $groupEntry->DeleteFromGroup($index); + } + else + { $index++; }; + }; + }; + + + # If there's no order, we still want to sort the new additions. + + if ($groupEntry->Flags() & ::MENU_GROUP_UNSORTED()) + { + if ($newEntriesIndex != -1) + { + my @newEntries = + @{$groupEntry->GroupContent()}[$newEntriesIndex..scalar @{$groupEntry->GroupContent()} - 1]; + + @newEntries = sort { $self->CompareEntries($a, $b) } @newEntries; + + foreach my $newEntry (@newEntries) + { + $groupEntry->GroupContent()->[$newEntriesIndex] = $newEntry; + $newEntriesIndex++; + }; + }; + } + + elsif ($groupEntry->Flags() & ::MENU_GROUP_EVERYTHINGSORTED()) + { + @{$groupEntry->GroupContent()} = sort { $self->CompareEntries($a, $b) } @{$groupEntry->GroupContent()}; + } + + elsif ( ($groupEntry->Flags() & ::MENU_GROUP_FILESSORTED()) || + ($groupEntry->Flags() & ::MENU_GROUP_FILESANDGROUPSSORTED()) ) + { + my $groupContent = $groupEntry->GroupContent(); + my @newEntries; + + if ($newEntriesIndex != -1) + { @newEntries = splice( @$groupContent, $newEntriesIndex ); }; + + + # First resort the existing entries. + + # A couple of support functions. They're defined here instead of spun off into their own functions because they're only + # used here and to make them general we would need to add support for the other sort options. + + sub IsIncludedInSort #(groupEntry, entry) + { + my ($self, $groupEntry, $entry) = @_; + + return ($entry->Type() == ::MENU_FILE() || + ( $entry->Type() == ::MENU_GROUP() && + ($groupEntry->Flags() & ::MENU_GROUP_FILESANDGROUPSSORTED()) ) ); + }; + + sub IsSorted #(groupEntry) + { + my ($self, $groupEntry) = @_; + my $lastApplicable; + + foreach my $entry (@{$groupEntry->GroupContent()}) + { + # If the entry is applicable to the sort order... + if ($self->IsIncludedInSort($groupEntry, $entry)) + { + if (defined $lastApplicable) + { + if ($self->CompareEntries($entry, $lastApplicable) < 0) + { return undef; }; + }; + + $lastApplicable = $entry; + }; + }; + + return 1; + }; + + + # There's a good chance it's still sorted. They should only become unsorted if an auto-title changes. + if (!$self->IsSorted($groupEntry)) + { + # Crap. Okay, method one is to sort each group of continuous sortable elements. There's a possibility that doing + # this will cause the whole to become sorted again. We try this first, even though it isn't guaranteed to succeed, + # because it will restore the sort without moving any unsortable entries. + + # Copy it because we'll need the original if this fails. + my @originalGroupContent = @$groupContent; + + my $index = 0; + my $startSortable = 0; + + while (1) + { + # If index is on an unsortable entry or the end of the array... + if ($index == scalar @$groupContent || !$self->IsIncludedInSort($groupEntry, $groupContent->[$index])) + { + # If we have at least two sortable entries... + if ($index - $startSortable >= 2) + { + # Sort them. + my @sortableEntries = @{$groupContent}[$startSortable .. $index - 1]; + @sortableEntries = sort { $self->CompareEntries($a, $b) } @sortableEntries; + foreach my $sortableEntry (@sortableEntries) + { + $groupContent->[$startSortable] = $sortableEntry; + $startSortable++; + }; + }; + + if ($index == scalar @$groupContent) + { last; }; + + $startSortable = $index + 1; + }; + + $index++; + }; + + if (!$self->IsSorted($groupEntry)) + { + # Crap crap. Okay, now we do a full sort but with potential damage to the original structure. Each unsortable + # element is locked to the next sortable element. We sort the sortable elements, bringing all the unsortable + # pieces with them. + + my @pieces = ( [ ] ); + my $currentPiece = $pieces[0]; + + foreach my $entry (@originalGroupContent) + { + push @$currentPiece, $entry; + + # If the entry is sortable... + if ($self->IsIncludedInSort($groupEntry, $entry)) + { + $currentPiece = [ ]; + push @pieces, $currentPiece; + }; + }; + + my $lastUnsortablePiece; + + # If the last entry was sortable, we'll have an empty piece at the end. Drop it. + if (scalar @{$pieces[-1]} == 0) + { pop @pieces; } + + # If the last entry wasn't sortable, the last piece won't end with a sortable element. Save it, but remove it + # from the list. + else + { $lastUnsortablePiece = pop @pieces; }; + + # Sort the list. + @pieces = sort { $self->CompareEntries( $a->[-1], $b->[-1] ) } @pieces; + + # Copy it back to the original. + if (defined $lastUnsortablePiece) + { push @pieces, $lastUnsortablePiece; }; + + my $index = 0; + + foreach my $piece (@pieces) + { + foreach my $entry (@{$piece}) + { + $groupEntry->GroupContent()->[$index] = $entry; + $index++; + }; + }; + }; + }; + + + # Okay, the orginal entries are sorted now. Sort the new entries and apply. + + if (scalar @newEntries) + { + @newEntries = sort { $self->CompareEntries($a, $b) } @newEntries; + my @originalEntries = @$groupContent; + @$groupContent = ( ); + + while (1) + { + while (scalar @originalEntries && !$self->IsIncludedInSort($groupEntry, $originalEntries[0])) + { push @$groupContent, (shift @originalEntries); }; + + if (!scalar @originalEntries || !scalar @newEntries) + { last; }; + + while (scalar @newEntries && $self->CompareEntries($newEntries[0], $originalEntries[0]) < 0) + { push @$groupContent, (shift @newEntries); }; + + push @$groupContent, (shift @originalEntries); + + if (!scalar @originalEntries || !scalar @newEntries) + { last; }; + }; + + if (scalar @originalEntries) + { push @$groupContent, @originalEntries; } + elsif (scalar @newEntries) + { push @$groupContent, @newEntries; }; + }; + }; + + + # Now re-add the exceptions. + + if (defined $leadingGeneralIndex) + { + unshift @{$groupEntry->GroupContent()}, $leadingGeneralIndex; + }; + + if (defined $trailingIndexGroup) + { + $groupEntry->PushToGroup($trailingIndexGroup); + }; + + }; + + foreach my $entry (@{$groupEntry->GroupContent()}) + { + if ($entry->Type() == ::MENU_GROUP()) + { push @groupStack, $entry; }; + }; + }; + }; + + +# +# Function: CompareEntries +# +# A comparison function for use in sorting. Compares the two entries by their titles with <StringCompare()>, but in the case +# of a tie, puts <MENU_FILE> entries above <MENU_GROUP> entries. +# +sub CompareEntries #(a, b) + { + my ($self, $a, $b) = @_; + + my $result = ::StringCompare($a->Title(), $b->Title()); + + if ($result == 0) + { + if ($a->Type() == ::MENU_FILE() && $b->Type() == ::MENU_GROUP()) + { $result = -1; } + elsif ($a->Type() == ::MENU_GROUP() && $b->Type() == ::MENU_FILE()) + { $result = 1; }; + }; + + return $result; + }; + + +# +# Function: SharedDirectoriesOf +# +# Returns an array of all the directories shared by the files in the group. If none, returns an empty array. +# +sub SharedDirectoriesOf #(group) + { + my ($self, $groupEntry) = @_; + my @sharedDirectories; + + foreach my $entry (@{$groupEntry->GroupContent()}) + { + if ($entry->Type() == ::MENU_FILE()) + { + my @entryDirectories = NaturalDocs::File->SplitDirectories( (NaturalDocs::File->SplitPath($entry->Target()))[1] ); + + if (!scalar @sharedDirectories) + { @sharedDirectories = @entryDirectories; } + else + { ::ShortenToMatchStrings(\@sharedDirectories, \@entryDirectories); }; + + if (!scalar @sharedDirectories) + { last; }; + }; + }; + + return @sharedDirectories; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Menu/Entry.pm b/docs/tool/Modules/NaturalDocs/Menu/Entry.pm new file mode 100644 index 00000000..58989cca --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Menu/Entry.pm @@ -0,0 +1,201 @@ +############################################################################### +# +# Package: NaturalDocs::Menu::Entry +# +############################################################################### +# +# A class representing an entry in the menu. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Menu::Entry; + + +############################################################################### +# Group: Implementation + +# +# Constants: Members +# +# The object is implemented as a blessed arrayref with the indexes below. +# +# TYPE - The <MenuEntryType> +# TITLE - The title of the entry. +# TARGET - The target of the entry. If the type is <MENU_FILE>, it will be the source <FileName>. If the type is +# <MENU_LINK>, it will be the URL. If the type is <MENU_GROUP>, it will be an arrayref of +# <NaturalDocs::Menu::Entry> objects representing the group's content. If the type is <MENU_INDEX>, it will be +# a <TopicType>. +# FLAGS - Any <Menu Entry Flags> that apply. +# +use constant TYPE => 0; +use constant TITLE => 1; +use constant TARGET => 2; +use constant FLAGS => 3; +# DEPENDENCY: New() depends on the order of these constants. + + +############################################################################### +# Group: Functions + +# +# Function: New +# +# Creates and returns a new object. +# +# Parameters: +# +# type - The <MenuEntryType>. +# title - The title of the entry. +# target - The target of the entry, if applicable. If the type is <MENU_FILE>, use the source <FileName>. If the type is +# <MENU_LINK>, use the URL. If the type is <MENU_INDEX>, use the <TopicType>. Otherwise set it to undef. +# flags - Any <Menu Entry Flags> that apply. +# +sub New #(type, title, target, flags) + { + # DEPENDENCY: This gode depends on the order of the constants. + + my $package = shift; + + my $object = [ @_ ]; + bless $object, $package; + + if ($object->[TYPE] == ::MENU_GROUP()) + { $object->[TARGET] = [ ]; }; + if (!defined $object->[FLAGS]) + { $object->[FLAGS] = 0; }; + + return $object; + }; + + +# Function: Type +# Returns the <MenuEntryType>. +sub Type + { return $_[0]->[TYPE]; }; + +# Function: Title +# Returns the title of the entry. +sub Title + { return $_[0]->[TITLE]; }; + +# Function: SetTitle +# Replaces the entry's title. +sub SetTitle #(title) + { $_[0]->[TITLE] = $_[1]; }; + +# +# Function: Target +# +# Returns the target of the entry, if applicable. If the type is <MENU_FILE>, it returns the source <FileName>. If the type is +# <MENU_LINK>, it returns the URL. If the type is <MENU_INDEX>, it returns the <TopicType>. Otherwise it returns undef. +# +sub Target + { + my $self = shift; + + # Group entries are the only time when target won't be undef when it should be. + if ($self->Type() == ::MENU_GROUP()) + { return undef; } + else + { return $self->[TARGET]; }; + }; + +# Function: SetTarget +# Replaces the entry's target. +sub SetTarget #(target) + { $_[0]->[TARGET] = $_[1]; }; + +# Function: Flags +# Returns the <Menu Entry Flags>. +sub Flags + { return $_[0]->[FLAGS]; }; + +# Function: SetFlags +# Replaces the <Menu Entry Flags>. +sub SetFlags #(flags) + { $_[0]->[FLAGS] = $_[1]; }; + + + +############################################################################### +# Group: Group Functions +# +# All of these functions assume the type is <MENU_GROUP>. Do *not* call any of these without checking <Type()> first. + + +# +# Function: GroupContent +# +# Returns an arrayref of <NaturalDocs::Menu::Entry> objects representing the contents of the +# group, or undef otherwise. This arrayref will always exist for <MENU_GROUP>'s and can be changed. +# +sub GroupContent + { + return $_[0]->[TARGET]; + }; + + +# +# Function: GroupIsEmpty +# +# If the type is <MENU_GROUP>, returns whether the group is empty. +# +sub GroupIsEmpty + { + my $self = shift; + return (scalar @{$self->GroupContent()} > 0); + }; + + +# +# Function: PushToGroup +# +# Pushes the entry to the end of the group content. +# +sub PushToGroup #(entry) + { + my ($self, $entry) = @_; + push @{$self->GroupContent()}, $entry; + }; + + +# +# Function: DeleteFromGroup +# +# Deletes an entry from the group content by index. +# +sub DeleteFromGroup #(index) + { + my ($self, $index) = @_; + + my $groupContent = $self->GroupContent(); + + splice( @$groupContent, $index, 1 ); + }; + + +# +# Function: MarkEndOfOriginal +# +# If the group doesn't already have one, adds a <MENU_ENDOFORIGINAL> entry to the end and sets the +# <MENU_GROUP_HASENDOFORIGINAL> flag. +# +sub MarkEndOfOriginal + { + my $self = shift; + + if (($self->Flags() & ::MENU_GROUP_HASENDOFORIGINAL()) == 0) + { + $self->PushToGroup( NaturalDocs::Menu::Entry->New(::MENU_ENDOFORIGINAL(), undef, undef, undef) ); + $self->SetFlags( $self->Flags() | ::MENU_GROUP_HASENDOFORIGINAL() ); + }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/NDMarkup.pm b/docs/tool/Modules/NaturalDocs/NDMarkup.pm new file mode 100644 index 00000000..d394d2c7 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/NDMarkup.pm @@ -0,0 +1,76 @@ +############################################################################### +# +# Package: NaturalDocs::NDMarkup +# +############################################################################### +# +# A package of support functions for dealing with <NDMarkup>. +# +# Usage and Dependencies: +# +# The package doesn't depend on any Natural Docs packages and is ready to use right away. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + + +use strict; +use integer; + +package NaturalDocs::NDMarkup; + +# +# Function: ConvertAmpChars +# +# Substitutes certain characters with their <NDMarkup> amp chars. +# +# Parameters: +# +# text - The block of text to convert. +# +# Returns: +# +# The converted text block. +# +sub ConvertAmpChars #(text) + { + my ($self, $text) = @_; + + $text =~ s/&/&/g; + $text =~ s/</</g; + $text =~ s/>/>/g; + $text =~ s/\"/"/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/"/\"/g; + $text =~ s/>/>/g; + $text =~ s/</</g; + $text =~ s/&/&/g; + + return $text; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Parser.pm b/docs/tool/Modules/NaturalDocs/Parser.pm new file mode 100644 index 00000000..e88cd289 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Parser.pm @@ -0,0 +1,1331 @@ +############################################################################### +# +# Package: NaturalDocs::Parser +# +############################################################################### +# +# A package that coordinates source file parsing between the <NaturalDocs::Languages::Base>-derived objects and its own +# sub-packages such as <NaturalDocs::Parser::Native>. Also handles sending symbols to <NaturalDocs::SymbolTable> and +# other generic topic processing. +# +# Usage and Dependencies: +# +# - Prior to use, <NaturalDocs::Settings>, <NaturalDocs::Languages>, <NaturalDocs::Project>, <NaturalDocs::SymbolTable>, +# and <NaturalDocs::ClassHierarchy> must be initialized. <NaturalDocs::SymbolTable> and <NaturalDocs::ClassHierarchy> +# do not have to be fully resolved. +# +# - Aside from that, the package is ready to use right away. It does not have its own initialization function. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use NaturalDocs::Parser::ParsedTopic; +use NaturalDocs::Parser::Native; +use NaturalDocs::Parser::JavaDoc; + +use strict; +use integer; + +package NaturalDocs::Parser; + + + +############################################################################### +# Group: Variables + + +# +# var: sourceFile +# +# The source <FileName> currently being parsed. +# +my $sourceFile; + +# +# var: language +# +# The language object for the file, derived from <NaturalDocs::Languages::Base>. +# +my $language; + +# +# Array: parsedFile +# +# An array of <NaturalDocs::Parser::ParsedTopic> objects. +# +my @parsedFile; + + +# +# bool: parsingForInformation +# Whether <ParseForInformation()> was called. If false, then <ParseForBuild()> was called. +# +my $parsingForInformation; + + + +############################################################################### +# Group: Functions + +# +# Function: ParseForInformation +# +# Parses the input file for information. Will update the information about the file in <NaturalDocs::SymbolTable> and +# <NaturalDocs::Project>. +# +# Parameters: +# +# file - The <FileName> to parse. +# +sub ParseForInformation #(file) + { + my ($self, $file) = @_; + $sourceFile = $file; + + $parsingForInformation = 1; + + # Watch this parse so we detect any changes. + NaturalDocs::SymbolTable->WatchFileForChanges($sourceFile); + NaturalDocs::ClassHierarchy->WatchFileForChanges($sourceFile); + NaturalDocs::SourceDB->WatchFileForChanges($sourceFile); + + my $defaultMenuTitle = $self->Parse(); + + foreach my $topic (@parsedFile) + { + # Add a symbol for the topic. + + my $type = $topic->Type(); + if ($type eq ::TOPIC_ENUMERATION()) + { $type = ::TOPIC_TYPE(); }; + + NaturalDocs::SymbolTable->AddSymbol($topic->Symbol(), $sourceFile, $type, + $topic->Prototype(), $topic->Summary()); + + + # You can't put the function call directly in a while with a regex. It has to sit in a variable to work. + my $body = $topic->Body(); + + + # If it's a list or enum topic, add a symbol for each description list entry. + + if ($topic->IsList() || $topic->Type() eq ::TOPIC_ENUMERATION()) + { + # We'll hijack the enum constants to apply to non-enum behavior too. + my $behavior; + + if ($topic->Type() eq ::TOPIC_ENUMERATION()) + { + $type = ::TOPIC_CONSTANT(); + $behavior = $language->EnumValues(); + } + elsif (NaturalDocs::Topics->TypeInfo($topic->Type())->Scope() == ::SCOPE_ALWAYS_GLOBAL()) + { + $behavior = ::ENUM_GLOBAL(); + } + else + { + $behavior = ::ENUM_UNDER_PARENT(); + }; + + while ($body =~ /<ds>([^<]+)<\/ds><dd>(.*?)<\/dd>/g) + { + my ($listTextSymbol, $listSummary) = ($1, $2); + + $listTextSymbol = NaturalDocs::NDMarkup->RestoreAmpChars($listTextSymbol); + my $listSymbol = NaturalDocs::SymbolString->FromText($listTextSymbol); + + if ($behavior == ::ENUM_UNDER_PARENT()) + { $listSymbol = NaturalDocs::SymbolString->Join($topic->Package(), $listSymbol); } + elsif ($behavior == ::ENUM_UNDER_TYPE()) + { $listSymbol = NaturalDocs::SymbolString->Join($topic->Symbol(), $listSymbol); }; + + NaturalDocs::SymbolTable->AddSymbol($listSymbol, $sourceFile, $type, undef, + $self->GetSummaryFromDescriptionList($listSummary)); + }; + }; + + + # Add references in the topic. + + while ($body =~ /<link target=\"([^\"]*)\" name=\"[^\"]*\" original=\"[^\"]*\">/g) + { + my $linkText = NaturalDocs::NDMarkup->RestoreAmpChars($1); + my $linkSymbol = NaturalDocs::SymbolString->FromText($linkText); + + NaturalDocs::SymbolTable->AddReference(::REFERENCE_TEXT(), $linkSymbol, + $topic->Package(), $topic->Using(), $sourceFile); + }; + + + # Add images in the topic. + + while ($body =~ /<img mode=\"[^\"]*\" target=\"([^\"]+)\" original=\"[^\"]*\">/g) + { + my $target = NaturalDocs::NDMarkup->RestoreAmpChars($1); + NaturalDocs::ImageReferenceTable->AddReference($sourceFile, $target); + }; + }; + + # Handle any changes to the file. + NaturalDocs::ClassHierarchy->AnalyzeChanges(); + NaturalDocs::SymbolTable->AnalyzeChanges(); + NaturalDocs::SourceDB->AnalyzeWatchedFileChanges(); + + # Update project on the file's characteristics. + my $hasContent = (scalar @parsedFile > 0); + + NaturalDocs::Project->SetHasContent($sourceFile, $hasContent); + if ($hasContent) + { NaturalDocs::Project->SetDefaultMenuTitle($sourceFile, $defaultMenuTitle); }; + + # We don't need to keep this around. + @parsedFile = ( ); + }; + + +# +# Function: ParseForBuild +# +# Parses the input file for building, returning it as a <NaturalDocs::Parser::ParsedTopic> arrayref. +# +# Note that all new and changed files should be parsed for symbols via <ParseForInformation()> before calling this function on +# *any* file. The reason is that <NaturalDocs::SymbolTable> needs to know about all the symbol definitions and references to +# resolve them properly. +# +# Parameters: +# +# file - The <FileName> to parse for building. +# +# Returns: +# +# An arrayref of the source file as <NaturalDocs::Parser::ParsedTopic> objects. +# +sub ParseForBuild #(file) + { + my ($self, $file) = @_; + $sourceFile = $file; + + $parsingForInformation = undef; + + $self->Parse(); + + return \@parsedFile; + }; + + + + +############################################################################### +# Group: Interface Functions + + +# +# Function: OnComment +# +# The function called by <NaturalDocs::Languages::Base>-derived objects when their parsers encounter a comment +# suitable for documentation. +# +# Parameters: +# +# commentLines - An arrayref of the comment's lines. The language's comment symbols should be converted to spaces, +# and there should be no line break characters at the end of each line. *The original memory will be +# changed.* +# lineNumber - The line number of the first of the comment lines. +# isJavaDoc - Whether the comment is in JavaDoc format. +# +# Returns: +# +# The number of topics created by this comment, or zero if none. +# +sub OnComment #(string[] commentLines, int lineNumber, bool isJavaDoc) + { + my ($self, $commentLines, $lineNumber, $isJavaDoc) = @_; + + $self->CleanComment($commentLines); + + # We check if it's definitely Natural Docs content first. This overrides all else, since it's possible that a comment could start + # with a topic line yet have something that looks like a JavaDoc tag. Natural Docs wins in this case. + if (NaturalDocs::Parser::Native->IsMine($commentLines, $isJavaDoc)) + { return NaturalDocs::Parser::Native->ParseComment($commentLines, $isJavaDoc, $lineNumber, \@parsedFile); } + + elsif (NaturalDocs::Parser::JavaDoc->IsMine($commentLines, $isJavaDoc)) + { return NaturalDocs::Parser::JavaDoc->ParseComment($commentLines, $isJavaDoc, $lineNumber, \@parsedFile); } + + # If the content is ambiguous and it's a JavaDoc-styled comment, treat it as Natural Docs content. + elsif ($isJavaDoc) + { return NaturalDocs::Parser::Native->ParseComment($commentLines, $isJavaDoc, $lineNumber, \@parsedFile); } + }; + + +# +# Function: OnClass +# +# A function called by <NaturalDocs::Languages::Base>-derived objects when their parsers encounter a class declaration. +# +# Parameters: +# +# class - The <SymbolString> of the class encountered. +# +sub OnClass #(class) + { + my ($self, $class) = @_; + + if ($parsingForInformation) + { NaturalDocs::ClassHierarchy->AddClass($sourceFile, $class); }; + }; + + +# +# Function: OnClassParent +# +# A function called by <NaturalDocs::Languages::Base>-derived objects when their parsers encounter a declaration of +# inheritance. +# +# Parameters: +# +# class - The <SymbolString> of the class we're in. +# parent - The <SymbolString> of the class it inherits. +# scope - The package <SymbolString> that the reference appeared in. +# using - An arrayref of package <SymbolStrings> that the reference has access to via "using" statements. +# resolvingFlags - Any <Resolving Flags> to be used when resolving the reference. <RESOLVE_NOPLURAL> is added +# automatically since that would never apply to source code. +# +sub OnClassParent #(class, parent, scope, using, resolvingFlags) + { + my ($self, $class, $parent, $scope, $using, $resolvingFlags) = @_; + + if ($parsingForInformation) + { + NaturalDocs::ClassHierarchy->AddParentReference($sourceFile, $class, $parent, $scope, $using, + $resolvingFlags | ::RESOLVE_NOPLURAL()); + }; + }; + + + +############################################################################### +# Group: Support Functions + + +# Function: Parse +# +# Opens the source file and parses process. Most of the actual parsing is done in <NaturalDocs::Languages::Base->ParseFile()> +# and <OnComment()>, though. +# +# *Do not call externally.* Rather, call <ParseForInformation()> or <ParseForBuild()>. +# +# Returns: +# +# The default menu title of the file. Will be the <FileName> if nothing better is found. +# +sub Parse + { + my ($self) = @_; + + NaturalDocs::Error->OnStartParsing($sourceFile); + + $language = NaturalDocs::Languages->LanguageOf($sourceFile); + NaturalDocs::Parser::Native->Start(); + @parsedFile = ( ); + + my ($autoTopics, $scopeRecord) = $language->ParseFile($sourceFile, \@parsedFile); + + + $self->AddToClassHierarchy(); + + $self->BreakLists(); + + if (defined $autoTopics) + { + if (defined $scopeRecord) + { $self->RepairPackages($autoTopics, $scopeRecord); }; + + $self->MergeAutoTopics($language, $autoTopics); + }; + + $self->RemoveRemainingHeaderlessTopics(); + + + # We don't need to do this if there aren't any auto-topics because the only package changes would be implied by the comments. + if (defined $autoTopics) + { $self->AddPackageDelineators(); }; + + if (!NaturalDocs::Settings->NoAutoGroup()) + { $self->MakeAutoGroups($autoTopics); }; + + + # Set the menu title. + + my $defaultMenuTitle = $sourceFile; + + if (scalar @parsedFile) + { + my $addFileTitle; + + if (NaturalDocs::Settings->OnlyFileTitles()) + { + # We still want to use the title from the topics if the first one is a file. + if ($parsedFile[0]->Type() eq ::TOPIC_FILE()) + { $addFileTitle = 0; } + else + { $addFileTitle = 1; }; + } + elsif (scalar @parsedFile == 1 || NaturalDocs::Topics->TypeInfo( $parsedFile[0]->Type() )->PageTitleIfFirst()) + { $addFileTitle = 0; } + else + { $addFileTitle = 1; }; + + if (!$addFileTitle) + { + $defaultMenuTitle = $parsedFile[0]->Title(); + } + else + { + # If the title ended up being the file name, add a leading section for it. + + unshift @parsedFile, + NaturalDocs::Parser::ParsedTopic->New(::TOPIC_FILE(), (NaturalDocs::File->SplitPath($sourceFile))[2], + undef, undef, undef, undef, undef, 1, undef); + }; + }; + + NaturalDocs::Error->OnEndParsing($sourceFile); + + return $defaultMenuTitle; + }; + + +# +# Function: CleanComment +# +# Removes any extraneous formatting and whitespace from the comment. Eliminates comment boxes, horizontal lines, trailing +# whitespace from lines, and expands all tab characters. It keeps leading whitespace, though, since it may be needed for +# example code, and blank lines, since the original line numbers are needed. +# +# Parameters: +# +# commentLines - An arrayref of the comment lines to clean. *The original memory will be changed.* Lines should have the +# language's comment symbols replaced by spaces and not have a trailing line break. +# +sub CleanComment #(commentLines) + { + my ($self, $commentLines) = @_; + + use constant DONT_KNOW => 0; + use constant IS_UNIFORM => 1; + use constant IS_UNIFORM_IF_AT_END => 2; + use constant IS_NOT_UNIFORM => 3; + + my $leftSide = DONT_KNOW; + my $rightSide = DONT_KNOW; + my $leftSideChar; + my $rightSideChar; + + my $index = 0; + my $tabLength = NaturalDocs::Settings->TabLength(); + + while ($index < scalar @$commentLines) + { + # Strip trailing whitespace from the original. + + $commentLines->[$index] =~ s/[ \t]+$//; + + + # Expand tabs in the original. This method is almost six times faster than Text::Tabs' method. + + my $tabIndex = index($commentLines->[$index], "\t"); + + while ($tabIndex != -1) + { + substr( $commentLines->[$index], $tabIndex, 1, ' ' x ($tabLength - ($tabIndex % $tabLength)) ); + $tabIndex = index($commentLines->[$index], "\t", $tabIndex); + }; + + + # Make a working copy and strip leading whitespace as well. This has to be done after tabs are expanded because + # stripping indentation could change how far tabs are expanded. + + my $line = $commentLines->[$index]; + $line =~ s/^ +//; + + # If the line is blank... + if (!length $line) + { + # If we have a potential vertical line, this only acceptable if it's at the end of the comment. + if ($leftSide == IS_UNIFORM) + { $leftSide = IS_UNIFORM_IF_AT_END; }; + if ($rightSide == IS_UNIFORM) + { $rightSide = IS_UNIFORM_IF_AT_END; }; + } + + # If there's at least four symbols in a row, it's a horizontal line. The second regex supports differing edge characters. It + # doesn't matter if any of this matches the left and right side symbols. The length < 256 is a sanity check, because that + # regexp has caused the perl regexp engine to choke on an insane line someone sent me from an automatically generated + # file. It had over 10k characters on the first line, and most of them were 0x00. + elsif ($line =~ /^([^a-zA-Z0-9 ])\1{3,}$/ || + (length $line < 256 && $line =~ /^([^a-zA-Z0-9 ])\1*([^a-zA-Z0-9 ])\2{3,}([^a-zA-Z0-9 ])\3*$/) ) + { + # Ignore it. This has no effect on the vertical line detection. We want to keep it in the output though in case it was + # in a code section. + } + + # If the line is not blank or a horizontal line... + else + { + # More content means any previous blank lines are no longer tolerated in vertical line detection. They are only + # acceptable at the end of the comment. + + if ($leftSide == IS_UNIFORM_IF_AT_END) + { $leftSide = IS_NOT_UNIFORM; }; + if ($rightSide == IS_UNIFORM_IF_AT_END) + { $rightSide = IS_NOT_UNIFORM; }; + + + # Detect vertical lines. Lines are only lines if they are followed by whitespace or a connected horizontal line. + # Otherwise we may accidentally detect lines from short comments that just happen to have every first or last + # character the same. + + if ($leftSide != IS_NOT_UNIFORM) + { + if ($line =~ /^([^a-zA-Z0-9])\1*(?: |$)/) + { + if ($leftSide == DONT_KNOW) + { + $leftSide = IS_UNIFORM; + $leftSideChar = $1; + } + else # ($leftSide == IS_UNIFORM) Other choices already ruled out. + { + if ($leftSideChar ne $1) + { $leftSide = IS_NOT_UNIFORM; }; + }; + } + # We'll tolerate the lack of symbols on the left on the first line, because it may be a + # /* Function: Whatever + # * Description. + # */ + # comment which would have the leading /* blanked out. + elsif ($index != 0) + { + $leftSide = IS_NOT_UNIFORM; + }; + }; + + if ($rightSide != IS_NOT_UNIFORM) + { + if ($line =~ / ([^a-zA-Z0-9])\1*$/) + { + if ($rightSide == DONT_KNOW) + { + $rightSide = IS_UNIFORM; + $rightSideChar = $1; + } + else # ($rightSide == IS_UNIFORM) Other choices already ruled out. + { + if ($rightSideChar ne $1) + { $rightSide = IS_NOT_UNIFORM; }; + }; + } + else + { + $rightSide = IS_NOT_UNIFORM; + }; + }; + + # We'll remove vertical lines later if they're uniform throughout the entire comment. + }; + + $index++; + }; + + + if ($leftSide == IS_UNIFORM_IF_AT_END) + { $leftSide = IS_UNIFORM; }; + if ($rightSide == IS_UNIFORM_IF_AT_END) + { $rightSide = IS_UNIFORM; }; + + + $index = 0; + my $inCodeSection = 0; + + while ($index < scalar @$commentLines) + { + # Clear horizontal lines only if we're not in a code section. + if ($commentLines->[$index] =~ /^ *([^a-zA-Z0-9 ])\1{3,}$/ || + ( length $commentLines->[$index] < 256 && + $commentLines->[$index] =~ /^ *([^a-zA-Z0-9 ])\1*([^a-zA-Z0-9 ])\2{3,}([^a-zA-Z0-9 ])\3*$/ ) ) + { + if (!$inCodeSection) + { $commentLines->[$index] = ''; } + } + + else + { + # Clear vertical lines. + + if ($leftSide == IS_UNIFORM) + { + # This works because every line should either start this way, be blank, or be the first line that doesn't start with a + # symbol. + $commentLines->[$index] =~ s/^ *([^a-zA-Z0-9 ])\1*//; + }; + + if ($rightSide == IS_UNIFORM) + { + $commentLines->[$index] =~ s/ *([^a-zA-Z0-9 ])\1*$//; + }; + + + # Clear horizontal lines again if there were vertical lines. This catches lines that were separated from the verticals by + # whitespace. + + if (($leftSide == IS_UNIFORM || $rightSide == IS_UNIFORM) && !$inCodeSection) + { + $commentLines->[$index] =~ s/^ *([^a-zA-Z0-9 ])\1{3,}$//; + $commentLines->[$index] =~ s/^ *([^a-zA-Z0-9 ])\1*([^a-zA-Z0-9 ])\2{3,}([^a-zA-Z0-9 ])\3*$//; + }; + + + # Check for the start and end of code sections. Note that this doesn't affect vertical line removal. + + if (!$inCodeSection && + $commentLines->[$index] =~ /^ *\( *(?:(?:start|begin)? +)?(?:table|code|example|diagram) *\)$/i ) + { + $inCodeSection = 1; + } + elsif ($inCodeSection && + $commentLines->[$index] =~ /^ *\( *(?:end|finish|done)(?: +(?:table|code|example|diagram))? *\)$/i) + { + $inCodeSection = 0; + } + } + + + $index++; + }; + + }; + + + +############################################################################### +# Group: Processing Functions + + +# +# Function: RepairPackages +# +# Recalculates the packages for all comment topics using the auto-topics and the scope record. Call this *before* calling +# <MergeAutoTopics()>. +# +# Parameters: +# +# autoTopics - A reference to the list of automatically generated <NaturalDocs::Parser::ParsedTopics>. +# scopeRecord - A reference to an array of <NaturalDocs::Languages::Advanced::ScopeChanges>. +# +sub RepairPackages #(autoTopics, scopeRecord) + { + my ($self, $autoTopics, $scopeRecord) = @_; + + my $topicIndex = 0; + my $autoTopicIndex = 0; + my $scopeIndex = 0; + + my $topic = $parsedFile[0]; + my $autoTopic = $autoTopics->[0]; + my $scopeChange = $scopeRecord->[0]; + + my $currentPackage; + my $inFakePackage; + + while (defined $topic) + { + # First update the scope via the record if its defined and has the lowest line number. + if (defined $scopeChange && + $scopeChange->LineNumber() <= $topic->LineNumber() && + (!defined $autoTopic || $scopeChange->LineNumber() <= $autoTopic->LineNumber()) ) + { + $currentPackage = $scopeChange->Scope(); + $scopeIndex++; + $scopeChange = $scopeRecord->[$scopeIndex]; # Will be undef when past end. + $inFakePackage = undef; + } + + # Next try to end a fake scope with an auto topic if its defined and has the lowest line number. + elsif (defined $autoTopic && + $autoTopic->LineNumber() <= $topic->LineNumber()) + { + if ($inFakePackage) + { + $currentPackage = $autoTopic->Package(); + $inFakePackage = undef; + }; + + $autoTopicIndex++; + $autoTopic = $autoTopics->[$autoTopicIndex]; # Will be undef when past end. + } + + + # Finally try to handle the topic, since it has the lowest line number. Check for Type() because headerless topics won't have + # one. + else + { + my $scope; + if ($topic->Type()) + { $scope = NaturalDocs::Topics->TypeInfo($topic->Type())->Scope(); } + else + { $scope = ::SCOPE_NORMAL(); }; + + if ($scope == ::SCOPE_START() || $scope == ::SCOPE_END()) + { + # They should already have the correct class and scope. + $currentPackage = $topic->Package(); + $inFakePackage = 1; + } + else + { + # Fix the package of everything else. + + # Note that the first function or variable topic to appear in a fake package will assume that package even if it turns out + # to be incorrect in the actual code, since the topic will come before the auto-topic. This will be corrected in + # MergeAutoTopics(). + + $topic->SetPackage($currentPackage); + }; + + $topicIndex++; + $topic = $parsedFile[$topicIndex]; # Will be undef when past end. + }; + }; + + }; + + +# +# Function: MergeAutoTopics +# +# Merges the automatically generated topics into the file. If an auto-topic matches an existing topic, it will have it's prototype +# and package transferred. If it doesn't, the auto-topic will be inserted into the list unless +# <NaturalDocs::Settings->DocumentedOnly()> is set. If an existing topic doesn't have a title, it's assumed to be a headerless +# comment and will be merged with the next auto-topic or discarded. +# +# Parameters: +# +# language - The <NaturalDocs::Languages::Base>-derived class for the file. +# autoTopics - A reference to the list of automatically generated topics. +# +sub MergeAutoTopics #(language, autoTopics) + { + my ($self, $language, $autoTopics) = @_; + + my $topicIndex = 0; + my $autoTopicIndex = 0; + + # Keys are topic types, values are existence hashrefs of titles. + my %topicsInLists; + + while ($topicIndex < scalar @parsedFile && $autoTopicIndex < scalar @$autoTopics) + { + my $topic = $parsedFile[$topicIndex]; + my $autoTopic = $autoTopics->[$autoTopicIndex]; + + my $cleanTitle = $topic->Title(); + $cleanTitle =~ s/[\t ]*\([^\(]*$//; + + # Add the auto-topic if it's higher in the file than the current topic. + if ($autoTopic->LineNumber() < $topic->LineNumber()) + { + if (exists $topicsInLists{$autoTopic->Type()} && + exists $topicsInLists{$autoTopic->Type()}->{$autoTopic->Title()}) + { + # Remove it from the list so a second one with the same name will be added. + delete $topicsInLists{$autoTopic->Type()}->{$autoTopic->Title()}; + } + elsif (!NaturalDocs::Settings->DocumentedOnly()) + { + splice(@parsedFile, $topicIndex, 0, $autoTopic); + $topicIndex++; + }; + + $autoTopicIndex++; + } + + # Remove a headerless topic if there's another topic between it and the next auto-topic. + elsif (!$topic->Title() && $topicIndex + 1 < scalar @parsedFile && + $parsedFile[$topicIndex+1]->LineNumber() < $autoTopic->LineNumber()) + { + splice(@parsedFile, $topicIndex, 1); + } + + # Transfer information if we have a match or a headerless topic. + elsif ( !$topic->Title() || ($topic->Type() == $autoTopic->Type() && index($autoTopic->Title(), $cleanTitle) != -1) ) + { + $topic->SetType($autoTopic->Type()); + $topic->SetPrototype($autoTopic->Prototype()); + $topic->SetUsing($autoTopic->Using()); + + if (!$topic->Title()) + { $topic->SetTitle($autoTopic->Title()); }; + + if (NaturalDocs::Topics->TypeInfo($topic->Type())->Scope() != ::SCOPE_START()) + { $topic->SetPackage($autoTopic->Package()); } + elsif ($autoTopic->Package() ne $topic->Package()) + { + my @autoPackageIdentifiers = NaturalDocs::SymbolString->IdentifiersOf($autoTopic->Package()); + my @packageIdentifiers = NaturalDocs::SymbolString->IdentifiersOf($topic->Package()); + + while (scalar @autoPackageIdentifiers && $autoPackageIdentifiers[-1] eq $packageIdentifiers[-1]) + { + pop @autoPackageIdentifiers; + pop @packageIdentifiers; + }; + + if (scalar @autoPackageIdentifiers) + { $topic->SetPackage( NaturalDocs::SymbolString->Join(@autoPackageIdentifiers) ); }; + }; + + $topicIndex++; + $autoTopicIndex++; + } + + # Extract topics in lists. + elsif ($topic->IsList()) + { + if (!exists $topicsInLists{$topic->Type()}) + { $topicsInLists{$topic->Type()} = { }; }; + + my $body = $topic->Body(); + + while ($body =~ /<ds>([^<]+)<\/ds>/g) + { $topicsInLists{$topic->Type()}->{NaturalDocs::NDMarkup->RestoreAmpChars($1)} = 1; }; + + $topicIndex++; + } + + # Otherwise there's no match. Skip the topic. The auto-topic will be added later. + else + { + $topicIndex++; + } + }; + + # Add any auto-topics remaining. + if (!NaturalDocs::Settings->DocumentedOnly()) + { + while ($autoTopicIndex < scalar @$autoTopics) + { + my $autoTopic = $autoTopics->[$autoTopicIndex]; + + if (exists $topicsInLists{$autoTopic->Type()} && + exists $topicsInLists{$autoTopic->Type()}->{$autoTopic->Title()}) + { + # Remove it from the list so a second one with the same name will be added. + delete $topicsInLists{$autoTopic->Type()}->{$autoTopic->Title()}; + } + else + { + push(@parsedFile, $autoTopic); + }; + + $autoTopicIndex++; + }; + }; + }; + + +# +# Function: RemoveRemainingHeaderlessTopics +# +# After <MergeAutoTopics()> is done, this function removes any remaining headerless topics from the file. If they don't merge +# into anything, they're not valid topics. +# +sub RemoveRemainingHeaderlessTopics + { + my ($self) = @_; + + my $index = 0; + while ($index < scalar @parsedFile) + { + if ($parsedFile[$index]->Title()) + { $index++; } + else + { splice(@parsedFile, $index, 1); }; + }; + }; + + +# +# Function: MakeAutoGroups +# +# Creates group topics for files that do not have them. +# +sub MakeAutoGroups + { + my ($self) = @_; + + # No groups only one topic. + if (scalar @parsedFile < 2) + { return; }; + + my $index = 0; + my $startStretch = 0; + + # Skip the first entry if its the page title. + if (NaturalDocs::Topics->TypeInfo( $parsedFile[0]->Type() )->PageTitleIfFirst()) + { + $index = 1; + $startStretch = 1; + }; + + # Make auto-groups for each stretch between scope-altering topics. + while ($index < scalar @parsedFile) + { + my $scope = NaturalDocs::Topics->TypeInfo($parsedFile[$index]->Type())->Scope(); + + if ($scope == ::SCOPE_START() || $scope == ::SCOPE_END()) + { + if ($index > $startStretch) + { $index += $self->MakeAutoGroupsFor($startStretch, $index); }; + + $startStretch = $index + 1; + }; + + $index++; + }; + + if ($index > $startStretch) + { $self->MakeAutoGroupsFor($startStretch, $index); }; + }; + + +# +# Function: MakeAutoGroupsFor +# +# Creates group topics for sections of files that do not have them. A support function for <MakeAutoGroups()>. +# +# Parameters: +# +# startIndex - The index to start at. +# endIndex - The index to end at. Not inclusive. +# +# Returns: +# +# The number of group topics added. +# +sub MakeAutoGroupsFor #(startIndex, endIndex) + { + my ($self, $startIndex, $endIndex) = @_; + + # No groups if any are defined already. + for (my $i = $startIndex; $i < $endIndex; $i++) + { + if ($parsedFile[$i]->Type() eq ::TOPIC_GROUP()) + { return 0; }; + }; + + + use constant COUNT => 0; + use constant TYPE => 1; + use constant SECOND_TYPE => 2; + use constant SIZE => 3; + + # This is an array of ( count, type, secondType ) triples. Count and Type will always be filled in; count is the number of + # consecutive topics of type. On the second pass, if small groups are combined secondType will be filled in. There will not be + # more than two types per group. + my @groups; + my $groupIndex = 0; + + + # First pass: Determine all the groups. + + my $i = $startIndex; + my $currentType; + + while ($i < $endIndex) + { + if (!defined $currentType || ($parsedFile[$i]->Type() ne $currentType && $parsedFile[$i]->Type() ne ::TOPIC_GENERIC()) ) + { + if (defined $currentType) + { $groupIndex += SIZE; }; + + $currentType = $parsedFile[$i]->Type(); + + $groups[$groupIndex + COUNT] = 1; + $groups[$groupIndex + TYPE] = $currentType; + } + else + { $groups[$groupIndex + COUNT]++; }; + + $i++; + }; + + + # Second pass: Combine groups based on "noise". Noise means types go from A to B to A at least once, and there are at least + # two groups in a row with three or less, and at least one of those groups is two or less. So 3, 3, 3 doesn't count as noise, but + # 3, 2, 3 does. + + $groupIndex = 0; + + # While there are at least three groups left... + while ($groupIndex < scalar @groups - (2 * SIZE)) + { + # If the group two places in front of this one has the same type... + if ($groups[$groupIndex + (2 * SIZE) + TYPE] eq $groups[$groupIndex + TYPE]) + { + # It means we went from A to B to A, which partially qualifies as noise. + + my $firstType = $groups[$groupIndex + TYPE]; + my $secondType = $groups[$groupIndex + SIZE + TYPE]; + + if (NaturalDocs::Topics->TypeInfo($firstType)->CanGroupWith($secondType) || + NaturalDocs::Topics->TypeInfo($secondType)->CanGroupWith($firstType)) + { + my $hasNoise; + + my $hasThrees; + my $hasTwosOrOnes; + + my $endIndex = $groupIndex; + + while ($endIndex < scalar @groups && + ($groups[$endIndex + TYPE] eq $firstType || $groups[$endIndex + TYPE] eq $secondType)) + { + if ($groups[$endIndex + COUNT] > 3) + { + # They must be consecutive to count. + $hasThrees = 0; + $hasTwosOrOnes = 0; + } + elsif ($groups[$endIndex + COUNT] == 3) + { + $hasThrees = 1; + + if ($hasTwosOrOnes) + { $hasNoise = 1; }; + } + else # < 3 + { + if ($hasThrees || $hasTwosOrOnes) + { $hasNoise = 1; }; + + $hasTwosOrOnes = 1; + }; + + $endIndex += SIZE; + }; + + if (!$hasNoise) + { + $groupIndex = $endIndex - SIZE; + } + else # hasNoise + { + $groups[$groupIndex + SECOND_TYPE] = $secondType; + + for (my $noiseIndex = $groupIndex + SIZE; $noiseIndex < $endIndex; $noiseIndex += SIZE) + { + $groups[$groupIndex + COUNT] += $groups[$noiseIndex + COUNT]; + }; + + splice(@groups, $groupIndex + SIZE, $endIndex - $groupIndex - SIZE); + + $groupIndex += SIZE; + }; + } + + else # They can't group together + { + $groupIndex += SIZE; + }; + } + + else + { $groupIndex += SIZE; }; + }; + + + # Finally, create group topics for the parsed file. + + $groupIndex = 0; + $i = $startIndex; + + while ($groupIndex < scalar @groups) + { + if ($groups[$groupIndex + TYPE] ne ::TOPIC_GENERIC()) + { + my $topic = $parsedFile[$i]; + my $title = NaturalDocs::Topics->NameOfType($groups[$groupIndex + TYPE], 1); + + if (defined $groups[$groupIndex + SECOND_TYPE]) + { $title .= ' and ' . NaturalDocs::Topics->NameOfType($groups[$groupIndex + SECOND_TYPE], 1); }; + + splice(@parsedFile, $i, 0, NaturalDocs::Parser::ParsedTopic->New(::TOPIC_GROUP(), + $title, + $topic->Package(), $topic->Using(), + undef, undef, undef, + $topic->LineNumber()) ); + $i++; + }; + + $i += $groups[$groupIndex + COUNT]; + $groupIndex += SIZE; + }; + + return (scalar @groups / SIZE); + }; + + +# +# Function: AddToClassHierarchy +# +# Adds any class topics to the class hierarchy, since they may not have been called with <OnClass()> if they didn't match up to +# an auto-topic. +# +sub AddToClassHierarchy + { + my ($self) = @_; + + foreach my $topic (@parsedFile) + { + if ($topic->Type() && NaturalDocs::Topics->TypeInfo( $topic->Type() )->ClassHierarchy()) + { + if ($topic->IsList()) + { + my $body = $topic->Body(); + + while ($body =~ /<ds>([^<]+)<\/ds>/g) + { + $self->OnClass( NaturalDocs::SymbolString->FromText( NaturalDocs::NDMarkup->RestoreAmpChars($1) ) ); + }; + } + else + { + $self->OnClass($topic->Package()); + }; + }; + }; + }; + + +# +# Function: AddPackageDelineators +# +# Adds section and class topics to make sure the package is correctly represented in the documentation. Should be called last in +# this process. +# +sub AddPackageDelineators + { + my ($self) = @_; + + my $index = 0; + my $currentPackage; + + # Values are the arrayref [ title, type ]; + my %usedPackages; + + while ($index < scalar @parsedFile) + { + my $topic = $parsedFile[$index]; + + if ($topic->Package() ne $currentPackage) + { + $currentPackage = $topic->Package(); + my $scopeType = NaturalDocs::Topics->TypeInfo($topic->Type())->Scope(); + + if ($scopeType == ::SCOPE_START()) + { + $usedPackages{$currentPackage} = [ $topic->Title(), $topic->Type() ]; + } + elsif ($scopeType == ::SCOPE_END()) + { + my $newTopic; + + if (!defined $currentPackage) + { + $newTopic = NaturalDocs::Parser::ParsedTopic->New(::TOPIC_SECTION(), 'Global', + undef, undef, + undef, undef, undef, + $topic->LineNumber(), undef); + } + else + { + my ($title, $body, $summary, $type); + my @packageIdentifiers = NaturalDocs::SymbolString->IdentifiersOf($currentPackage); + + if (exists $usedPackages{$currentPackage}) + { + $title = $usedPackages{$currentPackage}->[0]; + $type = $usedPackages{$currentPackage}->[1]; + $body = '<p>(continued)</p>'; + $summary = '(continued)'; + } + else + { + $title = join($language->PackageSeparator(), @packageIdentifiers); + $type = ::TOPIC_CLASS(); + + # Body and summary stay undef. + + $usedPackages{$currentPackage} = $title; + }; + + my @titleIdentifiers = NaturalDocs::SymbolString->IdentifiersOf( NaturalDocs::SymbolString->FromText($title) ); + for (my $i = 0; $i < scalar @titleIdentifiers; $i++) + { pop @packageIdentifiers; }; + + $newTopic = NaturalDocs::Parser::ParsedTopic->New($type, $title, + NaturalDocs::SymbolString->Join(@packageIdentifiers), undef, + undef, $summary, $body, + $topic->LineNumber(), undef); + } + + splice(@parsedFile, $index, 0, $newTopic); + $index++; + } + }; + + $index++; + }; + }; + + +# +# Function: BreakLists +# +# Breaks list topics into individual topics. +# +sub BreakLists + { + my $self = shift; + + my $index = 0; + + while ($index < scalar @parsedFile) + { + my $topic = $parsedFile[$index]; + + if ($topic->IsList() && NaturalDocs::Topics->TypeInfo( $topic->Type() )->BreakLists()) + { + my $body = $topic->Body(); + + my @newTopics; + my $newBody; + + my $bodyIndex = 0; + + for (;;) + { + my $startList = index($body, '<dl>', $bodyIndex); + + if ($startList == -1) + { last; }; + + $newBody .= substr($body, $bodyIndex, $startList - $bodyIndex); + + my $endList = index($body, '</dl>', $startList); + my $listBody = substr($body, $startList, $endList - $startList); + + while ($listBody =~ /<ds>([^<]+)<\/ds><dd>(.*?)<\/dd>/g) + { + my ($symbol, $description) = ($1, $2); + + push @newTopics, NaturalDocs::Parser::ParsedTopic->New( $topic->Type(), $symbol, $topic->Package(), + $topic->Using(), undef, + $self->GetSummaryFromDescriptionList($description), + '<p>' . $description . '</p>', $topic->LineNumber(), + undef ); + }; + + $bodyIndex = $endList + 5; + }; + + $newBody .= substr($body, $bodyIndex); + + # Remove trailing headings. + $newBody =~ s/(?:<h>[^<]+<\/h>)+$//; + + # Remove empty headings. + $newBody =~ s/(?:<h>[^<]+<\/h>)+(<h>[^<]+<\/h>)/$1/g; + + if ($newBody) + { + unshift @newTopics, NaturalDocs::Parser::ParsedTopic->New( ::TOPIC_GROUP(), $topic->Title(), $topic->Package(), + $topic->Using(), undef, + $self->GetSummaryFromBody($newBody), $newBody, + $topic->LineNumber(), undef ); + }; + + splice(@parsedFile, $index, 1, @newTopics); + + $index += scalar @newTopics; + } + + else # not a list + { $index++; }; + }; + }; + + +# +# Function: GetSummaryFromBody +# +# Returns the summary text from the topic body. +# +# Parameters: +# +# body - The complete topic body, in <NDMarkup>. +# +# Returns: +# +# The topic summary, or undef if none. +# +sub GetSummaryFromBody #(body) + { + my ($self, $body) = @_; + + my $summary; + + # Extract the first sentence from the leading paragraph, if any. We'll tolerate a single header beforehand, but nothing else. + + if ($body =~ /^(?:<h>[^<]*<\/h>)?<p>(.*?)(<\/p>|[\.\!\?](?:[\)\}\'\ ]|"|>))/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 =~ /^(.*?)($|[\.\!\?](?:[\)\}\'\ ]|"|>))/) + { $summary = $1 . $2; }; + + return $summary; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Parser/JavaDoc.pm b/docs/tool/Modules/NaturalDocs/Parser/JavaDoc.pm new file mode 100644 index 00000000..860b0c5f --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Parser/JavaDoc.pm @@ -0,0 +1,464 @@ +############################################################################### +# +# Package: NaturalDocs::Parser::JavaDoc +# +############################################################################### +# +# A package for translating JavaDoc topics into Natural Docs. +# +# Supported tags: +# +# - @param +# - @author +# - @deprecated +# - @code, @literal (doesn't change font) +# - @exception, @throws (doesn't link to class) +# - @link, @linkplain (doesn't change font) +# - @return, @returns +# - @see +# - @since +# - @value (shown as link instead of replacement) +# - @version +# +# Stripped tags: +# +# - @inheritDoc +# - @serial, @serialField, @serialData +# - All other block level tags. +# +# Unsupported tags: +# +# These will appear literally in the output because I cannot handle them easily. +# +# - @docRoot +# - Any other tags not mentioned +# +# Supported HTML: +# +# - p +# - b, i, u +# - pre +# - a href +# - ol, ul, li (ol gets converted to ul) +# - gt, lt, amp, quot, nbsp entities +# +# Stripped HTML: +# +# - code +# - HTML comments +# +# Unsupported HTML: +# +# These will appear literally in the output because I cannot handle them easily. +# +# - Any tags with additional properties other than a href. (ex. <p class=Something>) +# - Any other tags not mentioned +# +# Reference: +# +# http://java.sun.com/j2se/1.5.0/docs/tooldocs/windows/javadoc.html +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Parser::JavaDoc; + + +# +# hash: blockTags +# An existence hash of the all-lowercase JavaDoc block tags, not including the @. +# +my %blockTags = ( 'param' => 1, 'author' => 1, 'deprecated' => 1, 'exception' => 1, 'return' => 1, 'see' => 1, + 'serial' => 1, 'serialfield' => 1, 'serialdata' => 1, 'since' => 1, 'throws' => 1, 'version' => 1, + 'returns' => 1 ); + +# +# hash: inlineTags +# An existence hash of the all-lowercase JavaDoc inline tags, not including the @. +# +my %inlineTags = ( 'inheritdoc' => 1, 'docroot' => 1, 'code' => 1, 'literal' => 1, 'link' => 1, 'linkplain' => 1, 'value' => 1 ); + + +## +# Examines the comment and returns whether it is *definitely* JavaDoc content, i.e. is owned by this package. +# +# Parameters: +# +# commentLines - An arrayref of the comment lines. Must have been run through <NaturalDocs::Parser->CleanComment()>. +# isJavaDoc - Whether the comment is JavaDoc styled. This doesn't necessarily mean it has JavaDoc content. +# +# Returns: +# +# Whether the comment is *definitely* JavaDoc content. +# +sub IsMine #(string[] commentLines, bool isJavaDoc) + { + my ($self, $commentLines, $isJavaDoc) = @_; + + if (!$isJavaDoc) + { return undef; }; + + for (my $line = 0; $line < scalar @$commentLines; $line++) + { + if ($commentLines->[$line] =~ /^ *@([a-z]+) /i && exists $blockTags{$1} || + $commentLines->[$line] =~ /\{@([a-z]+) /i && exists $inlineTags{$1}) + { + return 1; + }; + }; + + return 0; + }; + + +## +# Parses the JavaDoc-syntax comment and adds it to the parsed topic list. +# +# Parameters: +# +# commentLines - An arrayref of the comment lines. Must have been run through <NaturalDocs::Parser->CleanComment()>. +# *The original memory will be changed.* +# isJavaDoc - Whether the comment is JavaDoc styled. This doesn't necessarily mean it has JavaDoc content. +# lineNumber - The line number of the first of the comment lines. +# parsedTopics - A reference to the array where any new <NaturalDocs::Parser::ParsedTopics> should be placed. +# +# Returns: +# +# The number of parsed topics added to the array, which in this case will always be one. +# +sub ParseComment #(string[] commentLines, bool isJavaDoc, int lineNumber, ParsedTopics[]* parsedTopics) + { + my ($self, $commentLines, $isJavaDoc, $lineNumber, $parsedTopics) = @_; + + + # Stage one: Before block level tags. + + my $i = 0; + my $output; + my $unformattedText; + my $inCode; + my $sharedCodeIndent; + + while ($i < scalar @$commentLines && + !($commentLines->[$i] =~ /^ *@([a-z]+) /i && exists $blockTags{$1}) ) + { + my $line = $self->ConvertAmpChars($commentLines->[$i]); + my @tokens = split(/(<\/?pre>)/, $line); + + foreach my $token (@tokens) + { + if ($token =~ /^<pre>$/i) + { + if (!$inCode && $unformattedText) + { + $output .= '<p>' . $self->FormatText($unformattedText, 1) . '</p>'; + }; + + $inCode = 1; + $unformattedText = undef; + } + elsif ($token =~ /^<\/pre>$/i) + { + if ($inCode && $unformattedText) + { + $unformattedText =~ s/^ {$sharedCodeIndent}//mg; + $unformattedText =~ s/\n{3,}/\n\n/g; + $unformattedText =~ s/\n+$//; + $output .= '<code>' . $unformattedText . '</code>'; + + $sharedCodeIndent = undef; + }; + + $inCode = 0; + $unformattedText = undef; + } + elsif (length($token)) + { + if (!$inCode) + { + $token =~ s/^ +//; + if ($unformattedText) + { $unformattedText .= ' '; }; + } + else + { + $token =~ /^( *)/; + my $indent = length($1); + + if (!defined $sharedCodeIndent || $indent < $sharedCodeIndent) + { $sharedCodeIndent = $indent; }; + }; + + $unformattedText .= $token; + }; + }; + + if ($inCode && $unformattedText) + { $unformattedText .= "\n"; }; + + $i++; + }; + + if ($unformattedText) + { + if ($inCode) + { + $unformattedText =~ s/^ {$sharedCodeIndent}//mg; + $unformattedText =~ s/\n{3,}/\n\n/g; + $unformattedText =~ s/\n+$//; + $output .= '<code>' . $unformattedText . '</code>'; + } + else + { $output .= '<p>' . $self->FormatText($unformattedText, 1) . '</p>'; }; + + $unformattedText = undef; + }; + + + # Stage two: Block level tags. + + my ($keyword, $value, $unformattedTextPtr, $unformattedTextCloser); + my ($params, $authors, $deprecation, $throws, $returns, $seeAlso, $since, $version); + + + while ($i < scalar @$commentLines) + { + my $line = $self->ConvertAmpChars($commentLines->[$i]); + $line =~ s/^ +//; + + if ($line =~ /^@([a-z]+) ?(.*)$/i) + { + ($keyword, $value) = (lc($1), $2); + + # Process the previous one, if any. + if ($unformattedText) + { + $$unformattedTextPtr .= $self->FormatText($unformattedText) . $unformattedTextCloser; + $unformattedText = undef; + }; + + if ($keyword eq 'param') + { + $value =~ /^([a-z0-9_]+) *(.*)$/i; + + $params .= '<de>' . $1 . '</de><dd>'; + $unformattedText = $2; + + $unformattedTextPtr = \$params; + $unformattedTextCloser = '</dd>'; + } + elsif ($keyword eq 'exception' || $keyword eq 'throws') + { + $value =~ /^([a-z0-9_]+) *(.*)$/i; + + $throws .= '<de>' . $1 . '</de><dd>'; + $unformattedText = $2; + + $unformattedTextPtr = \$throws; + $unformattedTextCloser = '</dd>'; + } + elsif ($keyword eq 'return' || $keyword eq 'returns') + { + if ($returns) + { $returns .= ' '; }; + + $unformattedText = $value; + $unformattedTextPtr = \$returns; + $unformattedTextCloser = undef; + } + elsif ($keyword eq 'author') + { + if ($authors) + { $authors .= ', '; }; + + $unformattedText = $value; + $unformattedTextPtr = \$authors; + $unformattedTextCloser = undef; + } + elsif ($keyword eq 'deprecated') + { + if ($deprecation) + { $deprecation .= ' '; }; + + $unformattedText = $value; + $unformattedTextPtr = \$deprecation; + $unformattedTextCloser = undef; + } + elsif ($keyword eq 'since') + { + if ($since) + { $since .= ', '; }; + + $unformattedText = $value; + $unformattedTextPtr = \$since; + $unformattedTextCloser = undef; + } + elsif ($keyword eq 'version') + { + if ($version) + { $version .= ', '; }; + + $unformattedText = $value; + $unformattedTextPtr = \$version; + $unformattedTextCloser = undef; + } + elsif ($keyword eq 'see') + { + if ($seeAlso) + { $seeAlso .= ', '; }; + + $unformattedText = undef; + + if ($value =~ /^&(?:quot|lt);/i) + { $seeAlso .= $self->FormatText($value); } + else + { $seeAlso .= $self->ConvertLink($value); }; + }; + + # Everything else will be skipped. + } + elsif ($unformattedText) + { + $unformattedText .= ' ' . $line; + }; + + $i++; + }; + + if ($unformattedText) + { + $$unformattedTextPtr .= $self->FormatText($unformattedText) . $unformattedTextCloser; + $unformattedText = undef; + }; + + if ($params) + { $output .= '<h>Parameters</h><dl>' . $params . '</dl>'; }; + if ($returns) + { $output .= '<h>Returns</h><p>' . $returns . '</p>'; }; + if ($throws) + { $output .= '<h>Throws</h><dl>' . $throws . '</dl>'; }; + if ($since) + { $output .= '<h>Since</h><p>' . $since . '</p>'; }; + if ($version) + { $output .= '<h>Version</h><p>' . $version . '</p>'; }; + if ($deprecation) + { $output .= '<h>Deprecated</h><p>' . $deprecation . '</p>'; }; + if ($authors) + { $output .= '<h>Author</h><p>' . $authors . '</p>'; }; + if ($seeAlso) + { $output .= '<h>See Also</h><p>' . $seeAlso . '</p>'; }; + + + # Stage three: Build the parsed topic. + + my $summary = NaturalDocs::Parser->GetSummaryFromBody($output); + + push @$parsedTopics, NaturalDocs::Parser::ParsedTopic->New(undef, undef, undef, undef, undef, $summary, + $output, $lineNumber, undef); + return 1; + }; + + +## +# Translates any inline tags or HTML codes to <NDMarkup> and returns it. +# +sub FormatText #(string text, bool inParagraph) + { + my ($self, $text, $inParagraph) = @_; + + # JavaDoc Literal + + $text =~ s/\{\@(?:code|literal) ([^\}]*)\}/$self->ConvertAmpChars($1)/gie; + + + # HTML + + $text =~ s/<b>(.*?)<\/b>/<b>$1<\/b>/gi; + $text =~ s/<i>(.*?)<\/i>/<i>$1<\/i>/gi; + $text =~ s/<u>(.*?)<\/u>/<u>$1<\/u>/gi; + + $text =~ s/<code>(.*?)<\/code>/$1/gi; + + $text =~ s/<ul.*?>(.*?)<\/ul>/<ul>$1<\/ul>/gi; + $text =~ s/<ol.*?>(.*?)<\/ol>/<ul>$1<\/ul>/gi; + $text =~ s/<li.*?>(.*?)<\/li>/<li>$1<\/li>/gi; + + $text =~ s/<!--.*?-->//gi; + + $text =~ s/<\/p>//gi; + $text =~ s/^<p>//i; + if ($inParagraph) + { $text =~ s/<p>/<\/p><p>/gi; } + else + { $text =~ s/<p>//gi; }; + + $text =~ s/<a href="mailto:(.*?)".*?>(.*?)<\/a>/$self->MakeEMailLink($1, $2)/gie; + $text =~ s/<a href="(.*?)".*?>(.*?)<\/a>/$self->MakeURLLink($1, $2)/gie; + + $text =~ s/&nbsp;/ /gi; + $text =~ s/&amp;/&/gi; + $text =~ s/&gt;/>/gi; + $text =~ s/&lt;/</gi; + $text =~ s/&quot;/"/gi; + + + + # JavaDoc + + $text =~ s/\{\@inheritdoc\}//gi; + $text =~ s/\{\@(?:linkplain|link|value) ([^\}]*)\}/$self->ConvertLink($1)/gie; + + return $text; + }; + + +sub ConvertAmpChars #(text) + { + my ($self, $text) = @_; + + $text =~ s/&/&/g; + $text =~ s/</</g; + $text =~ s/>/>/g; + $text =~ s/"/"/g; + + return $text; + }; + +sub ConvertLink #(text) + { + my ($self, $text) = @_; + + $text =~ /^ *([a-z0-9\_\.\:\#]+(?:\([^\)]*\))?) *(.*)$/i; + my ($target, $label) = ($1, $2); + + # Convert the anchor to part of the link, but remove it altogether if it's the beginning of the link. + $target =~ s/^\#//; + $target =~ s/\#/\./; + + $label =~ s/ +$//; + + if (!length $label) + { return '<link target="' . $target . '" name="' . $target . '" original="' . $target . '">'; } + else + { return '<link target="' . $target . '" name="' . $label . '" original="' . $label . ' (' . $target . ')">'; }; + }; + +sub MakeURLLink #(target, text) + { + my ($self, $target, $text) = @_; + return '<url target="' . $target . '" name="' . $text . '">'; + }; + +sub MakeEMailLink #(target, text) + { + my ($self, $target, $text) = @_; + return '<email target="' . $target . '" name="' . $text . '">'; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Parser/Native.pm b/docs/tool/Modules/NaturalDocs/Parser/Native.pm new file mode 100644 index 00000000..61ba97e5 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Parser/Native.pm @@ -0,0 +1,1060 @@ +############################################################################### +# +# Package: NaturalDocs::Parser::Native +# +############################################################################### +# +# A package that converts comments from Natural Docs' native format into <NaturalDocs::Parser::ParsedTopic> objects. +# Unlike most second-level packages, these are packages and not object classes. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + + +use strict; +use integer; + +package NaturalDocs::Parser::Native; + + +############################################################################### +# Group: Variables + + +# Return values of TagType(). Not documented here. +use constant POSSIBLE_OPENING_TAG => 1; +use constant POSSIBLE_CLOSING_TAG => 2; +use constant NOT_A_TAG => 3; + + +# +# var: package +# +# A <SymbolString> representing the package normal topics will be a part of at the current point in the file. This is a package variable +# because it needs to be reserved between function calls. +# +my $package; + +# +# hash: functionListIgnoredHeadings +# +# An existence hash of all the headings that prevent the parser from creating function list symbols. Whenever one of +# these headings are used in a function list topic, symbols are not created from definition lists until the next heading. The keys +# are in all lowercase. +# +my %functionListIgnoredHeadings = ( 'parameters' => 1, + 'parameter' => 1, + 'params' => 1, + 'param' => 1, + 'arguments' => 1, + 'argument' => 1, + 'args' => 1, + 'arg' => 1 ); + + +############################################################################### +# Group: Interface Functions + + +# +# Function: Start +# +# This will be called whenever a file is about to be parsed. It allows the package to reset its internal state. +# +sub Start + { + my ($self) = @_; + $package = undef; + }; + + +# +# Function: IsMine +# +# Examines the comment and returns whether it is *definitely* Natural Docs content, i.e. it is owned by this package. Note +# that a comment can fail this function and still be interpreted as a Natural Docs content, for example a JavaDoc-styled comment +# that doesn't have header lines but no JavaDoc tags either. +# +# Parameters: +# +# commentLines - An arrayref of the comment lines. Must have been run through <NaturalDocs::Parser->CleanComment()>. +# isJavaDoc - Whether the comment was JavaDoc-styled. +# +# Returns: +# +# Whether the comment is *definitely* Natural Docs content. +# +sub IsMine #(string[] commentLines, bool isJavaDoc) + { + my ($self, $commentLines, $isJavaDoc) = @_; + + # Skip to the first line with content. + my $line = 0; + + while ($line < scalar @$commentLines && !length $commentLines->[$line]) + { $line++; }; + + return $self->ParseHeaderLine($commentLines->[$line]); + }; + + + +# +# Function: ParseComment +# +# This will be called whenever a comment capable of containing Natural Docs content is found. +# +# Parameters: +# +# commentLines - An arrayref of the comment lines. Must have been run through <NaturalDocs::Parser->CleanComment()>. +# *The original memory will be changed.* +# isJavaDoc - Whether the comment is JavaDoc styled. +# lineNumber - The line number of the first of the comment lines. +# parsedTopics - A reference to the array where any new <NaturalDocs::Parser::ParsedTopics> should be placed. +# +# Returns: +# +# The number of parsed topics added to the array, or zero if none. +# +sub ParseComment #(commentLines, isJavaDoc, lineNumber, parsedTopics) + { + my ($self, $commentLines, $isJavaDoc, $lineNumber, $parsedTopics) = @_; + + my $topicCount = 0; + my $prevLineBlank = 1; + my $inCodeSection = 0; + + my ($type, $scope, $isPlural, $title, $symbol); + #my $package; # package variable. + my ($newKeyword, $newTitle); + + my $index = 0; + + my $bodyStart = 0; + my $bodyEnd = 0; # Not inclusive. + + while ($index < scalar @$commentLines) + { + # Everything but leading whitespace was removed beforehand. + + # If we're in a code section... + if ($inCodeSection) + { + if ($commentLines->[$index] =~ /^ *\( *(?:end|finish|done)(?: +(?:table|code|example|diagram))? *\)$/i) + { $inCodeSection = undef; }; + + $prevLineBlank = 0; + $bodyEnd++; + } + + # If the line is empty... + elsif (!length($commentLines->[$index])) + { + $prevLineBlank = 1; + + if ($topicCount) + { $bodyEnd++; }; + } + + # If the line has a recognized header and the previous line is blank... + elsif ($prevLineBlank && (($newKeyword, $newTitle) = $self->ParseHeaderLine($commentLines->[$index])) ) + { + # Process the previous one, if any. + + if ($topicCount) + { + if ($scope == ::SCOPE_START() || $scope == ::SCOPE_END()) + { $package = undef; }; + + my $body = $self->FormatBody($commentLines, $bodyStart, $bodyEnd, $type, $isPlural); + my $newTopic = $self->MakeParsedTopic($type, $title, $package, $body, $lineNumber + $bodyStart - 1, $isPlural); + push @$parsedTopics, $newTopic; + + $package = $newTopic->Package(); + }; + + $title = $newTitle; + + my $typeInfo; + ($type, $typeInfo, $isPlural) = NaturalDocs::Topics->KeywordInfo($newKeyword); + $scope = $typeInfo->Scope(); + + $bodyStart = $index + 1; + $bodyEnd = $index + 1; + + $topicCount++; + + $prevLineBlank = 0; + } + + # If we're on a non-empty, non-header line of a JavaDoc-styled comment and we haven't started a topic yet... + elsif ($isJavaDoc && !$topicCount) + { + $type = undef; + $scope = ::SCOPE_NORMAL(); # The scope repair and topic merging processes will handle if this is a class topic. + $isPlural = undef; + $title = undef; + $symbol = undef; + + $bodyStart = $index; + $bodyEnd = $index + 1; + + $topicCount++; + + $prevLineBlank = undef; + } + + # If we're on a normal content line within a topic + elsif ($topicCount) + { + $prevLineBlank = 0; + $bodyEnd++; + + if ($commentLines->[$index] =~ /^ *\( *(?:(?:start|begin)? +)?(?:table|code|example|diagram) *\)$/i) + { $inCodeSection = 1; }; + }; + + + $index++; + }; + + + # Last one, if any. This is the only one that gets the prototypes. + if ($bodyStart) + { + if ($scope == ::SCOPE_START() || $scope == ::SCOPE_END()) + { $package = undef; }; + + my $body = $self->FormatBody($commentLines, $bodyStart, $bodyEnd, $type, $isPlural); + my $newTopic = $self->MakeParsedTopic($type, $title, $package, $body, $lineNumber + $bodyStart - 1, $isPlural); + push @$parsedTopics, $newTopic; + $topicCount++; + + $package = $newTopic->Package(); + }; + + return $topicCount; + }; + + +# +# Function: ParseHeaderLine +# +# If the passed line is a topic header, returns the array ( keyword, title ). Otherwise returns an empty array. +# +sub ParseHeaderLine #(line) + { + my ($self, $line) = @_; + + if ($line =~ /^ *([a-z0-9 ]*[a-z0-9]): +(.*)$/i) + { + my ($keyword, $title) = ($1, $2); + + # We need to do it this way because if you do "if (ND:T->KeywordInfo($keyword)" and the last element of the array it + # returns is false, the statement is false. That is really retarded, but there it is. + my ($type, undef, undef) = NaturalDocs::Topics->KeywordInfo($keyword); + + if ($type) + { return ($keyword, $title); } + else + { return ( ); }; + } + else + { return ( ); }; + }; + + + +############################################################################### +# Group: Support Functions + + +# +# Function: MakeParsedTopic +# +# Creates a <NaturalDocs::Parser::ParsedTopic> object for the passed parameters. Scope is gotten from +# the package variable <package> instead of from the parameters. The summary is generated from the body. +# +# Parameters: +# +# type - The <TopicType>. May be undef for headerless topics. +# title - The title of the topic. May be undef for headerless topics. +# package - The package <SymbolString> the topic appears in. +# body - The topic's body in <NDMarkup>. +# lineNumber - The topic's line number. +# isList - Whether the topic is a list. +# +# Returns: +# +# The <NaturalDocs::Parser::ParsedTopic> object. +# +sub MakeParsedTopic #(type, title, package, body, lineNumber, isList) + { + my ($self, $type, $title, $package, $body, $lineNumber, $isList) = @_; + + my $summary; + + if (defined $body) + { $summary = NaturalDocs::Parser->GetSummaryFromBody($body); }; + + return NaturalDocs::Parser::ParsedTopic->New($type, $title, $package, undef, undef, $summary, + $body, $lineNumber, $isList); + }; + + +# +# Function: FormatBody +# +# Converts the section body to <NDMarkup>. +# +# Parameters: +# +# commentLines - The arrayref of comment lines. +# startingIndex - The starting index of the body to format. +# endingIndex - The ending index of the body to format, *not* inclusive. +# type - The type of the section. May be undef for headerless comments. +# isList - Whether it's a list topic. +# +# Returns: +# +# The body formatted in <NDMarkup>. +# +sub FormatBody #(commentLines, startingIndex, endingIndex, type, isList) + { + my ($self, $commentLines, $startingIndex, $endingIndex, $type, $isList) = @_; + + use constant TAG_NONE => 1; + use constant TAG_PARAGRAPH => 2; + use constant TAG_BULLETLIST => 3; + use constant TAG_DESCRIPTIONLIST => 4; + use constant TAG_HEADING => 5; + use constant TAG_PREFIXCODE => 6; + use constant TAG_TAGCODE => 7; + + my %tagEnders = ( TAG_NONE() => '', + TAG_PARAGRAPH() => '</p>', + TAG_BULLETLIST() => '</li></ul>', + TAG_DESCRIPTIONLIST() => '</dd></dl>', + TAG_HEADING() => '</h>', + TAG_PREFIXCODE() => '</code>', + TAG_TAGCODE() => '</code>' ); + + my $topLevelTag = TAG_NONE; + + my $output; + my $textBlock; + my $prevLineBlank = 1; + + my $codeBlock; + my $removedCodeSpaces; + + my $ignoreListSymbols; + + my $index = $startingIndex; + + while ($index < $endingIndex) + { + # If we're in a tagged code section... + if ($topLevelTag == TAG_TAGCODE) + { + if ($commentLines->[$index] =~ /^ *\( *(?:end|finish|done)(?: +(?:table|code|example|diagram))? *\)$/i) + { + $codeBlock =~ s/\n+$//; + $output .= NaturalDocs::NDMarkup->ConvertAmpChars($codeBlock) . '</code>'; + $codeBlock = undef; + $topLevelTag = TAG_NONE; + $prevLineBlank = undef; + } + else + { + $self->AddToCodeBlock($commentLines->[$index], \$codeBlock, \$removedCodeSpaces); + }; + } + + # If the line starts with a code designator... + elsif ($commentLines->[$index] =~ /^ *[>:|](.*)$/) + { + my $code = $1; + + if ($topLevelTag == TAG_PREFIXCODE) + { + $self->AddToCodeBlock($code, \$codeBlock, \$removedCodeSpaces); + } + else # $topLevelTag != TAG_PREFIXCODE + { + if (defined $textBlock) + { + $output .= $self->RichFormatTextBlock($textBlock) . $tagEnders{$topLevelTag}; + $textBlock = undef; + }; + + $topLevelTag = TAG_PREFIXCODE; + $output .= '<code>'; + $self->AddToCodeBlock($code, \$codeBlock, \$removedCodeSpaces); + }; + } + + # If we're not in either code style... + else + { + # Strip any leading whitespace. + $commentLines->[$index] =~ s/^ +//; + + # If we were in a prefixed code section... + if ($topLevelTag == TAG_PREFIXCODE) + { + $codeBlock =~ s/\n+$//; + $output .= NaturalDocs::NDMarkup->ConvertAmpChars($codeBlock) . '</code>'; + $codeBlock = undef; + $topLevelTag = TAG_NONE; + $prevLineBlank = undef; + }; + + + # If the line is blank... + if (!length($commentLines->[$index])) + { + # End a paragraph. Everything else ignores it for now. + if ($topLevelTag == TAG_PARAGRAPH) + { + $output .= $self->RichFormatTextBlock($textBlock) . '</p>'; + $textBlock = undef; + $topLevelTag = TAG_NONE; + }; + + $prevLineBlank = 1; + } + + # If the line starts with a bullet... + elsif ($commentLines->[$index] =~ /^[-\*o+] +([^ ].*)$/ && + substr($1, 0, 2) ne '- ') # Make sure "o - Something" is a definition, not a bullet. + { + my $bulletedText = $1; + + if (defined $textBlock) + { $output .= $self->RichFormatTextBlock($textBlock); }; + + if ($topLevelTag == TAG_BULLETLIST) + { + $output .= '</li><li>'; + } + else #($topLevelTag != TAG_BULLETLIST) + { + $output .= $tagEnders{$topLevelTag} . '<ul><li>'; + $topLevelTag = TAG_BULLETLIST; + }; + + $textBlock = $bulletedText; + + $prevLineBlank = undef; + } + + # If the line looks like a description list entry... + elsif ($commentLines->[$index] =~ /^(.+?) +- +([^ ].*)$/ && $topLevelTag != TAG_PARAGRAPH) + { + my $entry = $1; + my $description = $2; + + if (defined $textBlock) + { $output .= $self->RichFormatTextBlock($textBlock); }; + + if ($topLevelTag == TAG_DESCRIPTIONLIST) + { + $output .= '</dd>'; + } + else #($topLevelTag != TAG_DESCRIPTIONLIST) + { + $output .= $tagEnders{$topLevelTag} . '<dl>'; + $topLevelTag = TAG_DESCRIPTIONLIST; + }; + + if (($isList && !$ignoreListSymbols) || $type eq ::TOPIC_ENUMERATION()) + { + $output .= '<ds>' . NaturalDocs::NDMarkup->ConvertAmpChars($entry) . '</ds><dd>'; + } + else + { + $output .= '<de>' . NaturalDocs::NDMarkup->ConvertAmpChars($entry) . '</de><dd>'; + }; + + $textBlock = $description; + + $prevLineBlank = undef; + } + + # If the line could be a header... + elsif ($prevLineBlank && $commentLines->[$index] =~ /^(.*)([^ ]):$/) + { + my $headerText = $1 . $2; + + if (defined $textBlock) + { + $output .= $self->RichFormatTextBlock($textBlock); + $textBlock = undef; + } + + $output .= $tagEnders{$topLevelTag}; + $topLevelTag = TAG_NONE; + + $output .= '<h>' . $self->RichFormatTextBlock($headerText) . '</h>'; + + if ($type eq ::TOPIC_FUNCTION() && $isList) + { + $ignoreListSymbols = exists $functionListIgnoredHeadings{lc($headerText)}; + }; + + $prevLineBlank = undef; + } + + # If the line looks like a code tag... + elsif ($commentLines->[$index] =~ /^\( *(?:(?:start|begin)? +)?(?:table|code|example|diagram) *\)$/i) + { + if (defined $textBlock) + { + $output .= $self->RichFormatTextBlock($textBlock); + $textBlock = undef; + }; + + $output .= $tagEnders{$topLevelTag} . '<code>'; + $topLevelTag = TAG_TAGCODE; + } + + # If the line looks like an inline image... + elsif ($commentLines->[$index] =~ /^(\( *see +)([^\)]+?)( *\))$/i) + { + if (defined $textBlock) + { + $output .= $self->RichFormatTextBlock($textBlock); + $textBlock = undef; + }; + + $output .= $tagEnders{$topLevelTag}; + $topLevelTag = TAG_NONE; + + $output .= '<img mode="inline" target="' . NaturalDocs::NDMarkup->ConvertAmpChars($2) . '" ' + . 'original="' . NaturalDocs::NDMarkup->ConvertAmpChars($1 . $2 . $3) . '">'; + + $prevLineBlank = undef; + } + + # If the line isn't any of those, we consider it normal text. + else + { + # A blank line followed by normal text ends lists. We don't handle this when we detect if the line's blank because + # we don't want blank lines between list items to break the list. + if ($prevLineBlank && ($topLevelTag == TAG_BULLETLIST || $topLevelTag == TAG_DESCRIPTIONLIST)) + { + $output .= $self->RichFormatTextBlock($textBlock) . $tagEnders{$topLevelTag} . '<p>'; + + $topLevelTag = TAG_PARAGRAPH; + $textBlock = undef; + } + + elsif ($topLevelTag == TAG_NONE) + { + $output .= '<p>'; + $topLevelTag = TAG_PARAGRAPH; + # textBlock will already be undef. + }; + + if (defined $textBlock) + { $textBlock .= ' '; }; + + $textBlock .= $commentLines->[$index]; + + $prevLineBlank = undef; + }; + }; + + $index++; + }; + + # Clean up anything left dangling. + if (defined $textBlock) + { + $output .= $self->RichFormatTextBlock($textBlock) . $tagEnders{$topLevelTag}; + } + elsif (defined $codeBlock) + { + $codeBlock =~ s/\n+$//; + $output .= NaturalDocs::NDMarkup->ConvertAmpChars($codeBlock) . '</code>'; + }; + + return $output; + }; + + +# +# Function: AddToCodeBlock +# +# Adds a line of text to a code block, handling all the indentation processing required. +# +# Parameters: +# +# line - The line of text to add. +# codeBlockRef - A reference to the code block to add it to. +# removedSpacesRef - A reference to a variable to hold the number of spaces removed. It needs to be stored between calls. +# It will reset itself automatically when the code block codeBlockRef points to is undef. +# +sub AddToCodeBlock #(line, codeBlockRef, removedSpacesRef) + { + my ($self, $line, $codeBlockRef, $removedSpacesRef) = @_; + + $line =~ /^( *)(.*)$/; + my ($spaces, $code) = ($1, $2); + + if (!defined $$codeBlockRef) + { + if (length($code)) + { + $$codeBlockRef = $code . "\n"; + $$removedSpacesRef = length($spaces); + }; + # else ignore leading line breaks. + } + + elsif (length $code) + { + # Make sure we have the minimum amount of spaces to the left possible. + if (length($spaces) != $$removedSpacesRef) + { + my $spaceDifference = abs( length($spaces) - $$removedSpacesRef ); + my $spacesToAdd = ' ' x $spaceDifference; + + if (length($spaces) > $$removedSpacesRef) + { + $$codeBlockRef .= $spacesToAdd; + } + else + { + $$codeBlockRef =~ s/^(.)/$spacesToAdd . $1/gme; + $$removedSpacesRef = length($spaces); + }; + }; + + $$codeBlockRef .= $code . "\n"; + } + + else # (!length $code) + { + $$codeBlockRef .= "\n"; + }; + }; + + +# +# Function: RichFormatTextBlock +# +# Applies rich <NDMarkup> formatting to a chunk of text. This includes both amp chars, formatting tags, and link tags. +# +# Parameters: +# +# text - The block of text to format. +# +# Returns: +# +# The formatted text block. +# +sub RichFormatTextBlock #(text) + { + my ($self, $text) = @_; + my $output; + + + # First find bare urls, e-mail addresses, and images. We have to do this before the split because they may contain underscores + # or asterisks. We have to mark the tags with \x1E and \x1F so they don't get confused with angle brackets from the comment. + # We can't convert the amp chars beforehand because we need lookbehinds in the regexps below and they need to be + # constant length. Sucks, huh? + + $text =~ s{ + # The previous character can't be an alphanumeric or an opening angle bracket. + (?<! [a-z0-9<] ) + + # Optional mailto:. Ignored in output. + (?:mailto\:)? + + # Begin capture + ( + + # The user portion. Alphanumeric and - _. Dots can appear between, but not at the edges or more than + # one in a row. + (?: [a-z0-9\-_]+ \. )* [a-z0-9\-_]+ + + @ + + # The domain. Alphanumeric and -. Dots same as above, however, there must be at least two sections + # and the last one must be two to four alphanumeric characters (.com, .uk, .info, .203 for IP addresses) + (?: [a-z0-9\-]+ \. )+ [a-z]{2,4} + + # End capture. + ) + + # The next character can't be an alphanumeric, which should prevent .abcde from matching the two to + # four character requirement, or a closing angle bracket. + (?! [a-z0-9>] ) + + } + + {"\x1E" . 'email target="' . NaturalDocs::NDMarkup->ConvertAmpChars($1) . '" ' + . 'name="' . NaturalDocs::NDMarkup->ConvertAmpChars($1) . '"' . "\x1F"}igxe; + + $text =~ s{ + # The previous character can't be an alphanumeric or an opening angle bracket. + (?<! [a-z0-9<] ) + + # Begin capture. + ( + + # URL must start with one of the acceptable protocols. + (?:http|https|ftp|news|file)\: + + # The acceptable URL characters as far as I know. + [a-z0-9\-\=\~\@\#\%\&\_\+\/\;\:\?\*\.\,]* + + # The URL characters minus period and comma. If it ends on them, they're probably intended as + # punctuation. + [a-z0-9\-\=\~\@\#\%\&\_\+\/\;\:\?\*] + + # End capture. + ) + + # The next character must not be an acceptable character or a closing angle bracket. This will prevent the URL + # from ending early just to get a match. + (?! [a-z0-9\-\=\~\@\#\%\&\_\+\/\;\:\?\*\>] ) + + } + + {"\x1E" . 'url target="' . NaturalDocs::NDMarkup->ConvertAmpChars($1) . '" ' + . 'name="' . NaturalDocs::NDMarkup->ConvertAmpChars($1) . '"' . "\x1F"}igxe; + + + # Find image links. Inline images should already be pulled out by now. + + $text =~ s{(\( *see +)([^\)]+?)( *\))} + {"\x1E" . 'img mode="link" target="' . NaturalDocs::NDMarkup->ConvertAmpChars($2) . '" ' + . 'original="' . NaturalDocs::NDMarkup->ConvertAmpChars($1 . $2 . $3) . '"' . "\x1F"}gie; + + + + # Split the text from the potential tags. + + my @tempTextBlocks = split(/([\*_<>\x1E\x1F])/, $text); + + # Since the symbols are considered dividers, empty strings could appear between two in a row or at the beginning/end of the + # array. This could seriously screw up TagType(), so we need to get rid of them. + my @textBlocks; + + while (scalar @tempTextBlocks) + { + my $tempTextBlock = shift @tempTextBlocks; + + if (length $tempTextBlock) + { push @textBlocks, $tempTextBlock; }; + }; + + + my $bold; + my $underline; + my $underlineHasWhitespace; + + my $index = 0; + + while ($index < scalar @textBlocks) + { + if ($textBlocks[$index] eq "\x1E") + { + $output .= '<'; + $index++; + + while ($textBlocks[$index] ne "\x1F") + { + $output .= $textBlocks[$index]; + $index++; + }; + + $output .= '>'; + } + + elsif ($textBlocks[$index] eq '<' && $self->TagType(\@textBlocks, $index) == POSSIBLE_OPENING_TAG) + { + my $endingIndex = $self->ClosingTag(\@textBlocks, $index, undef); + + if ($endingIndex != -1) + { + my $linkText; + $index++; + + while ($index < $endingIndex) + { + $linkText .= $textBlocks[$index]; + $index++; + }; + # Index will be incremented again at the end of the loop. + + $linkText = NaturalDocs::NDMarkup->ConvertAmpChars($linkText); + + if ($linkText =~ /^(?:mailto\:)?((?:[a-z0-9\-_]+\.)*[a-z0-9\-_]+@(?:[a-z0-9\-]+\.)+[a-z]{2,4})$/i) + { $output .= '<email target="' . $1 . '" name="' . $1 . '">'; } + elsif ($linkText =~ /^(?:http|https|ftp|news|file)\:/i) + { $output .= '<url target="' . $linkText . '" name="' . $linkText . '">'; } + else + { $output .= '<link target="' . $linkText . '" name="' . $linkText . '" original="<' . $linkText . '>">'; }; + } + + else # it's not a link. + { + $output .= '<'; + }; + } + + elsif ($textBlocks[$index] eq '*') + { + my $tagType = $self->TagType(\@textBlocks, $index); + + if ($tagType == POSSIBLE_OPENING_TAG && $self->ClosingTag(\@textBlocks, $index, undef) != -1) + { + # ClosingTag() makes sure tags aren't opened multiple times in a row. + $bold = 1; + $output .= '<b>'; + } + elsif ($bold && $tagType == POSSIBLE_CLOSING_TAG) + { + $bold = undef; + $output .= '</b>'; + } + else + { + $output .= '*'; + }; + } + + elsif ($textBlocks[$index] eq '_') + { + my $tagType = $self->TagType(\@textBlocks, $index); + + if ($tagType == POSSIBLE_OPENING_TAG && $self->ClosingTag(\@textBlocks, $index, \$underlineHasWhitespace) != -1) + { + # ClosingTag() makes sure tags aren't opened multiple times in a row. + $underline = 1; + #underlineHasWhitespace is set by ClosingTag(). + $output .= '<u>'; + } + elsif ($underline && $tagType == POSSIBLE_CLOSING_TAG) + { + $underline = undef; + #underlineHasWhitespace will be reset by the next opening underline. + $output .= '</u>'; + } + elsif ($underline && !$underlineHasWhitespace) + { + # If there's no whitespace between underline tags, all underscores are replaced by spaces so + # _some_underlined_text_ becomes <u>some underlined text</u>. The standard _some underlined text_ + # will work too. + $output .= ' '; + } + else + { + $output .= '_'; + }; + } + + else # plain text or a > that isn't part of a link + { + $output .= NaturalDocs::NDMarkup->ConvertAmpChars($textBlocks[$index]); + }; + + $index++; + }; + + return $output; + }; + + +# +# Function: TagType +# +# Returns whether the tag is a possible opening or closing tag, or neither. "Possible" because it doesn't check if an opening tag is +# closed or a closing tag is opened, just whether the surrounding characters allow it to be a candidate for a tag. For example, in +# "A _B" the underscore is a possible opening underline tag, but in "A_B" it is not. Support function for <RichFormatTextBlock()>. +# +# Parameters: +# +# textBlocks - A reference to an array of text blocks. +# index - The index of the tag. +# +# Returns: +# +# POSSIBLE_OPENING_TAG, POSSIBLE_CLOSING_TAG, or NOT_A_TAG. +# +sub TagType #(textBlocks, index) + { + my ($self, $textBlocks, $index) = @_; + + + # Possible opening tags + + if ( ( $textBlocks->[$index] =~ /^[\*_<]$/ ) && + + # Before it must be whitespace, the beginning of the text, or ({["'-/*_. + ( $index == 0 || $textBlocks->[$index-1] =~ /[\ \t\n\(\{\[\"\'\-\/\*\_]$/ ) && + + # Notes for 2.0: Include Spanish upside down ! and ? as well as opening quotes (66) and apostrophes (6). Look into + # Unicode character classes as well. + + # After it must be non-whitespace. + ( $index + 1 < scalar @$textBlocks && $textBlocks->[$index+1] !~ /^[\ \t\n]/) && + + # Make sure we don't accept <<, <=, <-, or *= as opening tags. + ( $textBlocks->[$index] ne '<' || $textBlocks->[$index+1] !~ /^[<=-]/ ) && + ( $textBlocks->[$index] ne '*' || $textBlocks->[$index+1] !~ /^[\=\*]/ ) && + + # Make sure we don't accept * or _ before it unless it's <. + ( $textBlocks->[$index] eq '<' || $index == 0 || $textBlocks->[$index-1] !~ /[\*\_]$/) ) + { + return POSSIBLE_OPENING_TAG; + } + + + # Possible closing tags + + elsif ( ( $textBlocks->[$index] =~ /^[\*_>]$/) && + + # After it must be whitespace, the end of the text, or )}].,!?"';:-/*_. + ( $index + 1 == scalar @$textBlocks || $textBlocks->[$index+1] =~ /^[ \t\n\)\]\}\.\,\!\?\"\'\;\:\-\/\*\_]/ || + # Links also get plurals, like <link>s, <linx>es, <link>'s, and <links>'. + ( $textBlocks->[$index] eq '>' && $textBlocks->[$index+1] =~ /^(?:es|s|\')/ ) ) && + + # Notes for 2.0: Include closing quotes (99) and apostrophes (9). Look into Unicode character classes as well. + + # Before it must be non-whitespace. + ( $index != 0 && $textBlocks->[$index-1] !~ /[ \t\n]$/ ) && + + # Make sure we don't accept >>, ->, or => as closing tags. >= is already taken care of. + ( $textBlocks->[$index] ne '>' || $textBlocks->[$index-1] !~ /[>=-]$/ ) && + + # Make sure we don't accept * or _ after it unless it's >. + ( $textBlocks->[$index] eq '>' || $textBlocks->[$index+1] !~ /[\*\_]$/) ) + { + return POSSIBLE_CLOSING_TAG; + } + + else + { + return NOT_A_TAG; + }; + + }; + + +# +# Function: ClosingTag +# +# Returns whether a tag is closed or not, where it's closed if it is, and optionally whether there is any whitespace between the +# tags. Support function for <RichFormatTextBlock()>. +# +# The results of this function are in full context, meaning that if it says a tag is closed, it can be interpreted as that tag in the +# final output. It takes into account any spoiling factors, like there being two opening tags in a row. +# +# Parameters: +# +# textBlocks - A reference to an array of text blocks. +# index - The index of the opening tag. +# hasWhitespaceRef - A reference to the variable that will hold whether there is whitespace between the tags or not. If +# undef, the function will not check. If the tag is not closed, the variable will not be changed. +# +# Returns: +# +# If the tag is closed, it returns the index of the closing tag and puts whether there was whitespace between the tags in +# hasWhitespaceRef if it was specified. If the tag is not closed, it returns -1 and doesn't touch the variable pointed to by +# hasWhitespaceRef. +# +sub ClosingTag #(textBlocks, index, hasWhitespace) + { + my ($self, $textBlocks, $index, $hasWhitespaceRef) = @_; + + my $hasWhitespace; + my $closingTag; + + if ($textBlocks->[$index] eq '*' || $textBlocks->[$index] eq '_') + { $closingTag = $textBlocks->[$index]; } + elsif ($textBlocks->[$index] eq '<') + { $closingTag = '>'; } + else + { return -1; }; + + my $beginningIndex = $index; + $index++; + + while ($index < scalar @$textBlocks) + { + if ($textBlocks->[$index] eq '<' && $self->TagType($textBlocks, $index) == POSSIBLE_OPENING_TAG) + { + # If we hit a < and we're checking whether a link is closed, it's not. The first < becomes literal and the second one + # becomes the new link opening. + if ($closingTag eq '>') + { + return -1; + } + + # If we're not searching for the end of a link, we have to skip the link because formatting tags cannot appear within + # them. That's of course provided it's closed. + else + { + my $linkHasWhitespace; + + my $endIndex = $self->ClosingTag($textBlocks, $index, + ($hasWhitespaceRef && !$hasWhitespace ? \$linkHasWhitespace : undef) ); + + if ($endIndex != -1) + { + if ($linkHasWhitespace) + { $hasWhitespace = 1; }; + + # index will be incremented again at the end of the loop, which will bring us past the link's >. + $index = $endIndex; + }; + }; + } + + elsif ($textBlocks->[$index] eq $closingTag) + { + my $tagType = $self->TagType($textBlocks, $index); + + if ($tagType == POSSIBLE_CLOSING_TAG) + { + # There needs to be something between the tags for them to count. + if ($index == $beginningIndex + 1) + { return -1; } + else + { + # Success! + + if ($hasWhitespaceRef) + { $$hasWhitespaceRef = $hasWhitespace; }; + + return $index; + }; + } + + # If there are two opening tags of the same type, the first becomes literal and the next becomes part of a tag. + elsif ($tagType == POSSIBLE_OPENING_TAG) + { return -1; } + } + + elsif ($hasWhitespaceRef && !$hasWhitespace) + { + if ($textBlocks->[$index] =~ /[ \t\n]/) + { $hasWhitespace = 1; }; + }; + + $index++; + }; + + # Hit the end of the text blocks if we're here. + return -1; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Parser/ParsedTopic.pm b/docs/tool/Modules/NaturalDocs/Parser/ParsedTopic.pm new file mode 100644 index 00000000..a08d65ad --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Parser/ParsedTopic.pm @@ -0,0 +1,253 @@ +############################################################################### +# +# Package: NaturalDocs::Parser::ParsedTopic +# +############################################################################### +# +# A class for parsed topics of source files. Also encompasses some of the <TopicType>-specific behavior. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Parser::ParsedTopic; + + +############################################################################### +# Group: Implementation + +# +# Constants: Members +# +# The object is a blessed arrayref with the following indexes. +# +# TYPE - The <TopicType>. +# TITLE - The title of the topic. +# PACKAGE - The package <SymbolString> the topic appears in, or undef if none. +# USING - An arrayref of additional package <SymbolStrings> available to the topic via "using" statements, or undef if +# none. +# PROTOTYPE - The prototype, if it exists and is applicable. +# SUMMARY - The summary, if it exists. +# BODY - The body of the topic, formatted in <NDMarkup>. Some topics may not have bodies, and if not, this +# will be undef. +# LINE_NUMBER - The line number the topic appears at in the file. +# IS_LIST - Whether the topic is a list. +# +use NaturalDocs::DefineMembers 'TYPE', 'TITLE', 'PACKAGE', 'USING', 'PROTOTYPE', 'SUMMARY', 'BODY', + 'LINE_NUMBER', 'IS_LIST'; +# DEPENDENCY: New() depends on the order of these constants, and that this class is not inheriting any members. + + +# +# Architecture: Title, Package, and Symbol Behavior +# +# Title, package, and symbol behavior is a little awkward so it deserves some explanation. Basically you set them according to +# certain rules, but you get computed values that try to hide all the different scoping situations. +# +# Normal Topics: +# +# Set them to the title and package as they appear. "Function" and "PkgA.PkgB" will return "Function" for the title, +# "PkgA.PkgB" for the package, and "PkgA.PkgB.Function" for the symbol. +# +# In the rare case that a title has a separator symbol it's treated as inadvertant, so "A vs. B" in "PkgA.PkgB" still returns just +# "PkgA.PkgB" for the package even though if you got it from the symbol it can be seen as "PkgA.PkgB.A vs". +# +# Scope Topics: +# +# Set the title normally and leave the package undef. So "PkgA.PkgB" and undef will return "PkgA.PkgB" for the title as well +# as for the package and symbol. +# +# The only time you should set the package is when you have full language support and they only documented the class with +# a partial title. So if you documented "PkgA.PkgB" with just "PkgB", you want to set the package to "PkgA". This +# will return "PkgB" as the title for presentation and will return "PkgA.PkgB" for the package and symbol, which is correct. +# +# Always Global Topics: +# +# Set the title and package normally, do not set the package to undef. So "Global" and "PkgA.PkgB" will return "Global" as +# the title, "PkgA.PkgB" as the package, and "Global" as the symbol. +# +# Um, yeah...: +# +# So does this suck? Yes, yes it does. But the suckiness is centralized here instead of having to be handled everywhere these +# issues come into play. Just realize there are a certain set of rules to follow when you *set* these variables, and the results +# you see when you *get* them are computed rather than literal. +# + + +############################################################################### +# Group: Functions + +# +# Function: New +# +# Creates a new object. +# +# Parameters: +# +# type - The <TopicType>. +# title - The title of the topic. +# package - The package <SymbolString> the topic appears in, or undef if none. +# using - An arrayref of additional package <SymbolStrings> available to the topic via "using" statements, or undef if +# none. +# prototype - The prototype, if it exists and is applicable. Otherwise set to undef. +# summary - The summary of the topic, if any. +# body - The body of the topic, formatted in <NDMarkup>. May be undef, as some topics may not have bodies. +# lineNumber - The line number the topic appears at in the file. +# isList - Whether the topic is a list topic or not. +# +# Returns: +# +# The new object. +# +sub New #(type, title, package, using, prototype, summary, body, lineNumber, isList) + { + # DEPENDENCY: This depends on the order of the parameter list being the same as the constants, and that there are no + # members inherited from a base class. + + my $package = shift; + + my $object = [ @_ ]; + bless $object, $package; + + if (defined $object->[USING]) + { $object->[USING] = [ @{$object->[USING]} ]; }; + + return $object; + }; + + +# Function: Type +# Returns the <TopicType>. +sub Type + { return $_[0]->[TYPE]; }; + +# Function: SetType +# Replaces the <TopicType>. +sub SetType #(type) + { $_[0]->[TYPE] = $_[1]; }; + +# Function: IsList +# Returns whether the topic is a list. +sub IsList + { return $_[0]->[IS_LIST]; }; + +# Function: SetIsList +# Sets whether the topic is a list. +sub SetIsList + { $_[0]->[IS_LIST] = $_[1]; }; + +# Function: Title +# Returns the title of the topic. +sub Title + { return $_[0]->[TITLE]; }; + +# Function: SetTitle +# Replaces the topic title. +sub SetTitle #(title) + { $_[0]->[TITLE] = $_[1]; }; + +# +# Function: Symbol +# +# Returns the <SymbolString> defined by the topic. It is fully resolved and does _not_ need to be joined with <Package()>. +# +# Type-Specific Behavior: +# +# - If the <TopicType> is always global, the symbol will be generated from the title only. +# - Everything else's symbols will be generated from the title and the package passed to <New()>. +# +sub Symbol + { + my ($self) = @_; + + my $titleSymbol = NaturalDocs::SymbolString->FromText($self->[TITLE]); + + if (NaturalDocs::Topics->TypeInfo($self->Type())->Scope() == ::SCOPE_ALWAYS_GLOBAL()) + { return $titleSymbol; } + else + { + return NaturalDocs::SymbolString->Join( $self->[PACKAGE], $titleSymbol ); + }; + }; + + +# +# Function: Package +# +# Returns the package <SymbolString> that the topic appears in. +# +# Type-Specific Behavior: +# +# - If the <TopicType> has scope, the package will be generated from both the title and the package passed to <New()>, not +# just the package. +# - If the <TopicType> is always global, the package will be the one passed to <New()>, even though it isn't part of it's +# <Symbol()>. +# - Everything else's package will be what was passed to <New()>, even if the title has separator symbols in it. +# +sub Package + { + my ($self) = @_; + + # Headerless topics may not have a type yet. + if ($self->Type() && NaturalDocs::Topics->TypeInfo($self->Type())->Scope() == ::SCOPE_START()) + { return $self->Symbol(); } + else + { return $self->[PACKAGE]; }; + }; + + +# Function: SetPackage +# Replaces the package the topic appears in. This will behave the same way as the package parameter in <New()>. Later calls +# to <Package()> will still be generated according to its type-specific behavior. +sub SetPackage #(package) + { $_[0]->[PACKAGE] = $_[1]; }; + +# Function: Using +# Returns an arrayref of additional scope <SymbolStrings> available to the topic via "using" statements, or undef if none. +sub Using + { return $_[0]->[USING]; }; + +# Function: SetUsing +# Replaces the using arrayref of sope <SymbolStrings>. +sub SetUsing #(using) + { $_[0]->[USING] = $_[1]; }; + +# Function: Prototype +# Returns the prototype if one is defined. Will be undef otherwise. +sub Prototype + { return $_[0]->[PROTOTYPE]; }; + +# Function: SetPrototype +# Replaces the function or variable prototype. +sub SetPrototype #(prototype) + { $_[0]->[PROTOTYPE] = $_[1]; }; + +# Function: Summary +# Returns the topic summary, if it exists, formatted in <NDMarkup>. +sub Summary + { return $_[0]->[SUMMARY]; }; + +# Function: Body +# Returns the topic's body, formatted in <NDMarkup>. May be undef. +sub Body + { return $_[0]->[BODY]; }; + +# Function: SetBody +# Replaces the topic's body, formatted in <NDMarkup>. May be undef. +sub SetBody #(body) + { + my ($self, $body) = @_; + $self->[BODY] = $body; + }; + +# Function: LineNumber +# Returns the line the topic appears at in the file. +sub LineNumber + { return $_[0]->[LINE_NUMBER]; }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Project.pm b/docs/tool/Modules/NaturalDocs/Project.pm new file mode 100644 index 00000000..e4d94bad --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Project.pm @@ -0,0 +1,1402 @@ +############################################################################### +# +# Package: NaturalDocs::Project +# +############################################################################### +# +# A package that manages information about the files in the source tree, as well as the list of files that have to be parsed +# and built. +# +# Usage and Dependencies: +# +# - All the <Config and Data File Functions> are available immediately, except for the status functions. +# +# - <ReparseEverything()> and <RebuildEverything()> are available immediately, because they may need to be called +# after <LoadConfigFileInfo()> but before <LoadSourceFileInfo()>. +# +# - Prior to <LoadConfigFileInfo()>, <NaturalDocs::Settings> must be initialized. +# +# - After <LoadConfigFileInfo()>, the status <Config and Data File Functions> are available as well. +# +# - Prior to <LoadSourceFileInfo()>, <NaturalDocs::Settings> and <NaturalDocs::Languages> must be initialized. +# +# - After <LoadSourceFileInfo()>, the rest of the <Source File Functions> are available. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use NaturalDocs::Project::SourceFile; +use NaturalDocs::Project::ImageFile; + +use strict; +use integer; + +package NaturalDocs::Project; + + +############################################################################### +# Group: File Handles + +# +# handle: FH_FILEINFO +# +# The file handle for the file information file, <FileInfo.nd>. +# + +# +# handle: FH_CONFIGFILEINFO +# +# The file handle for the config file information file, <ConfigFileInfo.nd>. +# + +# +# handle: FH_IMAGEFILE +# +# The file handle for determining the dimensions of image files. +# + + + +############################################################################### +# Group: Source File Variables + + +# +# hash: supportedFiles +# +# A hash of all the supported files in the input directory. The keys are the <FileNames>, and the values are +# <NaturalDocs::Project::SourceFile> objects. +# +my %supportedFiles; + +# +# hash: filesToParse +# +# An existence hash of all the <FileNames> that need to be parsed. +# +my %filesToParse; + +# +# hash: filesToBuild +# +# An existence hash of all the <FileNames> that need to be built. +# +my %filesToBuild; + +# +# hash: filesToPurge +# +# An existence hash of the <FileNames> that had Natural Docs content last time, but now either don't exist or no longer have +# content. +# +my %filesToPurge; + +# +# hash: unbuiltFilesWithContent +# +# An existence hash of all the <FileNames> that have Natural Docs content but are not part of <filesToBuild>. +# +my %unbuiltFilesWithContent; + + +# bool: reparseEverything +# Whether all the source files need to be reparsed. +my $reparseEverything; + +# bool: rebuildEverything +# Whether all the source files need to be rebuilt. +my $rebuildEverything; + +# hash: mostUsedLanguage +# The name of the most used language. Doesn't include text files. +my $mostUsedLanguage; + + + +############################################################################### +# Group: Configuration File Variables + + +# +# hash: mainConfigFile +# +# A hash mapping all the main configuration file names without paths to their <FileStatus>. Prior to <LoadConfigFileInfo()>, +# it serves as an existence hashref of the file names. +# +my %mainConfigFiles = ( 'Topics.txt' => 1, 'Languages.txt' => 1 ); + +# +# hash: userConfigFiles +# +# A hash mapping all the user configuration file names without paths to their <FileStatus>. Prior to <LoadConfigFileInfo()>, +# it serves as an existence hashref of the file names. +# +my %userConfigFiles = ( 'Topics.txt' => 1, 'Languages.txt' => 1, 'Menu.txt' => 1 ); + + + + +############################################################################### +# Group: Image File Variables + + +# +# hash: imageFileExtensions +# +# An existence hash of all the file extensions for images. Extensions are in all lowercase. +# +my %imageFileExtensions = ( 'jpg' => 1, 'jpeg' => 1, 'gif' => 1, 'png' => 1, 'bmp' => 1 ); + + +# +# hash: imageFiles +# +# A hash of all the image files in the project. The keys are the <FileNames> and the values are +# <NaturalDocs::Project::ImageFiles>. +# +my %imageFiles; + + +# +# hash: imageFilesToUpdate +# +# An existence hash of all the image <FileNames> that need to be updated, either because they changed or they're new to the +# project. +# +my %imageFilesToUpdate; + + +# +# hash: imageFilesToPurge +# +# An existence hash of all the image <FileNames> that need to be purged, either because the files no longer exist or because +# they are no longer used. +# +my %imageFilesToPurge; + + +# +# hash: insensitiveImageFiles +# +# A hash that maps all lowercase image <FileNames> to their proper case as it would appear in <imageFiles>. Used for +# case insensitivity, obviously. +# +# You can't just use all lowercase in <imageFiles> because both Linux and HTTP are case sensitive, so the original case must +# be preserved. We also want to allow separate entries for files that differ based only on case, so it goes to <imageFiles> first +# where they can be distinguished and here only if there's no match. Ties are broken by whichever is lower with cmp, because +# it has to resolve consistently on all runs of the program. +# +my %insensitiveImageFiles; + + + +############################################################################### +# Group: Files + + +# +# File: FileInfo.nd +# +# An index of the state of the files as of the last parse. Used to determine if files were added, deleted, or changed. +# +# Format: +# +# The format is a text file. +# +# > [VersionInt: app version] +# +# The beginning of the file is the <VersionInt> it was generated with. +# +# > [most used language name] +# +# Next is the name of the most used language in the source tree. Does not include text files. +# +# Each following line is +# +# > [file name] tab [last modification time] tab [has ND content (0 or 1)] tab [default menu title] \n +# +# Revisions: +# +# 1.3: +# +# - The line following the <VersionInt>, which was previously the last modification time of <Menu.txt>, was changed to +# the name of the most used language. +# +# 1.16: +# +# - File names are now absolute. Prior to 1.16, they were relative to the input directory since only one was allowed. +# +# 1.14: +# +# - The file was renamed from NaturalDocs.files to FileInfo.nd and moved into the Data subdirectory. +# +# 0.95: +# +# - The file version was changed to match the program version. Prior to 0.95, the version line was 1. Test for "1" instead +# of "1.0" to distinguish. +# + + +# +# File: ConfigFileInfo.nd +# +# An index of the state of the config files as of the last parse. +# +# Format: +# +# > [BINARY_FORMAT] +# > [VersionInt: app version] +# +# First is the standard <BINARY_FORMAT> <VersionInt> header. +# +# > [UInt32: last modification time of menu] +# > [UInt32: last modification of main topics file] +# > [UInt32: last modification of user topics file] +# > [UInt32: last modification of main languages file] +# > [UInt32: last modification of user languages file] +# +# Next are the last modification times of various configuration files as UInt32s in the standard Unix format. +# +# +# Revisions: +# +# 1.3: +# +# - The file was added to Natural Docs. Previously the last modification of <Menu.txt> was stored in <FileInfo.nd>, and +# <Topics.txt> and <Languages.txt> didn't exist. +# + + +# +# File: ImageFileInfo.nd +# +# An index of the state of the image files as of the last parse. +# +# Format: +# +# > [Standard Binary Header] +# +# First is the standard binary file header as defined by <NaturalDocs::BinaryFile>. +# +# > [AString16: file name or undef] +# > [UInt32: last modification time] +# > [UInt8: was used] +# +# This section is repeated until the file name is null. The last modification times are UInt32s in the standard Unix format. +# +# +# Revisions: +# +# 1.4: +# +# - The file was added to Natural Docs. +# + + + +############################################################################### +# Group: File Functions + +# +# Function: LoadSourceFileInfo +# +# Loads the project file from disk and compares it against the files in the input directory. Project is loaded from +# <FileInfo.nd>. New and changed files will be added to <FilesToParse()>, and if they have content, +# <FilesToBuild()>. +# +# Will call <NaturalDocs::Languages->OnMostUsedLanguageKnown()> if <MostUsedLanguage()> changes. +# +# Returns: +# +# Returns whether the project was changed in any way. +# +sub LoadSourceFileInfo + { + my ($self) = @_; + + $self->GetAllSupportedFiles(); + NaturalDocs::Languages->OnMostUsedLanguageKnown(); + + my $fileIsOkay; + my $version; + my $hasChanged; + + if (open(FH_FILEINFO, '<' . $self->DataFile('FileInfo.nd'))) + { + # Check if the file is in the right format. + $version = NaturalDocs::Version->FromTextFile(\*FH_FILEINFO); + + # The project file need to be rebuilt for 1.16. The source files need to be reparsed and the output files rebuilt for 1.4. + # We'll tolerate the difference between 1.16 and 1.3 in the loader. + + if (NaturalDocs::Version->CheckFileFormat( $version, NaturalDocs::Version->FromString('1.16') )) + { + $fileIsOkay = 1; + + if (!NaturalDocs::Version->CheckFileFormat( $version, NaturalDocs::Version->FromString('1.4') )) + { + $reparseEverything = 1; + $rebuildEverything = 1; + $hasChanged = 1; + }; + } + else + { + close(FH_FILEINFO); + $hasChanged = 1; + }; + }; + + + if ($fileIsOkay) + { + my %indexedFiles; + + + my $line = <FH_FILEINFO>; + ::XChomp(\$line); + + # Prior to 1.3 it was the last modification time of Menu.txt, which we ignore and treat as though the most used language + # changed. Prior to 1.32 the settings didn't transfer over correctly to Menu.txt so we need to behave that way again. + if ($version < NaturalDocs::Version->FromString('1.32') || lc($mostUsedLanguage) ne lc($line)) + { + $reparseEverything = 1; + NaturalDocs::SymbolTable->RebuildAllIndexes(); + }; + + + # Parse the rest of the file. + + while ($line = <FH_FILEINFO>) + { + ::XChomp(\$line); + my ($file, $modification, $hasContent, $menuTitle) = split(/\t/, $line, 4); + + # If the file no longer exists... + if (!exists $supportedFiles{$file}) + { + if ($hasContent) + { $filesToPurge{$file} = 1; }; + + $hasChanged = 1; + } + + # If the file still exists... + else + { + $indexedFiles{$file} = 1; + + # If the file changed... + if ($supportedFiles{$file}->LastModified() != $modification) + { + $supportedFiles{$file}->SetStatus(::FILE_CHANGED()); + $filesToParse{$file} = 1; + + # If the file loses its content, this will be removed by SetHasContent(). + if ($hasContent) + { $filesToBuild{$file} = 1; }; + + $hasChanged = 1; + } + + # If the file has not changed... + else + { + my $status; + + if ($rebuildEverything && $hasContent) + { + $status = ::FILE_CHANGED(); + + # If the file loses its content, this will be removed by SetHasContent(). + $filesToBuild{$file} = 1; + $hasChanged = 1; + } + else + { + $status = ::FILE_SAME(); + + if ($hasContent) + { $unbuiltFilesWithContent{$file} = 1; }; + }; + + if ($reparseEverything) + { + $status = ::FILE_CHANGED(); + + $filesToParse{$file} = 1; + $hasChanged = 1; + }; + + $supportedFiles{$file}->SetStatus($status); + }; + + $supportedFiles{$file}->SetHasContent($hasContent); + $supportedFiles{$file}->SetDefaultMenuTitle($menuTitle); + }; + }; + + close(FH_FILEINFO); + + + # Check for added files. + + if (scalar keys %supportedFiles > scalar keys %indexedFiles) + { + foreach my $file (keys %supportedFiles) + { + if (!exists $indexedFiles{$file}) + { + $supportedFiles{$file}->SetStatus(::FILE_NEW()); + $supportedFiles{$file}->SetDefaultMenuTitle($file); + $supportedFiles{$file}->SetHasContent(undef); + $filesToParse{$file} = 1; + # It will be added to filesToBuild if HasContent gets set to true when it's parsed. + $hasChanged = 1; + }; + }; + }; + } + + # If something's wrong with FileInfo.nd, everything is new. + else + { + foreach my $file (keys %supportedFiles) + { + $supportedFiles{$file}->SetStatus(::FILE_NEW()); + $supportedFiles{$file}->SetDefaultMenuTitle($file); + $supportedFiles{$file}->SetHasContent(undef); + $filesToParse{$file} = 1; + # It will be added to filesToBuild if HasContent gets set to true when it's parsed. + }; + + $hasChanged = 1; + }; + + + # There are other side effects, so we need to call this. + if ($rebuildEverything) + { $self->RebuildEverything(); }; + + + return $hasChanged; + }; + + +# +# Function: SaveSourceFileInfo +# +# Saves the source file info to disk. Everything is saved in <FileInfo.nd>. +# +sub SaveSourceFileInfo + { + my ($self) = @_; + + open(FH_FILEINFO, '>' . $self->DataFile('FileInfo.nd')) + or die "Couldn't save project file " . $self->DataFile('FileInfo.nd') . "\n"; + + NaturalDocs::Version->ToTextFile(\*FH_FILEINFO, NaturalDocs::Settings->AppVersion()); + + print FH_FILEINFO $mostUsedLanguage . "\n"; + + while (my ($fileName, $file) = each %supportedFiles) + { + print FH_FILEINFO $fileName . "\t" + . $file->LastModified() . "\t" + . ($file->HasContent() || '0') . "\t" + . $file->DefaultMenuTitle() . "\n"; + }; + + close(FH_FILEINFO); + }; + + +# +# Function: LoadConfigFileInfo +# +# Loads the config file info from disk. +# +sub LoadConfigFileInfo + { + my ($self) = @_; + + my $fileIsOkay; + my $version; + my $fileName = NaturalDocs::Project->DataFile('ConfigFileInfo.nd'); + + if (open(FH_CONFIGFILEINFO, '<' . $fileName)) + { + # See if it's binary. + binmode(FH_CONFIGFILEINFO); + + my $firstChar; + read(FH_CONFIGFILEINFO, $firstChar, 1); + + if ($firstChar == ::BINARY_FORMAT()) + { + $version = NaturalDocs::Version->FromBinaryFile(\*FH_CONFIGFILEINFO); + + # It hasn't changed since being introduced. + + if (NaturalDocs::Version->CheckFileFormat($version)) + { $fileIsOkay = 1; } + else + { close(FH_CONFIGFILEINFO); }; + } + + else # it's not in binary + { close(FH_CONFIGFILEINFO); }; + }; + + my @configFiles = ( $self->UserConfigFile('Menu.txt'), \$userConfigFiles{'Menu.txt'}, + $self->MainConfigFile('Topics.txt'), \$mainConfigFiles{'Topics.txt'}, + $self->UserConfigFile('Topics.txt'), \$userConfigFiles{'Topics.txt'}, + $self->MainConfigFile('Languages.txt'), \$mainConfigFiles{'Languages.txt'}, + $self->UserConfigFile('Languages.txt'), \$userConfigFiles{'Languages.txt'} ); + + if ($fileIsOkay) + { + my $raw; + + read(FH_CONFIGFILEINFO, $raw, 20); + my @configFileDates = unpack('NNNNN', $raw); + + while (scalar @configFiles) + { + my $file = shift @configFiles; + my $fileStatus = shift @configFiles; + my $fileDate = shift @configFileDates; + + if (-e $file) + { + if ($fileDate == (stat($file))[9]) + { $$fileStatus = ::FILE_SAME(); } + else + { $$fileStatus = ::FILE_CHANGED(); }; + } + else + { $$fileStatus = ::FILE_DOESNTEXIST(); }; + }; + + close(FH_CONFIGFILEINFO); + } + else # !$fileIsOkay + { + while (scalar @configFiles) + { + my $file = shift @configFiles; + my $fileStatus = shift @configFiles; + + if (-e $file) + { $$fileStatus = ::FILE_CHANGED(); } + else + { $$fileStatus = ::FILE_DOESNTEXIST(); }; + }; + }; + + if ($userConfigFiles{'Menu.txt'} == ::FILE_SAME() && $rebuildEverything) + { $userConfigFiles{'Menu.txt'} = ::FILE_CHANGED(); }; + }; + + +# +# Function: SaveConfigFileInfo +# +# Saves the config file info to disk. You *must* save all other config files first, such as <Menu.txt> and <Topics.txt>. +# +sub SaveConfigFileInfo + { + my ($self) = @_; + + open (FH_CONFIGFILEINFO, '>' . NaturalDocs::Project->DataFile('ConfigFileInfo.nd')) + or die "Couldn't save " . NaturalDocs::Project->DataFile('ConfigFileInfo.nd') . ".\n"; + + binmode(FH_CONFIGFILEINFO); + + print FH_CONFIGFILEINFO '' . ::BINARY_FORMAT(); + + NaturalDocs::Version->ToBinaryFile(\*FH_CONFIGFILEINFO, NaturalDocs::Settings->AppVersion()); + + print FH_CONFIGFILEINFO pack('NNNNN', (stat($self->UserConfigFile('Menu.txt')))[9], + (stat($self->MainConfigFile('Topics.txt')))[9], + (stat($self->UserConfigFile('Topics.txt')))[9], + (stat($self->MainConfigFile('Languages.txt')))[9], + (stat($self->UserConfigFile('Languages.txt')))[9] ); + + close(FH_CONFIGFILEINFO); + }; + + +# +# Function: LoadImageFileInfo +# +# Loads the image file info from disk. +# +sub LoadImageFileInfo + { + my ($self) = @_; + + my $version = NaturalDocs::BinaryFile->OpenForReading( NaturalDocs::Project->DataFile('ImageFileInfo.nd') ); + my $fileIsOkay; + + if (defined $version) + { + # It hasn't changed since being introduced. + + if (NaturalDocs::Version->CheckFileFormat($version)) + { $fileIsOkay = 1; } + else + { NaturalDocs::BinaryFile->Close(); }; + }; + + if ($fileIsOkay) + { + # [AString16: file name or undef] + + while (my $imageFile = NaturalDocs::BinaryFile->GetAString16()) + { + # [UInt32: last modified] + # [UInt8: was used] + + my $lastModified = NaturalDocs::BinaryFile->GetUInt32(); + my $wasUsed = NaturalDocs::BinaryFile->GetUInt8(); + + my $imageFileObject = $imageFiles{$imageFile}; + + # If there's an image file in ImageFileInfo.nd that no longer exists... + if (!$imageFileObject) + { + $imageFileObject = NaturalDocs::Project::ImageFile->New($lastModified, ::FILE_DOESNTEXIST(), $wasUsed); + $imageFiles{$imageFile} = $imageFileObject; + + if ($wasUsed) + { $imageFilesToPurge{$imageFile} = 1; }; + } + else + { + $imageFileObject->SetWasUsed($wasUsed); + + # This will be removed if it gets any references. + if ($wasUsed) + { $imageFilesToPurge{$imageFile} = 1; }; + + if ($imageFileObject->LastModified() == $lastModified && !$rebuildEverything) + { $imageFileObject->SetStatus(::FILE_SAME()); } + else + { $imageFileObject->SetStatus(::FILE_CHANGED()); }; + }; + }; + + NaturalDocs::BinaryFile->Close(); + } + + else # !$fileIsOkay + { + $self->RebuildEverything(); + }; + }; + + +# +# Function: SaveImageFileInfo +# +# Saves the image file info to disk. +# +sub SaveImageFileInfo + { + my $self = shift; + + NaturalDocs::BinaryFile->OpenForWriting( NaturalDocs::Project->DataFile('ImageFileInfo.nd') ); + + while (my ($imageFile, $imageFileInfo) = each %imageFiles) + { + if ($imageFileInfo->Status() != ::FILE_DOESNTEXIST()) + { + # [AString16: file name or undef] + # [UInt32: last modification time] + # [UInt8: was used] + + NaturalDocs::BinaryFile->WriteAString16($imageFile); + NaturalDocs::BinaryFile->WriteUInt32($imageFileInfo->LastModified()); + NaturalDocs::BinaryFile->WriteUInt8( ($imageFileInfo->ReferenceCount() > 0 ? 1 : 0) ); + }; + }; + + NaturalDocs::BinaryFile->WriteAString16(undef); + NaturalDocs::BinaryFile->Close(); + }; + + +# +# Function: MigrateOldFiles +# +# If the project uses the old file names used prior to 1.14, it converts them to the new file names. +# +sub MigrateOldFiles + { + my ($self) = @_; + + my $projectDirectory = NaturalDocs::Settings->ProjectDirectory(); + + # We use the menu file as a test to see if we're using the new format. + if (-e NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs_Menu.txt')) + { + # The Data subdirectory would have been created by NaturalDocs::Settings. + + rename( NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs_Menu.txt'), $self->UserConfigFile('Menu.txt') ); + + if (-e NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.sym')) + { rename( NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.sym'), $self->DataFile('SymbolTable.nd') ); }; + + if (-e NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.files')) + { rename( NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.files'), $self->DataFile('FileInfo.nd') ); }; + + if (-e NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.m')) + { rename( NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.m'), $self->DataFile('PreviousMenuState.nd') ); }; + }; + }; + + + +############################################################################### +# Group: Config and Data File Functions + + +# +# Function: MainConfigFile +# +# Returns the full path to the passed main configuration file. Pass the file name only. +# +sub MainConfigFile #(string file) + { + my ($self, $file) = @_; + return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ConfigDirectory(), $file ); + }; + +# +# Function: MainConfigFileStatus +# +# Returns the <FileStatus> of the passed main configuration file. Pass the file name only. +# +sub MainConfigFileStatus #(string file) + { + my ($self, $file) = @_; + return $mainConfigFiles{$file}; + }; + +# +# Function: UserConfigFile +# +# Returns the full path to the passed user configuration file. Pass the file name only. +# +sub UserConfigFile #(string file) + { + my ($self, $file) = @_; + return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDirectory(), $file ); + }; + +# +# Function: UserConfigFileStatus +# +# Returns the <FileStatus> of the passed user configuration file. Pass the file name only. +# +sub UserConfigFileStatus #(string file) + { + my ($self, $file) = @_; + return $userConfigFiles{$file}; + }; + +# +# Function: DataFile +# +# Returns the full path to the passed data file. Pass the file name only. +# +sub DataFile #(string file) + { + my ($self, $file) = @_; + return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDataDirectory(), $file ); + }; + + + + +############################################################################### +# Group: Source File Functions + + +# Function: FilesToParse +# Returns an existence hashref of the <FileNames> to parse. This is not a copy of the data, so don't change it. +sub FilesToParse + { return \%filesToParse; }; + +# Function: FilesToBuild +# Returns an existence hashref of the <FileNames> to build. This is not a copy of the data, so don't change it. +sub FilesToBuild + { return \%filesToBuild; }; + +# Function: FilesToPurge +# Returns an existence hashref of the <FileNames> that had content last time, but now either don't anymore or were deleted. +# This is not a copy of the data, so don't change it. +sub FilesToPurge + { return \%filesToPurge; }; + +# +# Function: RebuildFile +# +# Adds the file to the list of files to build. This function will automatically filter out files that don't have Natural Docs content and +# files that are part of <FilesToPurge()>. If this gets called on a file and that file later gets Natural Docs content, it will be added. +# +# Parameters: +# +# file - The <FileName> to build or rebuild. +# +sub RebuildFile #(file) + { + my ($self, $file) = @_; + + # We don't want to add it to the build list if it doesn't exist, doesn't have Natural Docs content, or it's going to be purged. + # If it wasn't parsed yet and will later be found to have ND content, it will be added by SetHasContent(). + if (exists $supportedFiles{$file} && !exists $filesToPurge{$file} && $supportedFiles{$file}->HasContent()) + { + $filesToBuild{$file} = 1; + + if (exists $unbuiltFilesWithContent{$file}) + { delete $unbuiltFilesWithContent{$file}; }; + }; + }; + + +# +# Function: ReparseEverything +# +# Adds all supported files to the list of files to parse. This does not necessarily mean these files are going to be rebuilt. +# +sub ReparseEverything + { + my ($self) = @_; + + if (!$reparseEverything) + { + foreach my $file (keys %supportedFiles) + { + $filesToParse{$file} = 1; + }; + + $reparseEverything = 1; + }; + }; + + +# +# Function: RebuildEverything +# +# Adds all supported files to the list of files to build. This does not necessarily mean these files are going to be reparsed. +# +sub RebuildEverything + { + my ($self) = @_; + + foreach my $file (keys %unbuiltFilesWithContent) + { + $filesToBuild{$file} = 1; + }; + + %unbuiltFilesWithContent = ( ); + $rebuildEverything = 1; + + NaturalDocs::SymbolTable->RebuildAllIndexes(); + + if ($userConfigFiles{'Menu.txt'} == ::FILE_SAME()) + { $userConfigFiles{'Menu.txt'} = ::FILE_CHANGED(); }; + + while (my ($imageFile, $imageObject) = each %imageFiles) + { + if ($imageObject->ReferenceCount()) + { $imageFilesToUpdate{$imageFile} = 1; }; + }; + }; + + +# Function: UnbuiltFilesWithContent +# Returns an existence hashref of the <FileNames> that have Natural Docs content but are not part of <FilesToBuild()>. This is +# not a copy of the data so don't change it. +sub UnbuiltFilesWithContent + { return \%unbuiltFilesWithContent; }; + +# Function: FilesWithContent +# Returns and existence hashref of the <FileNames> that have Natural Docs content. +sub FilesWithContent + { + # Don't keep this one internally, but there's an easy way to make it. + return { %filesToBuild, %unbuiltFilesWithContent }; + }; + + +# +# Function: HasContent +# +# Returns whether the <FileName> contains Natural Docs content. +# +sub HasContent #(file) + { + my ($self, $file) = @_; + + if (exists $supportedFiles{$file}) + { return $supportedFiles{$file}->HasContent(); } + else + { return undef; }; + }; + + +# +# Function: SetHasContent +# +# Sets whether the <FileName> has Natural Docs content or not. +# +sub SetHasContent #(file, hasContent) + { + my ($self, $file, $hasContent) = @_; + + if (exists $supportedFiles{$file} && $supportedFiles{$file}->HasContent() != $hasContent) + { + # If the file now has content... + if ($hasContent) + { + $filesToBuild{$file} = 1; + } + + # If the file's content has been removed... + else + { + delete $filesToBuild{$file}; # may not be there + $filesToPurge{$file} = 1; + }; + + $supportedFiles{$file}->SetHasContent($hasContent); + }; + }; + + +# +# Function: StatusOf +# +# Returns the <FileStatus> of the passed <FileName>. +# +sub StatusOf #(file) + { + my ($self, $file) = @_; + + if (exists $supportedFiles{$file}) + { return $supportedFiles{$file}->Status(); } + else + { return ::FILE_DOESNTEXIST(); }; + }; + + +# +# Function: DefaultMenuTitleOf +# +# Returns the default menu title of the <FileName>. If one isn't specified, it returns the <FileName>. +# +sub DefaultMenuTitleOf #(file) + { + my ($self, $file) = @_; + + if (exists $supportedFiles{$file}) + { return $supportedFiles{$file}->DefaultMenuTitle(); } + else + { return $file; }; + }; + + +# +# Function: SetDefaultMenuTitle +# +# Sets the <FileName's> default menu title. +# +sub SetDefaultMenuTitle #(file, menuTitle) + { + my ($self, $file, $menuTitle) = @_; + + if (exists $supportedFiles{$file} && $supportedFiles{$file}->DefaultMenuTitle() ne $menuTitle) + { + $supportedFiles{$file}->SetDefaultMenuTitle($menuTitle); + NaturalDocs::Menu->OnDefaultTitleChange($file); + }; + }; + + +# +# Function: MostUsedLanguage +# +# Returns the name of the most used language in the source trees. Does not include text files. +# +sub MostUsedLanguage + { return $mostUsedLanguage; }; + + + + +############################################################################### +# Group: Image File Functions + + +# +# Function: ImageFileExists +# Returns whether the passed image file exists. +# +sub ImageFileExists #(FileName file) => bool + { + my ($self, $file) = @_; + + if (!exists $imageFiles{$file}) + { $file = $insensitiveImageFiles{lc($file)}; }; + + return (exists $imageFiles{$file} && $imageFiles{$file}->Status() != ::FILE_DOESNTEXIST()); + }; + + +# +# Function: ImageFileDimensions +# Returns the dimensions of the passed image file as the array ( width, height ). Returns them both as undef if it cannot be +# determined. +# +sub ImageFileDimensions #(FileName file) => (int, int) + { + my ($self, $file) = @_; + + if (!exists $imageFiles{$file}) + { $file = $insensitiveImageFiles{lc($file)}; }; + + my $object = $imageFiles{$file}; + if (!$object) + { die "Tried to get the dimensions of an image that doesn't exist."; }; + + if ($object->Width() == -1) + { $self->DetermineImageDimensions($file); }; + + return ($object->Width(), $object->Height()); + }; + + +# +# Function: ImageFileCapitalization +# Returns the properly capitalized version of the passed image <FileName>. Image file paths are treated as case insensitive +# regardless of whether the underlying operating system is or not, so we have to make sure the final version matches the +# capitalization of the actual file. +# +sub ImageFileCapitalization #(FileName file) => FileName + { + my ($self, $file) = @_; + + if (exists $imageFiles{$file}) + { return $file; } + elsif (exists $insensitiveImageFiles{lc($file)}) + { return $insensitiveImageFiles{lc($file)}; } + else + { die "Tried to get the capitalization of an image file that doesn't exist."; }; + }; + + +# +# Function: AddImageFileReference +# Adds a reference to the passed image <FileName>. +# +sub AddImageFileReference #(FileName imageFile) + { + my ($self, $imageFile) = @_; + + if (!exists $imageFiles{$imageFile}) + { $imageFile = $insensitiveImageFiles{lc($imageFile)}; }; + + my $imageFileInfo = $imageFiles{$imageFile}; + + if ($imageFileInfo == undef || $imageFileInfo->Status() == ::FILE_DOESNTEXIST()) + { die "Tried to add a reference to a non-existant image file."; }; + + if ($imageFileInfo->AddReference() == 1) + { + delete $imageFilesToPurge{$imageFile}; + + if (!$imageFileInfo->WasUsed() || + $imageFileInfo->Status() == ::FILE_NEW() || + $imageFileInfo->Status() == ::FILE_CHANGED()) + { $imageFilesToUpdate{$imageFile} = 1; }; + }; + }; + + +# +# Function: DeleteImageFileReference +# Deletes a reference from the passed image <FileName>. +# +sub DeleteImageFileReference #(FileName imageFile) + { + my ($self, $imageFile) = @_; + + if (!exists $imageFiles{$imageFile}) + { $imageFile = $insensitiveImageFiles{lc($imageFile)}; }; + + if (!exists $imageFiles{$imageFile}) + { die "Tried to delete a reference to a non-existant image file."; }; + + if ($imageFiles{$imageFile}->DeleteReference() == 0) + { + delete $imageFilesToUpdate{$imageFile}; + + if ($imageFiles{$imageFile}->WasUsed()) + { $imageFilesToPurge{$imageFile} = 1; }; + }; + }; + + +# +# Function: ImageFilesToUpdate +# Returns an existence hashref of image <FileNames> that need to be updated. *Do not change.* +# +sub ImageFilesToUpdate + { return \%imageFilesToUpdate; }; + + +# +# Function: ImageFilesToPurge +# Returns an existence hashref of image <FileNames> that need to be updated. *Do not change.* +# +sub ImageFilesToPurge + { return \%imageFilesToPurge; }; + + + +############################################################################### +# Group: Support Functions + +# +# Function: GetAllSupportedFiles +# +# Gets all the supported files in the passed directory and its subdirectories and puts them into <supportedFiles>. The only +# attribute that will be set is <NaturalDocs::Project::SourceFile->LastModified()>. Also sets <mostUsedLanguage>. +# +sub GetAllSupportedFiles + { + my ($self) = @_; + + my @directories = @{NaturalDocs::Settings->InputDirectories()}; + my $isCaseSensitive = NaturalDocs::File->IsCaseSensitive(); + + # Keys are language names, values are counts. + my %languageCounts; + + + # Make an existence hash of excluded directories. + + my %excludedDirectories; + my $excludedDirectoryArrayRef = NaturalDocs::Settings->ExcludedInputDirectories(); + + foreach my $excludedDirectory (@$excludedDirectoryArrayRef) + { + if ($isCaseSensitive) + { $excludedDirectories{$excludedDirectory} = 1; } + else + { $excludedDirectories{lc($excludedDirectory)} = 1; }; + }; + + + my $imagesOnly; + my $language; + + while (scalar @directories) + { + my $directory = pop @directories; + + opendir DIRECTORYHANDLE, $directory; + my @entries = readdir DIRECTORYHANDLE; + closedir DIRECTORYHANDLE; + + @entries = NaturalDocs::File->NoUpwards(@entries); + + foreach my $entry (@entries) + { + my $fullEntry = NaturalDocs::File->JoinPaths($directory, $entry); + + # If an entry is a directory, recurse. + if (-d $fullEntry) + { + # Join again with the noFile flag set in case the platform handles them differently. + $fullEntry = NaturalDocs::File->JoinPaths($directory, $entry, 1); + + if ($isCaseSensitive) + { + if (!exists $excludedDirectories{$fullEntry}) + { push @directories, $fullEntry; }; + } + else + { + if (!exists $excludedDirectories{lc($fullEntry)}) + { push @directories, $fullEntry; }; + }; + } + + # Otherwise add it if it's a supported extension. + else + { + my $extension = NaturalDocs::File->ExtensionOf($entry); + + if (exists $imageFileExtensions{lc($extension)}) + { + my $fileObject = NaturalDocs::Project::ImageFile->New( (stat($fullEntry))[9], ::FILE_NEW(), 0 ); + $imageFiles{$fullEntry} = $fileObject; + + my $lcFullEntry = lc($fullEntry); + + if (!exists $insensitiveImageFiles{$lcFullEntry} || + ($fullEntry cmp $insensitiveImageFiles{$lcFullEntry}) < 0) + { + $insensitiveImageFiles{$lcFullEntry} = $fullEntry; + }; + } + elsif (!$imagesOnly && ($language = NaturalDocs::Languages->LanguageOf($fullEntry)) ) + { + my $fileObject = NaturalDocs::Project::SourceFile->New(); + $fileObject->SetLastModified(( stat($fullEntry))[9] ); + $supportedFiles{$fullEntry} = $fileObject; + + $languageCounts{$language->Name()}++; + }; + }; + }; + + + # After we run out of source directories, add the image directories. + + if (scalar @directories == 0 && !$imagesOnly) + { + $imagesOnly = 1; + @directories = @{NaturalDocs::Settings->ImageDirectories()}; + }; + }; + + + my $topCount = 0; + + while (my ($language, $count) = each %languageCounts) + { + if ($count > $topCount && $language ne 'Text File') + { + $topCount = $count; + $mostUsedLanguage = $language; + }; + }; + }; + + +# +# Function: DetermineImageDimensions +# +# Attempts to determine the dimensions of the passed image and apply them to their object in <imageFiles>. Will set them to +# undef if they can't be determined. +# +sub DetermineImageDimensions #(FileName imageFile) + { + my ($self, $imageFile) = @_; + + my $imageFileObject = $imageFiles{$imageFile}; + if (!defined $imageFileObject) + { die "Tried to determine image dimensions of a file with no object."; }; + + my $extension = lc( NaturalDocs::File->ExtensionOf($imageFile) ); + my ($width, $height); + + if ($imageFileExtensions{$extension}) + { + open(FH_IMAGEFILE, '<' . $imageFile) + or die 'Could not open ' . $imageFile . "\n"; + binmode(FH_IMAGEFILE); + + my $raw; + + if ($extension eq 'gif') + { + read(FH_IMAGEFILE, $raw, 6); + + if ($raw eq 'GIF87a' || $raw eq 'GIF89a') + { + read(FH_IMAGEFILE, $raw, 4); + ($width, $height) = unpack('vv', $raw); + }; + } + + elsif ($extension eq 'png') + { + read(FH_IMAGEFILE, $raw, 8); + + if ($raw eq "\x89PNG\x0D\x0A\x1A\x0A") + { + seek(FH_IMAGEFILE, 4, 1); + read(FH_IMAGEFILE, $raw, 4); + + if ($raw eq 'IHDR') + { + read(FH_IMAGEFILE, $raw, 8); + ($width, $height) = unpack('NN', $raw); + }; + }; + } + + elsif ($extension eq 'bmp') + { + read(FH_IMAGEFILE, $raw, 2); + + if ($raw eq 'BM') + { + seek(FH_IMAGEFILE, 16, 1); + read(FH_IMAGEFILE, $raw, 8); + + ($width, $height) = unpack('VV', $raw); + }; + } + + elsif ($extension eq 'jpg' || $extension eq 'jpeg') + { + read(FH_IMAGEFILE, $raw, 2); + my $isOkay = ($raw eq "\xFF\xD8"); + + while ($isOkay) + { + read(FH_IMAGEFILE, $raw, 4); + my ($marker, $code, $length) = unpack('CCn', $raw); + + $isOkay = ($marker eq 0xFF); + + if ($isOkay) + { + if ($code >= 0xC0 && $code <= 0xC3) + { + read(FH_IMAGEFILE, $raw, 5); + ($height, $width) = unpack('xnn', $raw); + last; + } + + else + { + $isOkay = seek(FH_IMAGEFILE, $length - 2, 1); + }; + }; + }; + }; + + close(FH_IMAGEFILE); + }; + + + # Sanity check the values. Although images can theoretically be bigger than 5000, most won't. The worst that happens in this + # case is just that they don't get length and width values in the output anyway. + if ($width > 0 && $width < 5000 && $height > 0 && $height < 5000) + { $imageFileObject->SetDimensions($width, $height); } + else + { $imageFileObject->SetDimensions(undef, undef); }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Project/ImageFile.pm b/docs/tool/Modules/NaturalDocs/Project/ImageFile.pm new file mode 100644 index 00000000..bcf9b556 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Project/ImageFile.pm @@ -0,0 +1,160 @@ +############################################################################### +# +# Class: NaturalDocs::Project::ImageFile +# +############################################################################### +# +# A simple information class about project image files. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Project::ImageFile; + + + +############################################################################### +# Group: Implementation + +# +# Constants: Members +# +# The class is implemented as a blessed arrayref. The following constants are used as indexes. +# +# LAST_MODIFIED - The integer timestamp of when the file was last modified. +# STATUS - <FileStatus> since the last build. +# REFERENCE_COUNT - The number of references to the image from the source files. +# WAS_USED - Whether the image was used the last time Natural Docs was run. +# WIDTH - The image width. Undef if can't be determined, -1 if haven't attempted to determine yet. +# HEIGHT - The image height. Undef if can't be determined, -1 if haven't attempted to determine yet. +# + +use NaturalDocs::DefineMembers 'LAST_MODIFIED', 'LastModified()', 'SetLastModified()', + 'STATUS', 'Status()', 'SetStatus()', + 'REFERENCE_COUNT', 'ReferenceCount()', + 'WAS_USED', 'WasUsed()', 'SetWasUsed()', + 'WIDTH', 'Width()', + 'HEIGHT', 'Height()'; + + +# +# Topic: WasUsed versus References +# +# <WasUsed()> is a simple true/false that notes whether this image file was used the last time Natural Docs was run. +# <ReferenceCount()> is a counter for the number of times it's used *this* run. As such, it starts at zero regardless of whether +# <WasUsed()> is set or not. +# + + +############################################################################### +# Group: Functions + +# +# Function: New +# +# Creates and returns a new file object. +# +# Parameters: +# +# lastModified - The image file's last modification timestamp +# status - The <FileStatus>. +# wasUsed - Whether this image file was used the *last* time Natural Docs was run. +# +sub New #(timestamp lastModified, FileStatus status, bool wasUsed) + { + my ($package, $lastModified, $status, $width, $height, $wasUsed) = @_; + + my $object = [ ]; + $object->[LAST_MODIFIED] = $lastModified; + $object->[STATUS] = $status; + $object->[REFERENCE_COUNT] = 0; + $object->[WAS_USED] = $wasUsed; + $object->[WIDTH] = -1; + $object->[HEIGHT] = -1; + + bless $object, $package; + + return $object; + }; + + +# +# Functions: Member Functions +# +# LastModified - Returns the integer timestamp of when the file was last modified. +# SetLastModified - Sets the file's last modification timestamp. +# Status - Returns the <FileStatus> since the last build. +# SetStatus - Sets the <FileStatus> since the last build. +# + +# +# Function: ReferenceCount +# Returns the current number of references to this image file during *this* Natural Docs execution. +# + +# +# Function: AddReference +# Increases the number of references to this image file by one. Returns the new reference count. +# +sub AddReference + { + my $self = shift; + + $self->[REFERENCE_COUNT]++; + return $self->[REFERENCE_COUNT]; + }; + +# +# Function: DeleteReference +# Decreases the number of references to this image file by one. Returns the new reference count. +# +sub DeleteReference + { + my $self = shift; + $self->[REFERENCE_COUNT]--; + + if ($self->[REFERENCE_COUNT] < 0) + { die "Deleted more references to an image file than existed."; }; + + return $self->[REFERENCE_COUNT]; + }; + + +# +# Functions: Member Functions +# +# WasUsed - Returns whether this image file was used during the *last* Natural Docs execution. +# SetWasUsed - Sets whether this image file was used during the *last* Natural Docs execution. +# Width - Returns the width in pixels, undef if it can't be determined, and -1 if determination hasn't been attempted yet. +# Height - Returns the width in pixels, undef if it can't be determined, and -1 if determination hasn't been attempted yet. +# + + +# +# Function: SetDimensions +# Sets the width and height of the image. Set to undef if they can't be determined. +# +sub SetDimensions #(int width, int height) + { + my ($self, $width, $height) = @_; + + # If either are undef, both should be undef. This will also convert zeroes to undef. + if (!$width || !$height) + { + $self->[WIDTH] = undef; + $self->[HEIGHT] = undef; + } + else + { + $self->[WIDTH] = $width; + $self->[HEIGHT] = $height; + }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Project/SourceFile.pm b/docs/tool/Modules/NaturalDocs/Project/SourceFile.pm new file mode 100644 index 00000000..fd649d9c --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Project/SourceFile.pm @@ -0,0 +1,113 @@ +############################################################################### +# +# Class: NaturalDocs::Project::SourceFile +# +############################################################################### +# +# A simple information class about project files. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Project::SourceFile; + + + +############################################################################### +# Group: Implementation + +# +# Constants: Members +# +# The class is implemented as a blessed arrayref. The following constants are used as indexes. +# +# HAS_CONTENT - Whether the file contains Natural Docs content or not. +# LAST_MODIFIED - The integer timestamp of when the file was last modified. +# STATUS - <FileStatus> since the last build. +# DEFAULT_MENU_TITLE - The file's default title in the menu. +# + +# DEPENDENCY: New() depends on its parameter list being in the same order as these constants. If the order changes, New() +# needs to be changed. +use NaturalDocs::DefineMembers 'HAS_CONTENT', 'LAST_MODIFIED', 'STATUS', 'DEFAULT_MENU_TITLE'; + + +############################################################################### +# Group: Functions + +# +# Function: New +# +# Creates and returns a new file object. +# +# Parameters: +# +# hasContent - Whether the file contains Natural Docs content or not. +# lastModified - The integer timestamp of when the file was last modified. +# status - The <FileStatus> since the last build. +# defaultMenuTitle - The file's title in the menu. +# +# Returns: +# +# A reference to the new object. +# +sub New #(hasContent, lastModified, status, defaultMenuTitle) + { + # DEPENDENCY: This function depends on its parameter list being in the same order as the member constants. If either order + # changes, this function needs to be changed. + + my $package = shift; + + my $object = [ @_ ]; + bless $object, $package; + + return $object; + }; + +# Function: HasContent +# Returns whether the file contains Natural Docs content or not. +sub HasContent + { return $_[0]->[HAS_CONTENT]; }; + +# Function: SetHasContent +# Sets whether the file contains Natural Docs content or not. +sub SetHasContent #(hasContent) + { $_[0]->[HAS_CONTENT] = $_[1]; }; + +# Function: LastModified +# Returns the integer timestamp of when the file was last modified. +sub LastModified + { return $_[0]->[LAST_MODIFIED]; }; + +# Function: SetLastModified +# Sets the file's last modification timestamp. +sub SetLastModified #(lastModified) + { $_[0]->[LAST_MODIFIED] = $_[1]; }; + +# Function: Status +# Returns the <FileStatus> since the last build. +sub Status + { return $_[0]->[STATUS]; }; + +# Function: SetStatus +# Sets the <FileStatus> since the last build. +sub SetStatus #(status) + { $_[0]->[STATUS] = $_[1]; }; + +# Function: DefaultMenuTitle +# Returns the file's default title on the menu. +sub DefaultMenuTitle + { return $_[0]->[DEFAULT_MENU_TITLE]; }; + +# Function: SetDefaultMenuTitle +# Sets the file's default title on the menu. +sub SetDefaultMenuTitle #(menuTitle) + { $_[0]->[DEFAULT_MENU_TITLE] = $_[1]; }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/ReferenceString.pm b/docs/tool/Modules/NaturalDocs/ReferenceString.pm new file mode 100644 index 00000000..a5aeed2f --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/ReferenceString.pm @@ -0,0 +1,334 @@ +############################################################################### +# +# Package: NaturalDocs::ReferenceString +# +############################################################################### +# +# A package to manage <ReferenceString> handling throughout the program. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::ReferenceString; + +use vars '@ISA', '@EXPORT'; +@ISA = 'Exporter'; +@EXPORT = ( 'BINARYREF_NOTYPE', 'BINARYREF_NORESOLVINGFLAGS', + + 'REFERENCE_TEXT', 'REFERENCE_CH_CLASS', 'REFERENCE_CH_PARENT', + + 'RESOLVE_RELATIVE', 'RESOLVE_ABSOLUTE', 'RESOLVE_NOPLURAL', 'RESOLVE_NOUSING' ); + + +# +# Constants: Binary Format Flags +# +# These flags can be combined to specify the format when using <ToBinaryFile()> and <FromBinaryFile()>. All are exported +# by default. +# +# BINARYREF_NOTYPE - Do not include the <ReferenceType>. +# BINARYREF_NORESOLVEFLAGS - Do not include the <Resolving Flags>. +# +use constant BINARYREF_NOTYPE => 0x01; +use constant BINARYREF_NORESOLVINGFLAGS => 0x02; + + +# +# Constants: ReferenceType +# +# The type of a reference. +# +# REFERENCE_TEXT - The reference appears in the text of the documentation. +# REFERENCE_CH_CLASS - A class reference handled by <NaturalDocs::ClassHierarchy>. +# REFERENCE_CH_PARENT - A parent class reference handled by <NaturalDocs::ClassHierarchy>. +# +# Dependencies: +# +# - <ToBinaryFile()> and <FromBinaryFile()> require that these values fit into a UInt8, i.e. are <= 255. +# +use constant REFERENCE_TEXT => 1; +use constant REFERENCE_CH_CLASS => 2; +use constant REFERENCE_CH_PARENT => 3; + + +# +# Constants: Resolving Flags +# +# Used to influence the method of resolving references in <NaturalDocs::SymbolTable>. +# +# RESOLVE_RELATIVE - The reference text is truly relative, rather than Natural Docs' semi-relative. +# RESOLVE_ABSOLUTE - The reference text is always absolute. No local or relative references. +# RESOLVE_NOPLURAL - The reference text may not be interpreted as a plural, and thus match singular forms as well. +# RESOLVE_NOUSING - The reference text may not include "using" statements when being resolved. +# +# If neither <RESOLVE_RELATIVE> or <RESOLVE_ABSOLUTE> is specified, Natural Docs' semi-relative kicks in instead, +# which is where links are interpreted as local, then global, then relative. <RESOLVE_RELATIVE> states that links are +# local, then relative, then global. +# +# Dependencies: +# +# - <ToBinaryFile()> and <FromBinaryFile()> require that these values fit into a UInt8, i.e. are <= 255. +# +use constant RESOLVE_RELATIVE => 0x01; +use constant RESOLVE_ABSOLUTE => 0x02; +use constant RESOLVE_NOPLURAL => 0x04; +use constant RESOLVE_NOUSING => 0x08; + + +# +# +# Function: MakeFrom +# +# Encodes the passed information as a <ReferenceString>. The format of the string should be treated as opaque. However, the +# characteristic you can rely on is that the same string will always be made from the same parameters, and thus it's suitable +# for comparison and use as hash keys. +# +# Parameters: +# +# type - The <ReferenceType>. +# symbol - The <SymbolString> of the reference. +# language - The name of the language that defines the file this reference appears in. +# scope - The scope <SymbolString> the reference appears in, or undef if none. +# using - An arrayref of scope <SymbolStrings> that are also available for checking due to the equivalent a "using" statement, +# or undef if none. +# resolvingFlags - The <Resolving Flags> to use with this reference. They are ignored if the type is <REFERENCE_TEXT>. +# +# Returns: +# +# The encoded <ReferenceString>. +# +sub MakeFrom #(ReferenceType type, SymbolString symbol, string language, SymbolString scope, SymbolString[]* using, flags resolvingFlags) + { + my ($self, $type, $symbol, $language, $scope, $using, $resolvingFlags) = @_; + + if ($type == ::REFERENCE_TEXT() || $resolvingFlags == 0) + { $resolvingFlags = undef; }; + + # The format is [type] 0x1E [resolving flags] 0x1E [symbol] 0x1E [scope] ( 0x1E [using] )* + # If there is no scope and/or using, the separator characters still remain. + + # DEPENDENCY: SymbolString->FromText() removed all 0x1E characters. + # DEPENDENCY: SymbolString->FromText() doesn't use 0x1E characters in its encoding. + + my $string = $type . "\x1E" . $symbol . "\x1E" . $language . "\x1E" . $resolvingFlags . "\x1E"; + + if (defined $scope) + { + $string .= $scope; + }; + + $string .= "\x1E"; + + if (defined $using) + { + $string .= join("\x1E", @$using); + }; + + return $string; + }; + + +# +# Function: ToBinaryFile +# +# Writes a <ReferenceString> to the passed filehandle. Can also encode an undef. +# +# Parameters: +# +# fileHandle - The filehandle to write to. +# referenceString - The <ReferenceString> to write, or undef. +# binaryFormatFlags - Any <Binary Format Flags> you want to use to influence encoding. +# +# Format: +# +# > [SymbolString: Symbol or undef for an undef reference] +# > [AString16: language] +# > [SymbolString: Scope or undef for none] +# > +# > [SymbolString: Using or undef for none] +# > [SymbolString: Using or undef for no more] +# > ... +# > +# > [UInt8: Type unless BINARYREF_NOTYPE is set] +# > [UInt8: Resolving Flags unless BINARYREF_NORESOLVINGFLAGS is set] +# +# Dependencies: +# +# - <ReferenceTypes> must fit into a UInt8. All values must be <= 255. +# - All <Resolving Flags> must fit into a UInt8. All values must be <= 255. +# +sub ToBinaryFile #(FileHandle fileHandle, ReferenceString referenceString, flags binaryFormatFlags) + { + my ($self, $fileHandle, $referenceString, $binaryFormatFlags) = @_; + + my ($type, $symbol, $language, $scope, $using, $resolvingFlags) = $self->InformationOf($referenceString); + + # [SymbolString: Symbol or undef for an undef reference] + + NaturalDocs::SymbolString->ToBinaryFile($fileHandle, $symbol); + + # [AString16: language] + + print $fileHandle pack('nA*', length $language, $language); + + # [SymbolString: scope or undef if none] + + NaturalDocs::SymbolString->ToBinaryFile($fileHandle, $scope); + + # [SymbolString: using or undef if none/no more] ... + + if (defined $using) + { + foreach my $usingScope (@$using) + { NaturalDocs::SymbolString->ToBinaryFile($fileHandle, $usingScope); }; + }; + + NaturalDocs::SymbolString->ToBinaryFile($fileHandle, undef); + + # [UInt8: Type unless BINARYREF_NOTYPE is set] + + if (!($binaryFormatFlags & BINARYREF_NOTYPE)) + { print $fileHandle pack('C', $type); }; + + # [UInt8: Resolving Flags unless BINARYREF_NORESOLVINGFLAGS is set] + + if (!($binaryFormatFlags & BINARYREF_NORESOLVINGFLAGS)) + { print $fileHandle pack('C', $type); }; + }; + + +# +# Function: FromBinaryFile +# +# Reads a <ReferenceString> or undef from the passed filehandle. +# +# Parameters: +# +# fileHandle - The filehandle to read from. +# binaryFormatFlags - Any <Binary Format Flags> you want to use to influence decoding. +# type - The <ReferenceType> to use if <BINARYREF_NOTYPE> is set. +# resolvingFlags - The <Resolving Flags> to use if <BINARYREF_NORESOLVINGFLAGS> is set. +# +# Returns: +# +# The <ReferenceString> or undef. +# +# See Also: +# +# See <ToBinaryFile()> for format and dependencies. +# +sub FromBinaryFile #(FileHandle fileHandle, flags binaryFormatFlags, ReferenceType type, flags resolvingFlags) + { + my ($self, $fileHandle, $binaryFormatFlags, $type, $resolvingFlags) = @_; + my $raw; + + # [SymbolString: Symbol or undef for an undef reference] + + my $symbol = NaturalDocs::SymbolString->FromBinaryFile($fileHandle); + + if (!defined $symbol) + { return undef; }; + + + # [AString16: language] + + read($fileHandle, $raw, 2); + my $languageLength = unpack('n', $raw); + + my $language; + read($fileHandle, $language, $languageLength); + + + # [SymbolString: scope or undef if none] + + my $scope = NaturalDocs::SymbolString->FromBinaryFile($fileHandle); + + # [SymbolString: using or undef if none/no more] ... + + my $usingSymbol; + my @using; + + while ($usingSymbol = NaturalDocs::SymbolString->FromBinaryFile($fileHandle)) + { push @using, $usingSymbol; }; + + if (scalar @using) + { $usingSymbol = \@using; } + else + { $usingSymbol = undef; }; + + # [UInt8: Type unless BINARYREF_NOTYPE is set] + + if (!($binaryFormatFlags & BINARYREF_NOTYPE)) + { + my $raw; + read($fileHandle, $raw, 1); + $type = unpack('C', $raw); + }; + + # [UInt8: Resolving Flags unless BINARYREF_NORESOLVINGFLAGS is set] + + if (!($binaryFormatFlags & BINARYREF_NORESOLVINGFLAGS)) + { + my $raw; + read($fileHandle, $raw, 1); + $resolvingFlags = unpack('C', $raw); + }; + + return $self->MakeFrom($type, $symbol, $language, $scope, $usingSymbol, $resolvingFlags); + }; + + +# +# Function: InformationOf +# +# Returns the information encoded in a <ReferenceString>. +# +# Parameters: +# +# referenceString - The <ReferenceString> to decode. +# +# Returns: +# +# The array ( type, symbol, language, scope, using, resolvingFlags ). +# +# type - The <ReferenceType>. +# symbol - The <SymbolString>. +# language - The name of the language that defined the file the reference was defined in. +# scope - The scope <SymbolString>, or undef if none. +# using - An arrayref of scope <SymbolStrings> that the reference also has access to via "using" statements, or undef if none. +# resolvingFlags - The <Resolving Flags> of the reference. +# +sub InformationOf #(ReferenceString referenceString) + { + my ($self, $referenceString) = @_; + + my ($type, $symbolString, $language, $resolvingFlags, $scopeString, @usingStrings) = split(/\x1E/, $referenceString); + + if (!length $resolvingFlags) + { $resolvingFlags = undef; }; + + return ( $type, $symbolString, $language, $scopeString, [ @usingStrings ], $resolvingFlags ); + }; + + +# +# Function: TypeOf +# +# Returns the <ReferenceType> encoded in the reference string. This is faster than <InformationOf()> if this is +# the only information you need. +# +sub TypeOf #(ReferenceString referenceString) + { + my ($self, $referenceString) = @_; + + $referenceString =~ /^([^\x1E]+)/; + return $1; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Settings.pm b/docs/tool/Modules/NaturalDocs/Settings.pm new file mode 100644 index 00000000..8d1fc11b --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Settings.pm @@ -0,0 +1,1418 @@ +############################################################################### +# +# Package: NaturalDocs::Settings +# +############################################################################### +# +# A package to handle the command line and various other program settings. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use Cwd (); + +use NaturalDocs::Settings::BuildTarget; + +use strict; +use integer; + +package NaturalDocs::Settings; + + +############################################################################### +# Group: Information + +=pod begin nd + + Topic: Usage and Dependencies + + - The <Constant Functions> can be called immediately. + + - Prior to initialization, <NaturalDocs::Builder> must have all its output packages registered. + + - To initialize, call <Load()>. All functions except <InputDirectoryNameOf()> will then be available. + + - <GenerateDirectoryNames()> must be called before <InputDirectoryNameOf()> will work. Currently it is called by + <NaturalDocs::Menu->LoadAndUpdate()>. + + + Architecture: Internal Overview + + - <Load()> first parses the command line, gathering all the settings and checking for errors. All <NaturalDocs::Builder> + packages must be registered before this is called because it needs their command line options. + <NaturalDocs::Project->ReparseEverything()> and <NaturalDocs::Project->RebuildEverything()> are called right away if -r + or -ro are used. + + - Output directories are *not* named at this point. See <Named Directories>. + + - The previous settings from the last time Natural Docs was run are loaded and compared to the current settings. + <NaturalDocs::Project->ReparseEverything()> and <NaturalDocs::Project->RebuildEverything()> are called if there are + any differences that warrant it. + + - It then waits for <GenerateDirectoryNames()> to be called by <NaturalDocs::Menu>. The reason for this is that the + previous directory names are stored as hints in the menu file, for reasons explained in <Named Directories>. Once that + happens all the unnamed directories have names generated for them so everything is named. The package is completely + set up. + + - The input directories are stored in an array instead of a hash because the order they were declared in matters. If two + people use multiple input directories on separate computers without sharing a menu file, they should at least get consistent + directory names by declaring them in the same order. + + + Architecture: Named Directories + + Ever since Natural Docs introduced multiple input directories in 1.16, they've had to be named. Since they don't necessarily + extend from the same root anymore, they can't share an output directory without the risk of file name conflicts. There was + an early attempt at giving them actual names, but now they're just numbered from 1. + + Directory names aren't generated right away. It waits for <Menu.txt> to load because that holds the obfuscated names from + the last run. <NaturalDocs::Menu> then calls <GenerateDirectoryNames()> and passes those along as hints. + <GenerateDirectoryNames()> then applies them to any matches and generates new ones for any remaining. This is done so + that output page locations can remain consistent when built on multiple computers, so long as the menu file is shared. I tend + to think the menu file is the most likely configuration file to be shared. + + + Architecture: Removed Directories + + Directories that were part of the previous run but aren't anymore are still stored in the package. The primary reason, though + there may be others, is file purging. If an input directory is removed, all the output files that were generated from anything + in it need to be removed. To find out what the output file name was for a removed source file, it needs to be able to split it + from it's original input directory and know what that directory was named. If this didn't happen those output files would be + orphaned, as was the case prior to 1.32. + +=cut + + + +############################################################################### +# Group: Variables + + +# handle: PREVIOUS_SETTINGS_FILEHANDLE +# The file handle used with <PreviousSettings.nd>. + +# array: inputDirectories +# An array of input directories. +my @inputDirectories; + +# array: inputDirectoryNames +# An array of the input directory names. Each name corresponds to the directory of the same index in <inputDirectories>. +my @inputDirectoryNames; + +# array: imageDirectories +# An array of image directories. +my @imageDirectories; + +# array: imageDirectoryNames +# An array of the image directory names. Each name corresponds to the directory of the same index in <imageDirectories>. +my @imageDirectoryNames; + +# array: relativeImageDirectories +# An array of the relative paths for images. The asterisks found in the command line are not present. +my @relativeImageDirectories; + +# array: excludedInputDirectories +# An array of input directories to exclude. +my @excludedInputDirectories; + +# array: removedInputDirectories +# An array of input directories that were once in the command line but are no longer. +my @removedInputDirectories; + +# array: removedInputDirectoryNames +# An array of the removed input directories' names. Each name corresponds to the directory of the same index in +# <removedInputDirectories>. +my @removedInputDirectoryNames; + +# array: removedImageDirectories +# An array of image directories that were once in the command line but are no longer. +my @removedImageDirectories; + +# array: removedImageDirectoryNames +# An array of the removed image directories' names. Each name corresponds to the directory of the same index in +# <removedImageDirectories>. +my @removedImageDirectoryNames; + +# var: projectDirectory +# The project directory. +my $projectDirectory; + +# array: buildTargets +# An array of <NaturalDocs::Settings::BuildTarget>s. +my @buildTargets; + +# var: documentedOnly +# Whether undocumented code aspects should be included in the output. +my $documentedOnly; + +# int: tabLength +# The number of spaces in tabs. +my $tabLength; + +# bool: noAutoGroup +# Whether auto-grouping is turned off. +my $noAutoGroup; + +# bool: onlyFileTitles +# Whether source files should always use the file name as the title. +my $onlyFileTitles; + +# bool: isQuiet +# Whether the script should be run in quiet mode or not. +my $isQuiet; + +# bool: rebuildData +# WHether most data files should be ignored and rebuilt. +my $rebuildData; + +# array: styles +# An array of style names to use, most important first. +my @styles; + +# var: charset +# The character encoding of the source files, and thus the output. +my $charset; + + +############################################################################### +# Group: Files + + +# +# File: PreviousSettings.nd +# +# Stores the previous command line settings. +# +# Format: +# +# > [BINARY_FORMAT] +# > [VersionInt: app version] +# +# The file starts with the standard <BINARY_FORMAT> <VersionInt> header. +# +# > [UInt8: tab length] +# > [UInt8: documented only (0 or 1)] +# > [UInt8: no auto-group (0 or 1)] +# > [UInt8: only file titles (0 or 1)] +# > [AString16: charset] +# > +# > [UInt8: number of input directories] +# > [AString16: input directory] [AString16: input directory name] ... +# +# A count of input directories, then that number of directory/name pairs. +# +# > [UInt8: number of output targets] +# > [AString16: output directory] [AString16: output format command line option] ... +# +# A count of output targets, then that number of directory/format pairs. +# +# +# Revisions: +# +# 1.4: +# +# - Added only file titles. +# +# 1.33: +# +# - Added charset. +# +# 1.3: +# +# - Removed headers-only, which was a 0/1 UInt8 after tab length. +# - Change auto-group level (1 = no, 2 = yes, 3 = full only) to no auto-group (0 or 1). +# +# 1.22: +# +# - Added auto-group level. +# +# 1.2: +# +# - File was added to the project. Prior to 1.2, it didn't exist. +# + + +############################################################################### +# Group: Action Functions + +# +# Function: Load +# +# Loads and parses all settings from the command line and configuration files. Will exit if the options are invalid or the syntax +# reference was requested. +# +sub Load + { + my ($self) = @_; + + $self->ParseCommandLine(); + $self->LoadAndComparePreviousSettings(); + }; + + +# +# Function: Save +# +# Saves all settings in configuration files to disk. +# +sub Save + { + my ($self) = @_; + + $self->SavePreviousSettings(); + }; + + +# +# Function: GenerateDirectoryNames +# +# Generates names for each of the input and image directories, which can later be retrieved with <InputDirectoryNameOf()> +# and <ImageDirectoryNameOf()>. +# +# Parameters: +# +# inputHints - A hashref of suggested input directory names, where the keys are the directories and the values are the names. +# These take precedence over anything generated. You should include names for directories that are no longer in +# the command line. This parameter may be undef. +# imageHints - Same as inputHints, only for the image directories. +# +sub GenerateDirectoryNames #(hashref inputHints, hashref imageHints) + { + my ($self, $inputHints, $imageHints) = @_; + + my %usedInputNames; + my %usedImageNames; + + + if (defined $inputHints) + { + # First, we have to convert all non-numeric names to numbers, since they may come from a pre-1.32 menu file. We do it + # here instead of in NaturalDocs::Menu to keep the naming scheme centralized. + + my @names = values %$inputHints; + my $hasNonNumeric; + + foreach my $name (@names) + { + if ($name !~ /^[0-9]+$/) + { + $hasNonNumeric = 1; + last; + }; + }; + + + if ($hasNonNumeric) + { + # Hash mapping old names to new names. + my %conversion; + + # The sequential number to use. Starts at two because we want 'default' to be one. + my $currentNumber = 2; + + # If there's only one name, we set it to one no matter what it was set to before. + if (scalar @names == 1) + { $conversion{$names[0]} = 1; } + else + { + # We sort the list first because we want the end result to be predictable. This conversion could be happening on many + # machines, and they may not all specify the input directories in the same order. They need to all come up with the + # same result. + @names = sort @names; + + foreach my $name (@names) + { + if ($name eq 'default') + { $conversion{$name} = 1; } + else + { + $conversion{$name} = $currentNumber; + $currentNumber++; + }; + }; + }; + + # Convert them to the new names. + foreach my $directory (keys %$inputHints) + { + $inputHints->{$directory} = $conversion{ $inputHints->{$directory} }; + }; + }; + + + # Now we apply all the names from the hints, and save any unused ones as removed directories. + + for (my $i = 0; $i < scalar @inputDirectories; $i++) + { + if (exists $inputHints->{$inputDirectories[$i]}) + { + $inputDirectoryNames[$i] = $inputHints->{$inputDirectories[$i]}; + $usedInputNames{ $inputDirectoryNames[$i] } = 1; + delete $inputHints->{$inputDirectories[$i]}; + }; + }; + + + # Any remaining hints are saved as removed directories. + + while (my ($directory, $name) = each %$inputHints) + { + push @removedInputDirectories, $directory; + push @removedInputDirectoryNames, $name; + }; + }; + + + if (defined $imageHints) + { + # Image directory names were never non-numeric, so there is no conversion. Apply all the names from the hints. + + for (my $i = 0; $i < scalar @imageDirectories; $i++) + { + if (exists $imageHints->{$imageDirectories[$i]}) + { + $imageDirectoryNames[$i] = $imageHints->{$imageDirectories[$i]}; + $usedImageNames{ $imageDirectoryNames[$i] } = 1; + delete $imageHints->{$imageDirectories[$i]}; + }; + }; + + + # Any remaining hints are saved as removed directories. + + while (my ($directory, $name) = each %$imageHints) + { + push @removedImageDirectories, $directory; + push @removedImageDirectoryNames, $name; + }; + }; + + + # Now we generate names for anything remaining. + + my $inputCounter = 1; + + for (my $i = 0; $i < scalar @inputDirectories; $i++) + { + if (!defined $inputDirectoryNames[$i]) + { + while (exists $usedInputNames{$inputCounter}) + { $inputCounter++; }; + + $inputDirectoryNames[$i] = $inputCounter; + $usedInputNames{$inputCounter} = 1; + + $inputCounter++; + }; + }; + + + my $imageCounter = 1; + + for (my $i = 0; $i < scalar @imageDirectories; $i++) + { + if (!defined $imageDirectoryNames[$i]) + { + while (exists $usedImageNames{$imageCounter}) + { $imageCounter++; }; + + $imageDirectoryNames[$i] = $imageCounter; + $usedImageNames{$imageCounter} = 1; + + $imageCounter++; + }; + }; + }; + + + +############################################################################### +# Group: Information Functions + + +# +# Function: InputDirectories +# +# Returns an arrayref of input directories. Do not change. +# +# This will not return any removed input directories. +# +sub InputDirectories + { return \@inputDirectories; }; + +# +# Function: InputDirectoryNameOf +# +# Returns the generated name of the passed input directory. <GenerateDirectoryNames()> must be called once before this +# function is available. +# +# If a name for a removed input directory is available, it will be returned as well. +# +sub InputDirectoryNameOf #(directory) + { + my ($self, $directory) = @_; + + for (my $i = 0; $i < scalar @inputDirectories; $i++) + { + if ($directory eq $inputDirectories[$i]) + { return $inputDirectoryNames[$i]; }; + }; + + for (my $i = 0; $i < scalar @removedInputDirectories; $i++) + { + if ($directory eq $removedInputDirectories[$i]) + { return $removedInputDirectoryNames[$i]; }; + }; + + return undef; + }; + + +# +# Function: SplitFromInputDirectory +# +# Takes an input file name and returns the array ( inputDirectory, relativePath ). +# +# If the file cannot be split from an input directory, it will try to do it with the removed input directories. +# +sub SplitFromInputDirectory #(file) + { + my ($self, $file) = @_; + + foreach my $directory (@inputDirectories, @removedInputDirectories) + { + if (NaturalDocs::File->IsSubPathOf($directory, $file)) + { return ( $directory, NaturalDocs::File->MakeRelativePath($directory, $file) ); }; + }; + + return ( ); + }; + + +# +# Function: ImageDirectories +# +# Returns an arrayref of image directories. Do not change. +# +# This will not return any removed image directories. +# +sub ImageDirectories + { return \@imageDirectories; }; + + +# +# Function: ImageDirectoryNameOf +# +# Returns the generated name of the passed image or input directory. <GenerateDirectoryNames()> must be called once before +# this function is available. +# +# If a name for a removed input or image directory is available, it will be returned as well. +# +sub ImageDirectoryNameOf #(directory) + { + my ($self, $directory) = @_; + + for (my $i = 0; $i < scalar @imageDirectories; $i++) + { + if ($directory eq $imageDirectories[$i]) + { return $imageDirectoryNames[$i]; }; + }; + + for (my $i = 0; $i < scalar @removedImageDirectories; $i++) + { + if ($directory eq $removedImageDirectories[$i]) + { return $removedImageDirectoryNames[$i]; }; + }; + + return undef; + }; + + +# +# Function: SplitFromImageDirectory +# +# Takes an input image file name and returns the array ( imageDirectory, relativePath ). +# +# If the file cannot be split from an image directory, it will try to do it with the removed image directories. +# +sub SplitFromImageDirectory #(file) + { + my ($self, $file) = @_; + + foreach my $directory (@imageDirectories, @removedImageDirectories) + { + if (NaturalDocs::File->IsSubPathOf($directory, $file)) + { return ( $directory, NaturalDocs::File->MakeRelativePath($directory, $file) ); }; + }; + + return ( ); + }; + + +# +# Function: RelativeImageDirectories +# +# Returns an arrayref of relative image directories. Do not change. +# +sub RelativeImageDirectories + { return \@relativeImageDirectories; }; + + +# Function: ExcludedInputDirectories +# Returns an arrayref of input directories to exclude. Do not change. +sub ExcludedInputDirectories + { return \@excludedInputDirectories; }; + + +# Function: BuildTargets +# Returns an arrayref of <NaturalDocs::Settings::BuildTarget>s. Do not change. +sub BuildTargets + { return \@buildTargets; }; + + +# +# Function: OutputDirectoryOf +# +# Returns the output directory of a builder object. +# +# Parameters: +# +# object - The builder object, whose class is derived from <NaturalDocs::Builder::Base>. +# +# Returns: +# +# The builder directory, or undef if the object wasn't found.. +# +sub OutputDirectoryOf #(object) + { + my ($self, $object) = @_; + + foreach my $buildTarget (@buildTargets) + { + if ($buildTarget->Builder() == $object) + { return $buildTarget->Directory(); }; + }; + + return undef; + }; + + +# Function: Styles +# Returns an arrayref of the styles associated with the output. +sub Styles + { return \@styles; }; + +# Function: ProjectDirectory +# Returns the project directory. +sub ProjectDirectory + { return $projectDirectory; }; + +# Function: ProjectDataDirectory +# Returns the project data directory. +sub ProjectDataDirectory + { return NaturalDocs::File->JoinPaths($projectDirectory, 'Data', 1); }; + +# Function: StyleDirectory +# Returns the main style directory. +sub StyleDirectory + { return NaturalDocs::File->JoinPaths($FindBin::RealBin, 'Styles', 1); }; + +# Function: JavaScriptDirectory +# Returns the main JavaScript directory. +sub JavaScriptDirectory + { return NaturalDocs::File->JoinPaths($FindBin::RealBin, 'JavaScript', 1); }; + +# Function: ConfigDirectory +# Returns the main configuration directory. +sub ConfigDirectory + { return NaturalDocs::File->JoinPaths($FindBin::RealBin, 'Config', 1); }; + +# Function: DocumentedOnly +# Returns whether undocumented code aspects should be included in the output. +sub DocumentedOnly + { return $documentedOnly; }; + +# Function: TabLength +# Returns the number of spaces tabs should be expanded to. +sub TabLength + { return $tabLength; }; + +# Function: NoAutoGroup +# Returns whether auto-grouping is turned off. +sub NoAutoGroup + { return $noAutoGroup; }; + +# Function: OnlyFileTitles +# Returns whether source files should always use the file name as the title. +sub OnlyFileTitles + { return $onlyFileTitles; }; + +# Function: IsQuiet +# Returns whether the script should be run in quiet mode or not. +sub IsQuiet + { return $isQuiet; }; + +# Function: RebuildData +# Returns whether all data files should be ignored and rebuilt. +sub RebuildData + { return $rebuildData; }; + +# Function: CharSet +# Returns the character set, or undef if none. +sub CharSet + { return $charset; }; + + +############################################################################### +# Group: Constant Functions + +# +# Function: AppVersion +# +# Returns Natural Docs' version number as an integer. Use <TextAppVersion()> to get a printable version. +# +sub AppVersion + { + my ($self) = @_; + return NaturalDocs::Version->FromString($self->TextAppVersion()); + }; + +# +# Function: TextAppVersion +# +# Returns Natural Docs' version number as plain text. +# +sub TextAppVersion + { + return '1.4'; + }; + +# +# Function: AppURL +# +# Returns a string of the project's current web address. +# +sub AppURL + { return 'http://www.naturaldocs.org'; }; + + + +############################################################################### +# Group: Support Functions + + +# +# Function: ParseCommandLine +# +# Parses and validates the command line. Will cause the script to exit if the options ask for the syntax reference or +# are invalid. +# +sub ParseCommandLine + { + my ($self) = @_; + + my %synonyms = ( 'input' => '-i', + 'source' => '-i', + 'excludeinput' => '-xi', + 'excludesource' => '-xi', + 'images' => '-img', + 'output' => '-o', + 'project' => '-p', + 'documentedonly' => '-do', + 'style' => '-s', + 'rebuild' => '-r', + 'rebuildoutput' => '-ro', + 'tablength' => '-t', + 'quiet' => '-q', + 'headersonly' => '-ho', + 'help' => '-h', + 'autogroup' => '-ag', + 'noautogroup' => '-nag', + 'onlyfiletitles' => '-oft', + 'onlyfiletitle' => '-oft', + 'charset' => '-cs', + 'characterset' => '-cs' ); + + + my @errorMessages; + + my $valueRef; + my $option; + + my @outputStrings; + my @imageStrings; + + + # Sometimes $valueRef is set to $ignored instead of undef because we don't want certain errors to cause other, + # unnecessary errors. For example, if they set the input directory twice, we want to show that error and swallow the + # specified directory without complaint. Otherwise it would complain about the directory too as if it were random crap + # inserted into the command line. + my $ignored; + + my $index = 0; + + while ($index < scalar @ARGV) + { + my $arg = $ARGV[$index]; + + if (substr($arg, 0, 1) eq '-') + { + $option = lc($arg); + + # Support options like -t2 as well as -t 2. + if ($option =~ /^([^0-9]+)([0-9]+)$/) + { + $option = $1; + splice(@ARGV, $index + 1, 0, $2); + }; + + # Convert long forms to short. + if (substr($option, 1, 1) eq '-') + { + # Strip all dashes. + my $newOption = $option; + $newOption =~ tr/-//d; + + if (exists $synonyms{$newOption}) + { $option = $synonyms{$newOption}; } + } + + if ($option eq '-i') + { + push @inputDirectories, undef; + $valueRef = \$inputDirectories[-1]; + } + elsif ($option eq '-xi') + { + push @excludedInputDirectories, undef; + $valueRef = \$excludedInputDirectories[-1]; + } + elsif ($option eq '-img') + { + push @imageStrings, undef; + $valueRef = \$imageStrings[-1]; + } + elsif ($option eq '-p') + { + if (defined $projectDirectory) + { + push @errorMessages, 'You cannot have more than one project directory.'; + $valueRef = \$ignored; + } + else + { $valueRef = \$projectDirectory; }; + } + elsif ($option eq '-o') + { + push @outputStrings, undef; + $valueRef = \$outputStrings[-1]; + } + elsif ($option eq '-s') + { + $valueRef = \$styles[0]; + } + elsif ($option eq '-t') + { + $valueRef = \$tabLength; + } + elsif ($option eq '-cs') + { + $valueRef = \$charset; + } + elsif ($option eq '-ag') + { + push @errorMessages, 'The -ag setting is no longer supported. You can use -nag (--no-auto-group) to turn off ' + . "auto-grouping, but there aren't multiple levels anymore."; + $valueRef = \$ignored; + } + + # Options that aren't followed by content. + else + { + $valueRef = undef; + + if ($option eq '-r') + { + NaturalDocs::Project->ReparseEverything(); + NaturalDocs::Project->RebuildEverything(); + $rebuildData = 1; + } + elsif ($option eq '-ro') + { + NaturalDocs::Project->RebuildEverything(); + } + elsif ($option eq '-do') + { $documentedOnly = 1; } + elsif ($option eq '-oft') + { $onlyFileTitles = 1; } + elsif ($option eq '-q') + { $isQuiet = 1; } + elsif ($option eq '-ho') + { + push @errorMessages, 'The -ho setting is no longer supported. You can have Natural Docs skip over the source file ' + . 'extensions by editing Languages.txt in your project directory.'; + } + elsif ($option eq '-nag') + { $noAutoGroup = 1; } + elsif ($option eq '-?' || $option eq '-h') + { + $self->PrintSyntax(); + exit; + } + else + { push @errorMessages, 'Unrecognized option ' . $option; }; + + }; + + } + + # Is a segment of text, not an option... + else + { + if (defined $valueRef) + { + # We want to preserve spaces in paths. + if (defined $$valueRef) + { $$valueRef .= ' '; }; + + $$valueRef .= $arg; + } + + else + { + push @errorMessages, 'Unrecognized element ' . $arg; + }; + }; + + $index++; + }; + + + # Validate the style, if specified. + + if ($styles[0]) + { + my @stylePieces = split(/ +/, $styles[0]); + @styles = ( ); + + while (scalar @stylePieces) + { + if (lc($stylePieces[0]) eq 'custom') + { + push @errorMessages, 'The "Custom" style setting is no longer supported. Copy your custom style sheet to your ' + . 'project directory and you can refer to it with -s.'; + shift @stylePieces; + } + else + { + # People may use styles with spaces in them. If a style doesn't exist, we need to join the pieces until we find one that + # does or we run out of pieces. + + my $extras = 0; + my $success; + + while ($extras < scalar @stylePieces) + { + my $style; + + if (!$extras) + { $style = $stylePieces[0]; } + else + { $style = join(' ', @stylePieces[0..$extras]); }; + + my $cssFile = NaturalDocs::File->JoinPaths( $self->StyleDirectory(), $style . '.css' ); + if (-e $cssFile) + { + push @styles, $style; + splice(@stylePieces, 0, 1 + $extras); + $success = 1; + last; + } + else + { + $cssFile = NaturalDocs::File->JoinPaths( $self->ProjectDirectory(), $style . '.css' ); + + if (-e $cssFile) + { + push @styles, $style; + splice(@stylePieces, 0, 1 + $extras); + $success = 1; + last; + } + else + { $extras++; }; + }; + }; + + if (!$success) + { + push @errorMessages, 'The style "' . $stylePieces[0] . '" does not exist.'; + shift @stylePieces; + }; + }; + }; + } + else + { @styles = ( 'Default' ); }; + + + # Decode and validate the output strings. + + my %outputDirectories; + + foreach my $outputString (@outputStrings) + { + my ($format, $directory) = split(/ /, $outputString, 2); + + if (!defined $directory) + { push @errorMessages, 'The -o option needs two parameters: -o [format] [directory]'; } + else + { + if (!NaturalDocs::File->PathIsAbsolute($directory)) + { $directory = NaturalDocs::File->JoinPaths(Cwd::cwd(), $directory, 1); }; + + $directory = NaturalDocs::File->CanonizePath($directory); + + if (! -e $directory || ! -d $directory) + { + # They may have forgotten the format portion and the directory name had a space in it. + if (-e ($format . ' ' . $directory) && -d ($format . ' ' . $directory)) + { + push @errorMessages, 'The -o option needs two parameters: -o [format] [directory]'; + $format = undef; + } + else + { push @errorMessages, 'The output directory ' . $directory . ' does not exist.'; } + } + elsif (exists $outputDirectories{$directory}) + { push @errorMessages, 'You cannot specify the output directory ' . $directory . ' more than once.'; } + else + { $outputDirectories{$directory} = 1; }; + + if (defined $format) + { + my $builderPackage = NaturalDocs::Builder->OutputPackageOf($format); + + if (defined $builderPackage) + { + push @buildTargets, + NaturalDocs::Settings::BuildTarget->New($builderPackage->New(), $directory); + } + else + { + push @errorMessages, 'The output format ' . $format . ' doesn\'t exist or is not installed.'; + $valueRef = \$ignored; + }; + }; + }; + }; + + if (!scalar @buildTargets) + { push @errorMessages, 'You did not specify an output directory.'; }; + + + # Decode and validate the image strings. + + foreach my $imageString (@imageStrings) + { + if ($imageString =~ /^ *\*/) + { + # The below NaturalDocs::File functions assume everything is canonized. + $imageString = NaturalDocs::File->CanonizePath($imageString); + + my ($volume, $directoryString) = NaturalDocs::File->SplitPath($imageString, 1); + my @directories = NaturalDocs::File->SplitDirectories($directoryString); + + shift @directories; + + $directoryString = NaturalDocs::File->JoinDirectories(@directories); + push @relativeImageDirectories, NaturalDocs::File->JoinPath($volume, $directoryString); + } + else + { + if (!NaturalDocs::File->PathIsAbsolute($imageString)) + { $imageString = NaturalDocs::File->JoinPaths(Cwd::cwd(), $imageString, 1); }; + + $imageString = NaturalDocs::File->CanonizePath($imageString); + + if (! -e $imageString || ! -d $imageString) + { push @errorMessages, 'The image directory ' . $imageString . ' does not exist.'; }; + + push @imageDirectories, $imageString; + }; + }; + + + # Make sure the input and project directories are specified, canonized, and exist. + + if (scalar @inputDirectories) + { + for (my $i = 0; $i < scalar @inputDirectories; $i++) + { + if (!NaturalDocs::File->PathIsAbsolute($inputDirectories[$i])) + { $inputDirectories[$i] = NaturalDocs::File->JoinPaths(Cwd::cwd(), $inputDirectories[$i], 1); }; + + $inputDirectories[$i] = NaturalDocs::File->CanonizePath($inputDirectories[$i]); + + if (! -e $inputDirectories[$i] || ! -d $inputDirectories[$i]) + { push @errorMessages, 'The input directory ' . $inputDirectories[$i] . ' does not exist.'; }; + }; + } + else + { push @errorMessages, 'You did not specify an input (source) directory.'; }; + + if (defined $projectDirectory) + { + if (!NaturalDocs::File->PathIsAbsolute($projectDirectory)) + { $projectDirectory = NaturalDocs::File->JoinPaths(Cwd::cwd(), $projectDirectory, 1); }; + + $projectDirectory = NaturalDocs::File->CanonizePath($projectDirectory); + + if (! -e $projectDirectory || ! -d $projectDirectory) + { push @errorMessages, 'The project directory ' . $projectDirectory . ' does not exist.'; }; + + # Create the Data subdirectory if it doesn't exist. + NaturalDocs::File->CreatePath( NaturalDocs::File->JoinPaths($projectDirectory, 'Data', 1) ); + } + else + { push @errorMessages, 'You did not specify a project directory.'; }; + + + # Make sure the excluded input directories are canonized, and add the project and output directories to the list. + + for (my $i = 0; $i < scalar @excludedInputDirectories; $i++) + { + if (!NaturalDocs::File->PathIsAbsolute($excludedInputDirectories[$i])) + { $excludedInputDirectories[$i] = NaturalDocs::File->JoinPaths(Cwd::cwd(), $excludedInputDirectories[$i], 1); }; + + $excludedInputDirectories[$i] = NaturalDocs::File->CanonizePath($excludedInputDirectories[$i]); + }; + + push @excludedInputDirectories, $projectDirectory; + + foreach my $buildTarget (@buildTargets) + { + push @excludedInputDirectories, $buildTarget->Directory(); + }; + + + # Determine the tab length, and default to four if not specified. + + if (defined $tabLength) + { + if ($tabLength !~ /^[0-9]+$/) + { push @errorMessages, 'The tab length must be a number.'; }; + } + else + { $tabLength = 4; }; + + + # Strip any quotes off of the charset. + $charset =~ tr/\"//d; + + + # Exit with the error message if there was one. + + if (scalar @errorMessages) + { + print join("\n", @errorMessages) . "\nType NaturalDocs -h to see the syntax reference.\n"; + exit; + }; + }; + +# +# Function: PrintSyntax +# +# Prints the syntax reference. +# +sub PrintSyntax + { + my ($self) = @_; + + # Make sure all line lengths are under 80 characters. + + print + + "Natural Docs, version " . $self->TextAppVersion() . "\n" + . $self->AppURL() . "\n" + . "This program is licensed under the GPL\n" + . "--------------------------------------\n" + . "\n" + . "Syntax:\n" + . "\n" + . " NaturalDocs -i [input (source) directory]\n" + . " (-i [input (source) directory] ...)\n" + . " -o [output format] [output directory]\n" + . " (-o [output format] [output directory] ...)\n" + . " -p [project directory]\n" + . " [options]\n" + . "\n" + . "Examples:\n" + . "\n" + . " NaturalDocs -i C:\\My Project\\Source -o HTML C:\\My Project\\Docs\n" + . " -p C:\\My Project\\Natural Docs\n" + . " NaturalDocs -i /src/project -o HTML /doc/project\n" + . " -p /etc/naturaldocs/project -s Small -q\n" + . "\n" + . "Required Parameters:\n" + . "\n" + . " -i [dir]\n--input [dir]\n--source [dir]\n" + . " Specifies an input (source) directory. Required.\n" + . " Can be specified multiple times.\n" + . "\n" + . " -o [fmt] [dir]\n--output [fmt] [dir]\n" + . " Specifies an output format and directory. Required.\n" + . " Can be specified multiple times, but only once per directory.\n" + . " Possible output formats:\n"; + + $self->PrintOutputFormats(' - '); + + print + "\n" + . " -p [dir]\n--project [dir]\n" + . " Specifies the project directory. Required.\n" + . " There needs to be a unique project directory for every source directory.\n" + . "\n" + . "Optional Parameters:\n" + . "\n" + . " -s [style] ([style] [style] ...)\n--style [style] ([style] [style] ...)\n" + . " Specifies the CSS style when building HTML output. If multiple styles are\n" + . " specified, they will all be included in the order given.\n" + . "\n" + . " -img [image directory]\n--image [image directory]" + . " Specifies an image directory. Can be specified multiple times.\n" + . " Start with * to specify a relative directory, as in -img */images.\n" + . "\n" + . " -do\n--documented-only\n" + . " Specifies only documented code aspects should be included in the output.\n" + . "\n" + . " -t [len]\n--tab-length [len]\n" + . " Specifies the number of spaces tabs should be expanded to. This only needs\n" + . " to be set if you use tabs in example code and text diagrams. Defaults to 4.\n" + . "\n" + . " -xi [dir]\n--exclude-input [dir]\n--exclude-source [dir]\n" + . " Excludes an input (source) directory from the documentation.\n" + . " Automatically done for the project and output directories. Can\n" + . " be specified multiple times.\n" + . "\n" + . " -nag\n--no-auto-group\n" + . " Turns off auto-grouping completely.\n" + . "\n" + . " -oft\n--only-file-titles\n" + . " Source files will only use the file name as the title.\n" + . "\n" + . " -r\n--rebuild\n" + . " Rebuilds all output and data files from scratch.\n" + . " Does not affect the menu file.\n" + . "\n" + . " -ro\n--rebuild-output\n" + . " Rebuilds all output files from scratch.\n" + . "\n" + . " -q\n--quiet\n" + . " Suppresses all non-error output.\n" + . "\n" + . " -?\n -h\n--help\n" + . " Displays this syntax reference.\n"; + }; + + +# +# Function: PrintOutputFormats +# +# Prints all the possible output formats that can be specified with -o. Each one will be placed on its own line. +# +# Parameters: +# +# prefix - Characters to prefix each one with, such as for indentation. +# +sub PrintOutputFormats #(prefix) + { + my ($self, $prefix) = @_; + + my $outputPackages = NaturalDocs::Builder::OutputPackages(); + + foreach my $outputPackage (@$outputPackages) + { + print $prefix . $outputPackage->CommandLineOption() . "\n"; + }; + }; + + +# +# Function: LoadAndComparePreviousSettings +# +# Loads <PreviousSettings.nd> and compares the values there with those in the command line. If differences require it, +# sets <rebuildData> and/or <rebuildOutput>. +# +sub LoadAndComparePreviousSettings + { + my ($self) = @_; + + my $fileIsOkay; + + if (!NaturalDocs::Settings->RebuildData()) + { + my $version; + + if (NaturalDocs::BinaryFile->OpenForReading( NaturalDocs::Project->DataFile('PreviousSettings.nd'), + NaturalDocs::Version->FromString('1.4') )) + { $fileIsOkay = 1; }; + }; + + if (!$fileIsOkay) + { + # We need to reparse everything because --documented-only may have changed. + # We need to rebuild everything because --tab-length may have changed. + NaturalDocs::Project->ReparseEverything(); + NaturalDocs::Project->RebuildEverything(); + } + else + { + my $raw; + + # [UInt8: tab expansion] + # [UInt8: documented only (0 or 1)] + # [UInt8: no auto-group (0 or 1)] + # [UInt8: only file titles (0 or 1)] + # [AString16: charset] + + my $prevTabLength = NaturalDocs::BinaryFile->GetUInt8(); + my $prevDocumentedOnly = NaturalDocs::BinaryFile->GetUInt8(); + my $prevNoAutoGroup = NaturalDocs::BinaryFile->GetUInt8(); + my $prevOnlyFileTitles = NaturalDocs::BinaryFile->GetUInt8(); + my $prevCharset = NaturalDocs::BinaryFile->GetAString16(); + + if ($prevTabLength != $self->TabLength()) + { + # We need to rebuild all output because this affects all text diagrams. + NaturalDocs::Project->RebuildEverything(); + }; + + if ($prevDocumentedOnly == 0) + { $prevDocumentedOnly = undef; }; + if ($prevNoAutoGroup == 0) + { $prevNoAutoGroup = undef; }; + if ($prevOnlyFileTitles == 0) + { $prevOnlyFileTitles = undef; }; + + if ($prevDocumentedOnly != $self->DocumentedOnly() || + $prevNoAutoGroup != $self->NoAutoGroup() || + $prevOnlyFileTitles != $self->OnlyFileTitles()) + { + NaturalDocs::Project->ReparseEverything(); + }; + + if ($prevCharset ne $charset) + { NaturalDocs::Project->RebuildEverything(); }; + + + # [UInt8: number of input directories] + + my $inputDirectoryCount = NaturalDocs::BinaryFile->GetUInt8(); + + while ($inputDirectoryCount) + { + # [AString16: input directory] [AString16: input directory name] ... + + my $inputDirectory = NaturalDocs::BinaryFile->GetAString16(); + my $inputDirectoryName = NaturalDocs::BinaryFile->GetAString16(); + + # Not doing anything with this for now. + + $inputDirectoryCount--; + }; + + + # [UInt8: number of output targets] + + my $outputTargetCount = NaturalDocs::BinaryFile->GetUInt8(); + + # Keys are the directories, values are the command line options. + my %previousOutputDirectories; + + while ($outputTargetCount) + { + # [AString16: output directory] [AString16: output format command line option] ... + + my $outputDirectory = NaturalDocs::BinaryFile->GetAString16(); + my $outputCommand = NaturalDocs::BinaryFile->GetAString16(); + + $previousOutputDirectories{$outputDirectory} = $outputCommand; + + $outputTargetCount--; + }; + + # Check if any targets were added to the command line, or if their formats changed. We don't care if targets were + # removed. + my $buildTargets = $self->BuildTargets(); + + foreach my $buildTarget (@$buildTargets) + { + if (!exists $previousOutputDirectories{$buildTarget->Directory()} || + $buildTarget->Builder()->CommandLineOption() ne $previousOutputDirectories{$buildTarget->Directory()}) + { + NaturalDocs::Project->RebuildEverything(); + last; + }; + }; + + NaturalDocs::BinaryFile->Close(); + }; + }; + + +# +# Function: SavePreviousSettings +# +# Saves the settings into <PreviousSettings.nd>. +# +sub SavePreviousSettings + { + my ($self) = @_; + + NaturalDocs::BinaryFile->OpenForWriting( NaturalDocs::Project->DataFile('PreviousSettings.nd') ); + + # [UInt8: tab length] + # [UInt8: documented only (0 or 1)] + # [UInt8: no auto-group (0 or 1)] + # [UInt8: only file titles (0 or 1)] + # [AString16: charset] + # [UInt8: number of input directories] + + my $inputDirectories = $self->InputDirectories(); + + NaturalDocs::BinaryFile->WriteUInt8($self->TabLength()); + NaturalDocs::BinaryFile->WriteUInt8($self->DocumentedOnly() ? 1 : 0); + NaturalDocs::BinaryFile->WriteUInt8($self->NoAutoGroup() ? 1 : 0); + NaturalDocs::BinaryFile->WriteUInt8($self->OnlyFileTitles() ? 1 : 0); + NaturalDocs::BinaryFile->WriteAString16($charset); + NaturalDocs::BinaryFile->WriteUInt8(scalar @$inputDirectories); + + foreach my $inputDirectory (@$inputDirectories) + { + my $inputDirectoryName = $self->InputDirectoryNameOf($inputDirectory); + + # [AString16: input directory] [AString16: input directory name] ... + NaturalDocs::BinaryFile->WriteAString16($inputDirectory); + NaturalDocs::BinaryFile->WriteAString16($inputDirectoryName); + }; + + # [UInt8: number of output targets] + + my $buildTargets = $self->BuildTargets(); + NaturalDocs::BinaryFile->WriteUInt8(scalar @$buildTargets); + + foreach my $buildTarget (@$buildTargets) + { + # [AString16: output directory] [AString16: output format command line option] ... + NaturalDocs::BinaryFile->WriteAString16( $buildTarget->Directory() ); + NaturalDocs::BinaryFile->WriteAString16( $buildTarget->Builder()->CommandLineOption() ); + }; + + NaturalDocs::BinaryFile->Close(); + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Settings/BuildTarget.pm b/docs/tool/Modules/NaturalDocs/Settings/BuildTarget.pm new file mode 100644 index 00000000..b1a01b9a --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Settings/BuildTarget.pm @@ -0,0 +1,66 @@ +############################################################################### +# +# Class: NaturalDocs::Settings::BuildTarget +# +############################################################################### +# +# A class that stores information about a build target. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Settings::BuildTarget; + +use NaturalDocs::DefineMembers 'BUILDER', 'Builder()', 'SetBuilder()', + 'DIRECTORY', 'Directory()', 'SetDirectory()'; + + +# +# Constants: Members +# +# The class is implemented as a blessed arrayref with the members below. +# +# BUILDER - The <NaturalDocs::Builder::Base>-derived object for the target's output format. +# DIRECTORY - The output directory of the target. +# + +# +# Function: New +# +# Creates and returns a new object. +# +# Parameters: +# +# builder - The <NaturalDocs::Builder::Base>-derived object for the target's output format. +# directory - The directory to place the output files in. +# +sub New #(builder, directory) + { + my ($package, $builder, $directory) = @_; + + my $object = [ ]; + bless $object, $package; + + $object->SetBuilder($builder); + $object->SetDirectory($directory); + + return $object; + }; + + +# +# Functions: Member Functions +# +# Builder - Returns the <NaturalDocs::Builder::Base>-derived object for the target's output format. +# SetBuilder - Replaces the <NaturalDocs::Builder::Base>-derived object for the target's output format. +# Directory - Returns the directory for the target's output files. +# SetDirectory - Replaces the directory for the target's output files. +# + + +1; diff --git a/docs/tool/Modules/NaturalDocs/SourceDB.pm b/docs/tool/Modules/NaturalDocs/SourceDB.pm new file mode 100644 index 00000000..a1928f95 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/SourceDB.pm @@ -0,0 +1,678 @@ +############################################################################### +# +# Package: NaturalDocs::SourceDB +# +############################################################################### +# +# SourceDB is an experimental package meant to unify the tracking of various elements in the source code. +# +# Requirements: +# +# - All extension packages must call <RegisterExtension()> before they can be used. +# +# +# Architecture: The Idea +# +# For quite a while Natural Docs only needed <SymbolTable>. However, 1.3 introduced the <ClassHierarchy> package +# which duplicated some of its functionality to track classes and parent references. 1.4 now needs <ImageReferenceTable>, +# so this package was an attempt to isolate the common functionality so the wheel doesn't have to keep being rewritten as +# the scope of Natural Docs expands. +# +# SourceDB is designed around <Extensions> and items. The purposefully vague "items" are anything in the source code +# that we need to track the definitions of. Extensions are the packages to track them, only they're derived from +# <NaturalDocs::SourceDB::Extension> and registered with this package instead of being free standing and duplicating +# functionality such as watched files. +# +# The architecture on this package isn't comprehensive yet. As more extensions are added or previously made free standing +# packages are migrated to it it will expand to encompass them. However, it's still experimental so this concept may +# eventually be abandoned for something better instead. +# +# +# Architecture: Assumptions +# +# SourceDB is built around certain assumptions. +# +# One item per file: +# +# SourceDB assumes that only the first item per file with a particular item string is relevant. For example, if two functions +# have the exact same name, there's no way to link to the second one either in HTML or internally so it doesn't matter for +# our purposes. Likewise, if two references are exactly the same they go to the same target, so it doesn't matter whether +# there's one or two or a thousand. All that matters is that at least one reference exists in this file because you only need +# to determine whether the entire file gets rebuilt. If two items are different in some meaningful way, they should generate +# different item strings. +# +# Watched file parsing: +# +# SourceDB assumes the parse method is that the information that was stored from Natural Docs' previous run is loaded, a +# file is watched, that file is reparsed, and then <AnalyzeWatchedFileChanges()> is called. When the file is reparsed all +# items within it are added the same as if the file was never parsed before. +# +# If there's a new item this time around, that's fine no matter what. However, a changed item wouldn't normally be +# recorded because the previous run's definition is seen as the first one and subsequent ones are ignored. Also, deleted +# items would normally not be recorded either because we're only adding. +# +# The watched file method fixes this because everything is also added to a second, clean database specifically for the +# watched file. Because it starts clean, it always gets the first definition from the current parse which can then be +# compared to the original by <AnalyzeWatchedFileChanges()>. Because it starts clean you can also compare it to the +# main database to see if anything was deleted, because it would appear in the main database but not the watched one. +# +# This means that functions like <ChangeDefinition()> and <DeleteDefinition()> should only be called by +# <AnalyzeWatchedFileChanges()>. Externally only <AddDefinition()> should be called. <DeleteItem()> is okay to be +# called externally because entire items aren't managed by the watched file database, only definitions. +# +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + + +use NaturalDocs::SourceDB::Extension; +use NaturalDocs::SourceDB::Item; +use NaturalDocs::SourceDB::ItemDefinition; +use NaturalDocs::SourceDB::File; +use NaturalDocs::SourceDB::WatchedFileDefinitions; + + +package NaturalDocs::SourceDB; + + +############################################################################### +# Group: Types + + +# +# Type: ExtensionID +# +# A unique identifier for each <NaturalDocs::SourceDB> extension as given out by <RegisterExtension()>. +# + + + +############################################################################### +# Group: Variables + + +# +# array: extensions +# +# An array of <NaturalDocs::SourceDB::Extension>-derived extensions, as added with <RegisterExtension()>. The indexes +# are the <ExtensionIDs> and the values are package references. +# +my @extensions; + +# +# array: extensionUsesDefinitionObjects +# +# An array where the indexes are <ExtensionIDs> and the values are whether that extension uses its own definition class +# derived from <NaturalDocs::SourceDB::ItemDefinition> or it just tracks their existence. +# +my @extensionUsesDefinitionObjects; + + + +# +# array: items +# +# The array of source items. The <ExtensionIDs> are the indexes, and the values are hashrefs mapping the item +# string to <NaturalDocs::SourceDB::Item>-derived objects. Hashrefs may be undef. +# +my @items; + + +# +# hash: files +# +# A hashref mapping source <FileNames> to <NaturalDocs::SourceDB::Files>. +# +my %files; + + +# +# object: watchedFile +# +# When a file is being watched for changes, will be a <NaturalDocs::SourceDB::File> for that file. Is undef otherwise. +# +# When the file is parsed, items are added to both this and the version in <files>. Thus afterwards we can compare the two to +# see if any were deleted since the last time Natural Docs was run, because they would be in the <files> version but not this +# one. +# +my $watchedFile; + + +# +# string: watchedFileName +# +# When a file is being watched for changes, will be the <FileName> of the file being watched. Is undef otherwise. +# +my $watchedFileName; + + +# +# object: watchedFileDefinitions +# +# When a file is being watched for changes, will be a <NaturalDocs::SourceDB::WatchedFileDefinitions> object. Is undef +# otherwise. +# +# When the file is parsed, items are added to both this and the version in <items>. Since only the first definition is kept, this +# will always have the definition info from the file whereas the version in <items> will have the first definition as of the last time +# Natural Docs was run. Thus they can be compared to see if the definitions of items that existed the last time around have +# changed. +# +my $watchedFileDefinitions; + + + +############################################################################### +# Group: Extension Functions + + +# +# Function: RegisterExtension +# +# Registers a <NaturalDocs::SourceDB::Extension>-derived package and returns a unique <ExtensionID> for it. All extensions +# must call this before they can be used. +# +# Registration Order: +# +# The order in which extensions register is important. Whenever possible, items are added in the order their extensions +# registered. However, items are changed and deleted in the reverse order. Take advantage of this to minimize +# churn between extensions that are dependent on each other. +# +# For example, when symbols are added or deleted they may cause references to be retargeted and thus their files need to +# be rebuilt. However, adding or deleting references never causes the symbols' files to be rebuilt. So it makes sense that +# symbols should be created before references, and that references should be deleted before symbols. +# +# Parameters: +# +# extension - The package or object of the extension. Must be derived from <NaturalDocs::SourceDB::Extension>. +# usesDefinitionObjects - Whether the extension uses its own class derived from <NaturalDocs::SourceDB::ItemDefinition> +# or simply tracks each definitions existence. +# +# Returns: +# +# An <ExtensionID> unique to the extension. This should be saved because it's required in functions such as <AddItem()>. +# +sub RegisterExtension #(package extension, bool usesDefinitionObjects) => ExtensionID + { + my ($self, $extension, $usesDefinitionObjects) = @_; + + push @extensions, $extension; + push @extensionUsesDefinitionObjects, $usesDefinitionObjects; + + return scalar @extensions - 1; + }; + + + + +############################################################################### +# Group: File Functions + + +# +# Function: Load +# +# Loads the data of the source database and all the extensions. Will call <NaturalDocs::SourceDB::Extension->Load()> for +# all of them, unless there's a situation where all the source files are going to be reparsed anyway in which case it's not needed. +# +sub Load + { + my $self = shift; + + # No point loading if RebuildData is set. + if (!NaturalDocs::Settings->RebuildData()) + { + # If any load fails, stop loading the rest and just reparse all the source files. + my $success = 1; + + for (my $extension = 0; $extension < scalar @extensions && $success; $extension++) + { + $success = $extensions[$extension]->Load(); + }; + + if (!$success) + { NaturalDocs::Project->ReparseEverything(); }; + }; + }; + + +# +# Function: Save +# +# Saves the data of the source database and all its extensions. Will call <NaturalDocs::SourceDB::Extension->Save()> for all +# of them. +# +sub Save + { + my $self = shift; + + for (my $extension = scalar @extensions - 1; $extension >= 0; $extension--) + { + $extensions[$extension]->Save(); + }; + }; + + +# +# Function: PurgeDeletedSourceFiles +# +# Removes all data associated with deleted source files. +# +sub PurgeDeletedSourceFiles + { + my $self = shift; + + my $filesToPurge = NaturalDocs::Project->FilesToPurge(); + + # Extension is the outermost loop because we want the extensions added last to have their definitions removed first to cause + # the least amount of churn between interdependent extensions. + for (my $extension = scalar @extensions - 1; $extension >= 0; $extension--) + { + foreach my $file (keys %$filesToPurge) + { + if (exists $files{$file}) + { + my @items = $files{$file}->ListItems($extension); + + foreach my $item (@items) + { + $self->DeleteDefinition($extension, $item, $file); + }; + }; # file exists + }; # each file + }; # each extension + }; + + + + + +############################################################################### +# Group: Item Functions + + +# +# Function: AddItem +# +# Adds the passed item to the database. This will not work if the item string already exists. The item added should *not* +# already have definitions attached. Only use this to add blank items and then call <AddDefinition()> instead. +# +# Parameters: +# +# extension - An <ExtensionID>. +# itemString - The string serving as the item identifier. +# item - An object derived from <NaturalDocs::SourceDB::Item>. +# +# Returns: +# +# Whether the item was added, that is, whether it was the first time this item was added. +# +sub AddItem #(ExtensionID extension, string itemString, NaturalDocs::SourceDB::Item item) => bool + { + my ($self, $extension, $itemString, $item) = @_; + + if (!defined $items[$extension]) + { $items[$extension] = { }; }; + + if (!exists $items[$extension]->{$itemString}) + { + if ($item->HasDefinitions()) + { die "Tried to add an item to SourceDB that already had definitions."; }; + + $items[$extension]->{$itemString} = $item; + return 1; + }; + + return 0; + }; + + +# +# Function: GetItem +# +# Returns the <NaturalDocs::SourceDB::Item>-derived object for the passed <ExtensionID> and item string, or undef if there +# is none. +# +sub GetItem #(ExtensionID extension, string itemString) => bool + { + my ($self, $extensionID, $itemString) = @_; + + if (defined $items[$extensionID]) + { return $items[$extensionID]->{$itemString}; } + else + { return undef; }; + }; + + +# +# Function: DeleteItem +# +# Deletes the record of the passed <ExtensionID> and item string. Do *not* delete items that still have definitions. Use +# <DeleteDefinition()> first. +# +# Parameters: +# +# extension - The <ExtensionID>. +# itemString - The item's identifying string. +# +# Returns: +# +# Whether it was successful, meaning whether an entry existed for it. +# +sub DeleteItem #(ExtensionID extension, string itemString) => bool + { + my ($self, $extension, $itemString) = @_; + + if (defined $items[$extension] && exists $items[$extension]->{$itemString}) + { + if ($items[$extension]->{$itemString}->HasDefinitions()) + { die "Tried to delete an item from SourceDB that still has definitions."; }; + + delete $items[$extension]->{$itemString}; + return 1; + } + else + { return 0; }; + }; + + +# +# Function: HasItem +# +# Returns whether there is an item defined for the passed <ExtensionID> and item string. +# +sub HasItem #(ExtensionID extension, string itemString) => bool + { + my ($self, $extension, $itemString) = @_; + + if (defined $items[$extension]) + { return (exists $items[$extension]->{$itemString}); } + else + { return 0; }; + }; + + +# +# Function: GetAllItemsHashRef +# +# Returns a hashref of all the items defined for an extension. *Do not change the contents.* The keys are the item strings and +# the values are <NaturalDocs::SourceDB::Items> or derived classes. +# +sub GetAllItemsHashRef #(ExtensionID extension) => hashref + { + my ($self, $extension) = @_; + return $items[$extension]; + }; + + + +############################################################################### +# Group: Definition Functions + + +# +# Function: AddDefinition +# +# Adds a definition to an item. Assumes the item was already created with <AddItem()>. If there's already a definition for this +# file in the item, the new definition will be ignored. +# +# Parameters: +# +# extension - The <ExtensionID>. +# itemString - The item string. +# file - The <FileName> the definition is in. +# definition - If you're using a custom <NaturalDocs::SourceDB::ItemDefinition> class, you must include an object for it here. +# Otherwise this parameter is ignored. +# +# Returns: +# +# Whether the definition was added, which is to say, whether this was the first definition for the passed <FileName>. +# +sub AddDefinition #(ExtensionID extension, string itemString, FileName file, optional NaturalDocs::SourceDB::ItemDefinition definition) => bool + { + my ($self, $extension, $itemString, $file, $definition) = @_; + + + # Items + + my $item = $self->GetItem($extension, $itemString); + + if (!defined $item) + { die "Tried to add a definition to an undefined item in SourceDB."; }; + + if (!$extensionUsesDefinitionObjects[$extension]) + { $definition = 1; }; + + my $result = $item->AddDefinition($file, $definition); + + + # Files + + if (!exists $files{$file}) + { $files{$file} = NaturalDocs::SourceDB::File->New(); }; + + $files{$file}->AddItem($extension, $itemString); + + + # Watched File + + if ($self->WatchingFileForChanges()) + { + $watchedFile->AddItem($extension, $itemString); + + if ($extensionUsesDefinitionObjects[$extension]) + { $watchedFileDefinitions->AddDefinition($extension, $itemString, $definition); }; + }; + + + return $result; + }; + + +# +# Function: ChangeDefinition +# +# Changes the definition of an item. This function is only used for extensions that use custom +# <NaturalDocs::SourceDB::ItemDefinition>-derived classes. +# +# Parameters: +# +# extension - The <ExtensionID>. +# itemString - The item string. +# file - The <FileName> the definition is in. +# definition - The definition, which must be an object derived from <NaturalDocs::SourceDB::ItemDefinition>. +# +sub ChangeDefinition #(ExtensionID extension, string itemString, FileName file, NaturalDocs::SourceDB::ItemDefinition definition) + { + my ($self, $extension, $itemString, $file, $definition) = @_; + + my $item = $self->GetItem($extension, $itemString); + + if (!defined $item) + { die "Tried to change the definition of an undefined item in SourceDB."; }; + + if (!$extensionUsesDefinitionObjects[$extension]) + { die "Tried to change the definition of an item in an extension that doesn't use definition objects in SourceDB."; }; + + if (!$item->HasDefinition($file)) + { die "Tried to change a definition that doesn't exist in SourceDB."; }; + + $item->ChangeDefinition($file, $definition); + $extensions[$extension]->OnChangedDefinition($itemString, $file); + }; + + +# +# Function: GetDefinition +# +# If the extension uses custom <NaturalDocs::SourceDB::ItemDefinition> classes, returns it for the passed definition or undef +# if it doesn't exist. Otherwise returns whether it exists. +# +sub GetDefinition #(ExtensionID extension, string itemString, FileName file) => NaturalDocs::SourceDB::ItemDefinition or bool + { + my ($self, $extension, $itemString, $file) = @_; + + my $item = $self->GetItem($extension, $itemString); + + if (!defined $item) + { return undef; }; + + return $item->GetDefinition($file); + }; + + +# +# Function: DeleteDefinition +# +# Removes the definition for the passed item. Returns whether it was successful, meaning whether a definition existed for that +# file. +# +sub DeleteDefinition #(ExtensionID extension, string itemString, FileName file) => bool + { + my ($self, $extension, $itemString, $file) = @_; + + my $item = $self->GetItem($extension, $itemString); + + if (!defined $item) + { return 0; }; + + my $result = $item->DeleteDefinition($file); + + if ($result) + { + $files{$file}->DeleteItem($extension, $itemString); + $extensions[$extension]->OnDeletedDefinition($itemString, $file, !$item->HasDefinitions()); + }; + + return $result; + }; + + +# +# Function: HasDefinitions +# +# Returns whether there are any definitions for this item. +# +sub HasDefinitions #(ExtensionID extension, string itemString) => bool + { + my ($self, $extension, $itemString) = @_; + + my $item = $self->GetItem($extension, $itemString); + + if (!defined $item) + { return 0; }; + + return $item->HasDefinitions(); + }; + + +# +# Function: HasDefinition +# +# Returns whether there is a definition for the passed <FileName>. +# +sub HasDefinition #(ExtensionID extension, string itemString, FileName file) => bool + { + my ($self, $extension, $itemString, $file) = @_; + + my $item = $self->GetItem($extension, $itemString); + + if (!defined $item) + { return 0; }; + + return $item->HasDefinition($file); + }; + + + +############################################################################### +# Group: Watched File Functions + + +# +# Function: WatchFileForChanges +# +# Begins watching a file for changes. Only one file at a time can be watched. +# +# This should be called before a file is parsed so the file info goes both into the main database and the watched file info. +# Afterwards you call <AnalyzeWatchedFileChanges()> so item deletions and definition changes can be detected. +# +# Parameters: +# +# filename - The <FileName> to watch. +# +sub WatchFileForChanges #(FileName filename) + { + my ($self, $filename) = @_; + + $watchedFileName = $filename; + $watchedFile = NaturalDocs::SourceDB::File->New(); + $watchedFileDefinitions = NaturalDocs::SourceDB::WatchedFileDefinitions->New(); + }; + + +# +# Function: WatchingFileForChanges +# +# Returns whether we're currently watching a file for changes or not. +# +sub WatchingFileForChanges # => bool + { + my $self = shift; + return defined $watchedFileName; + }; + + +# +# Function: AnalyzeWatchedFileChanges +# +# Analyzes the watched file for changes. Will delete and change definitions as necessary. +# +sub AnalyzeWatchedFileChanges + { + my $self = shift; + + if (!$self->WatchingFileForChanges()) + { die "Tried to analyze watched file for changes in SourceDB when no file was being watched."; }; + if (!$files{$watchedFileName}) + { return; }; + + + # Process extensions last registered to first. + + for (my $extension = scalar @extensions - 1; $extension >= 0; $extension--) + { + my @items = $files{$watchedFileName}->ListItems($extension); + + foreach my $item (@items) + { + if ($watchedFile->HasItem($extension, $item)) + { + if ($extensionUsesDefinitionObjects[$extension]) + { + my $originalDefinition = $items[$extension]->GetDefinition($watchedFileName); + my $watchedDefinition = $watchedFileDefinitions->GetDefinition($extension, $item); + + if (!$originalDefinition->Compare($watchedDefinition)) + { $self->ChangeDefinition($extension, $item, $watchedFileName, $watchedDefinition); }; + } + } + else # !$watchedFile->HasItem($item) + { + $self->DeleteDefinition($extension, $item, $watchedFileName); + }; + }; + }; + + + $watchedFile = undef; + $watchedFileName = undef; + $watchedFileDefinitions = undef; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/SourceDB/Extension.pm b/docs/tool/Modules/NaturalDocs/SourceDB/Extension.pm new file mode 100644 index 00000000..c247ea02 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/SourceDB/Extension.pm @@ -0,0 +1,84 @@ +############################################################################### +# +# Package: NaturalDocs::SourceDB::Extension +# +############################################################################### +# +# A base package for all <SourceDB> extensions. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + + +package NaturalDocs::SourceDB::Extension; + + +############################################################################### +# Group: Interface Functions +# These functions must be overridden by the derived class. + + +# +# Function: Register +# +# Override this function to register the package with <NaturalDocs::SourceDB->RegisterExtension()>. +# +sub Register + { + die "Called SourceDB::Extension->Register(). This function should be overridden by every extension."; + }; + + +# +# Function: Load +# +# Called by <NaturalDocs::SourceDB->Load()> to load the extension's data. Returns whether it was successful. +# +# *This function might not be called.* If there's a situation that would cause all the source files to be reparsed anyway, +# <NaturalDocs::SourceDB> may skip calling Load() for the remaining extensions. You should *not* depend on this function +# for any critical initialization that needs to happen every time regardless. +# +sub Load # => bool + { + return 1; + }; + + +# +# Function: Save +# +# Called by <NaturalDocs::SourceDB->Save()> to save the extension's data. +# +sub Save + { + }; + + +# +# Function: OnDeletedDefinition +# +# Called for each definition deleted by <NaturalDocs::SourceDB>. This is called *after* the definition has been deleted from +# the database, so don't expect to be able to read it. +# +sub OnDeletedDefinition #(string itemString, FileName file, bool wasLastDefinition) + { + }; + + +# +# Function: OnChangedDefinition +# +# Called for each definition changed by <NaturalDocs::SourceDB>. This is called *after* the definition has been changed, so +# don't expect to be able to read the original value. +# +sub OnChangedDefinition #(string itemString, FileName file) + { + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/SourceDB/File.pm b/docs/tool/Modules/NaturalDocs/SourceDB/File.pm new file mode 100644 index 00000000..c364c948 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/SourceDB/File.pm @@ -0,0 +1,129 @@ +############################################################################### +# +# Package: NaturalDocs::SourceDB::File +# +############################################################################### +# +# A class used to index items by file. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + + +package NaturalDocs::SourceDB::File; + +use NaturalDocs::DefineMembers 'ITEMS'; + + +# +# Variables: Members +# +# These constants serve as indexes into the object array. +# +# ITEMS - An arrayref where an <ExtensionID> is the index and the members are existence hashrefs of the item strigs defined +# in this file. The arrayref will always exist, but the hashrefs may be undef. +# + + +# +# Function: New +# +# Returns a new object. +# +sub New + { + my $package = shift; + + my $object = [ ]; + $object->[ITEMS] = [ ]; + + bless $object, $package; + return $object; + }; + + +# +# Function: AddItem +# +# Adds an item to this file. Returns whether this added a new item. +# +sub AddItem #(ExtensionID extension, string itemString) => bool + { + my ($self, $extension, $itemString) = @_; + + if (!defined $self->[ITEMS]->[$extension]) + { + $self->[ITEMS]->[$extension] = { $itemString => 1 }; + return 1; + } + elsif (!exists $self->[ITEMS]->[$extension]->{$itemString}) + { + $self->[ITEMS]->[$extension]->{$itemString} = 1; + return 1; + } + else + { + return 0; + }; + }; + + +# +# Function: HasItem +# +# Returns whether the item exists in this file. +# +sub HasItem #(ExtensionID extension, string itemString) => bool + { + my ($self, $extension, $itemString) = @_; + + if (defined $self->[ITEMS]->[$extension]) + { return exists $self->[ITEMS]->[$extension]->{$itemString}; } + else + { return 0; }; + }; + + +# +# Function: DeleteItem +# +# Deletes the passed item. Returns whether it existed. +# +sub DeleteItem #(ExtensionID extension, string itemString) => bool + { + my ($self, $extension, $itemString) = @_; + + if (!defined $self->[ITEMS]->[$extension]) + { return 0; } + elsif (exists $self->[ITEMS]->[$extension]->{$itemString}) + { + delete $self->[ITEMS]->[$extension]->{$itemString}; + return 1; + } + else + { return 0; }; + }; + + +# +# Function: ListItems +# +# Returns an array of all the item strings defined for a particular extension, or an empty list if none. +# +sub ListItems #(ExtensionID extension) => string array + { + my ($self, $extension) = @_; + + if (defined $self->[ITEMS]->[$extension]) + { return keys %{$self->[ITEMS]->[$extension]}; } + else + { return ( ); }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/SourceDB/Item.pm b/docs/tool/Modules/NaturalDocs/SourceDB/Item.pm new file mode 100644 index 00000000..6654465c --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/SourceDB/Item.pm @@ -0,0 +1,201 @@ +############################################################################### +# +# Package: NaturalDocs::SourceDB::Item +# +############################################################################### +# +# A base class for something being tracked in <NaturalDocs::SourceDB>. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + + +package NaturalDocs::SourceDB::Item; + +use NaturalDocs::DefineMembers 'DEFINITIONS'; + + +# +# Variables: Members +# +# The following constants are indexes into the object array. +# +# DEFINITIONS - A hashref that maps <FileNames> to either <NaturalDocs::SourceDB::ItemDefinition>-derived objects or +# serves as an existence hashref depending on whether the extension only tracks existence. Will be undef if +# there are none. +# + + +# +# Function: New +# +# Creates and returns a new object. +# +sub New + { + my $class = shift; + + my $object = [ ]; + bless $object, $class; + + return $object; + }; + + + +############################################################################### +# +# Group: Definition Functions +# +# These functions should be called by <NaturalDocs::SourceDB>. You should not be calling them directly. Call functions +# like <NaturalDocs::SourceDB->AddDefinition()> instead. +# + + +# +# Function: AddDefinition +# +# Adds a definition for the passed <FileName>. If it's already defined, the new definition will be ignored. +# +# Parameters: +# +# file - The <FileName>. +# definition - The definition, which must be an object derived from <NaturalDocs::SourceDB::ItemDefinition> or undef if +# the extension only tracks existence. +# +# Returns: +# +# Whether the definition was added, which is to say, whether this was the first definition for the passed <FileName>. +# +sub AddDefinition #(FileName file, optional NaturalDocs::SourceDB::ItemDefinition definition) => bool + { + my ($self, $file, $definition) = @_; + + if (!defined $self->[DEFINITIONS]) + { $self->[DEFINITIONS] = { }; }; + + if (!exists $self->[DEFINITIONS]->{$file}) + { + if (!defined $definition) + { $definition = 1; }; + + $self->[DEFINITIONS]->{$file} = $definition; + return 1; + } + else + { return 0; }; + }; + + +# +# Function: ChangeDefinition +# +# Changes the definition for the passed <FileName>. +# +# Parameters: +# +# file - The <FileName>. +# definition - The definition, which must be an object derived from <NaturalDocs::SourceDB::ItemDefinition>. +# +sub ChangeDefinition #(FileName file, NaturalDocs::SourceDB::ItemDefinition definition) + { + my ($self, $file, $definition) = @_; + + if (!defined $self->[DEFINITIONS] || !exists $self->[DEFINITIONS]->{$file}) + { die "Tried to change a non-existant definition in SourceD::Item."; }; + + $self->[DEFINITIONS]->{$file} = $definition; + }; + + +# +# Function: GetDefinition +# +# Returns the <NaturalDocs::SourceDB::ItemDefinition>-derived object for the passed <FileName>, non-zero if it only tracks +# existence, or undef if there is no definition. +# +sub GetDefinition #(FileName file) => NaturalDocs::SourceDB::ItemDefinition or bool + { + my ($self, $file) = @_; + + if (defined $self->[DEFINITIONS]) + { return $self->[DEFINITIONS]->{$file}; } + else + { return undef; }; + }; + + +# +# Function: DeleteDefinition +# +# Removes the definition for the passed <FileName>. Returns whether it was successful, meaning whether a definition existed +# for that file. +# +sub DeleteDefinition #(FileName file) => bool + { + my ($self, $file) = @_; + + if (defined $self->[DEFINITIONS]) + { + if (exists $self->[DEFINITIONS]->{$file}) + { + delete $self->[DEFINITIONS]->{$file}; + + if (!scalar keys %{$self->[DEFINITIONS]}) + { $self->[DEFINITIONS] = undef; }; + + return 1; + }; + }; + + return 0; + }; + + +# +# Function: HasDefinitions +# +# Returns whether there are any definitions for this item. +# +sub HasDefinitions # => bool + { + my $self = shift; + return (defined $self->[DEFINITIONS]); + }; + + +# +# Function: HasDefinition +# +# Returns whether there is a definition for the passed <FileName>. +# +sub HasDefinition #(FileName file) => bool + { + my ($self, $file) = @_; + + if (defined $self->[DEFINITIONS]) + { return (exists $self->[DEFINITIONS]->{$file}); } + else + { return 0; }; + }; + + +# +# Function: GetAllDefinitionsHashRef +# +# Returns a hashref of all the definitions of this item. *Do not change.* The keys are the <FileNames>, and the values are +# either <NaturalDocs::SourceDB::ItemDefinition>-derived objects or it's just an existence hashref if those aren't used. +# +sub GetAllDefinitionsHashRef + { + my $self = shift; + return $self->[DEFINITIONS]; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/SourceDB/ItemDefinition.pm b/docs/tool/Modules/NaturalDocs/SourceDB/ItemDefinition.pm new file mode 100644 index 00000000..ee053fef --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/SourceDB/ItemDefinition.pm @@ -0,0 +1,45 @@ +############################################################################### +# +# Package: NaturalDocs::SourceDB::ItemDefinition +# +############################################################################### +# +# A base class for all item definitions for extensions that track more than existence. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + + +package NaturalDocs::SourceDB::ItemDefinition; + + +# +# Function: Compare +# +# Returns whether the definitions are equal. This version returns true by default, you must override it in your subclasses +# to make the results relevant. This is important for <NaturalDocs::SourceDB->AnalyzeTrackedFileChanges()>. +# +# This will only be called between objects of the same <ExtensionID>. If you use multiple derived classes for the same +# <ExtensionID>, you will have to take that into account yourself. +# +# Parameters: +# +# other - Another <NaturalDocs::SourceDB::ItemDefinition>-derived object to compare this one to. It will always be from +# the same <ExtensionID>. +# +# Returns: +# +# Whether they are equal. +# +sub Compare #(other) + { + return 1; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/SourceDB/WatchedFileDefinitions.pm b/docs/tool/Modules/NaturalDocs/SourceDB/WatchedFileDefinitions.pm new file mode 100644 index 00000000..956c6644 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/SourceDB/WatchedFileDefinitions.pm @@ -0,0 +1,159 @@ +############################################################################### +# +# Package: NaturalDocs::SourceDB::WatchedFileDefinitions +# +############################################################################### +# +# A class to track the definitions appearing in a watched file. This is only used for extensions that track definition info with +# <NaturalDocs::SourceDB::ItemDefinition>-derived objects. Do not use it for extensions that only track existence. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + + +package NaturalDocs::SourceDB::WatchedFileDefinitions; + + +# +# Variables: Members +# +# This object would only have one member, which is an array, so the object itself serves as that member. +# +# <ExtensionIDs> are used as indexes into this object. Each entry is a hashref that maps item strings to +# <NaturalDocs::SourceDB::ItemDefinition>-derived objects. This is only done for extensions that use those objects to track +# definitions, it's not needed for extensions that only track existence. If there are no definitions, the entry will be undef. +# + + +# +# Function: New +# +# Creates and returns a new object. +# +sub New + { + my $class = shift; + + my $object = [ ]; + bless $object, $class; + + return $object; + }; + + + +############################################################################### +# Group: Definition Functions +# + + +# +# Function: AddDefinition +# +# Adds a definition for the passed item string. If it's already defined, the new definition will be ignored. +# +# Parameters: +# +# extension - The <ExtensionID>. +# itemString - The item string. +# definition - The definition, which must be an object derived from <NaturalDocs::SourceDB::ItemDefinition>. +# +# Returns: +# +# Whether the definition was added, which is to say, whether this was the first definition for the passed <FileName>. +# +sub AddDefinition #(ExtensionID extension, string itemString, NaturalDocs::SourceDB::ItemDefinition definition) => bool + { + my ($self, $extension, $itemString, $definition) = @_; + + if (!defined $self->[$extension]) + { $self->[$extension] = { }; }; + + if (!exists $self->[$extension]->{$itemString}) + { + $self->[$extension]->{$itemString} = $definition; + return 1; + } + else + { return 0; }; + }; + + +# +# Function: GetDefinition +# +# Returns the <NaturalDocs::SourceDB::ItemDefinition>-derived object for the passed item string or undef if there is none. +# +sub GetDefinition #(ExtensionID extension, string itemString) => NaturalDocs::SourceDB::ItemDefinition + { + my ($self, $extension, $itemString) = @_; + + if (defined $self->[$extension]) + { return $self->[$extension]->{$itemString}; } + else + { return undef; }; + }; + + +# +# Function: DeleteDefinition +# +# Removes the definition for the passed item string. Returns whether it was successful, meaning whether a definition existed +# for that item. +# +sub DeleteDefinition #(ExtensionID extension, string itemString) => bool + { + my ($self, $extension, $itemString) = @_; + + if (defined $self->[$extension]) + { + if (exists $self->[$extension]->{$itemString}) + { + delete $self->[$extension]->{$itemString}; + + if (!scalar keys %{$self->[$extension]}) + { $self->[$extension] = undef; }; + + return 1; + }; + }; + + return 0; + }; + + +# +# Function: HasDefinitions +# +# Returns whether there are any definitions for this item. +# +sub HasDefinitions #(ExtensionID extension) => bool + { + my ($self, $extension) = @_; + + return (defined $self->[$extension]); + }; + + +# +# Function: HasDefinition +# +# Returns whether there is a definition for the passed item string. +# +sub HasDefinition #(ExtensionID extension, string itemString) => bool + { + my ($self, $extension, $itemString) = @_; + + if (defined $self->[$extension]) + { return (exists $self->[$extension]->{$itemString}); } + else + { return 0; }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/StatusMessage.pm b/docs/tool/Modules/NaturalDocs/StatusMessage.pm new file mode 100644 index 00000000..7d3de079 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/StatusMessage.pm @@ -0,0 +1,102 @@ +############################################################################### +# +# Package: NaturalDocs::StatusMessage +# +############################################################################### +# +# A package to handle status message updates. Automatically handles <NaturalDocs::Settings->IsQuiet()>. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::StatusMessage; + + +# +# var: message +# The message to display. +# +my $message; + +# +# var: total +# The number of items to work through. +# +my $total; + +# +# var: completed +# The number of items completed. +# +my $completed; + +# +# var: lastMessageTime +# The time the last message was posted. +# +my $lastMessageTime; + + +# +# constant: TIME_BETWEEN_UPDATES +# The number of seconds that should occur between updates. +# +use constant TIME_BETWEEN_UPDATES => 10; + + + +# +# Function: Start +# +# Starts the status message. +# +# Parameters: +# +# message - The message to post. +# total - The number of items that are going to be worked through. +# +sub Start #(message, total) + { + my $self = shift; + + if (!NaturalDocs::Settings->IsQuiet()) + { + ($message, $total) = @_; + $completed = 0; + + print $message . "\n"; + + $lastMessageTime = time(); + }; + }; + + +# +# Function: CompletedItem +# +# Should be called every time an item is completed. +# +sub CompletedItem + { + my $self = shift; + + if (!NaturalDocs::Settings->IsQuiet()) + { + # We scale completed by 100 since we need to anyway to get the percentage. + + $completed += 100; + + if (time() >= $lastMessageTime + TIME_BETWEEN_UPDATES && $completed != $total * 100) + { + print $message . ' (' . ($completed / $total) . '%)' . "\n"; + $lastMessageTime = time(); + }; + }; + }; + +1; diff --git a/docs/tool/Modules/NaturalDocs/SymbolString.pm b/docs/tool/Modules/NaturalDocs/SymbolString.pm new file mode 100644 index 00000000..c3b85e64 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/SymbolString.pm @@ -0,0 +1,212 @@ +############################################################################### +# +# Package: NaturalDocs::SymbolString +# +############################################################################### +# +# A package to manage <SymbolString> handling throughout the program. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::SymbolString; + + +# +# Function: FromText +# +# Extracts and returns a <SymbolString> from plain text. +# +# This should be the only way to get a <SymbolString> from plain text, as the splitting and normalization must be consistent +# throughout the application. +# +sub FromText #(string textSymbol) + { + my ($self, $textSymbol) = @_; + + # The internal format of a symbol is all the normalized identifiers separated by 0x1F characters. + + # Convert whitespace and reserved characters to spaces, and condense multiple consecutive ones. + $textSymbol =~ tr/ \t\r\n\x1C\x1D\x1E\x1F/ /s; + + # DEPENDENCY: ReferenceString->MakeFrom() assumes all 0x1E characters were removed. + # DEPENDENCY: ReferenceString->MakeFrom() assumes this encoding doesn't use 0x1E characters. + + # Remove spaces unless they're separating two alphanumeric/underscore characters. + $textSymbol =~ s/^ //; + $textSymbol =~ s/ $//; + $textSymbol =~ s/(\W) /$1/g; + $textSymbol =~ s/ (\W)/$1/g; + + # Remove trailing empty parenthesis, so Function and Function() are equivalent. + $textSymbol =~ s/\(\)$//; + + # Split the string into pieces. + my @pieces = split(/(\.|::|->)/, $textSymbol); + my $symbolString; + + my $lastWasSeparator = 1; + + foreach my $piece (@pieces) + { + if ($piece =~ /^(?:\.|::|->)$/) + { + if (!$lastWasSeparator) + { + $symbolString .= "\x1F"; + $lastWasSeparator = 1; + }; + } + elsif (length $piece) + { + $symbolString .= $piece; + $lastWasSeparator = 0; + }; + # Ignore empty pieces + }; + + $symbolString =~ s/\x1F$//; + + return $symbolString; + }; + + +# +# Function: ToText +# +# Converts a <SymbolString> to text, using the passed separator. +# +sub ToText #(SymbolString symbolString, string separator) + { + my ($self, $symbolString, $separator) = @_; + + my @identifiers = $self->IdentifiersOf($symbolString); + return join($separator, @identifiers); + }; + + +# +# Function: ToBinaryFile +# +# Writes a <SymbolString> to the passed filehandle. Can also encode an undef. +# +# Parameters: +# +# fileHandle - The filehandle to write to. +# symbol - The <SymbolString> to write, or undef. +# +# Format: +# +# > [UInt8: number of identifiers] +# > [AString16: identifier] [AString16: identifier] ... +# +# Undef is represented by a zero for the number of identifiers. +# +sub ToBinaryFile #(FileHandle fileHandle, SymbolString symbol) + { + my ($self, $fileHandle, $symbol) = @_; + + my @identifiers; + if (defined $symbol) + { @identifiers = $self->IdentifiersOf($symbol); }; + + print $fileHandle pack('C', scalar @identifiers); + + foreach my $identifier (@identifiers) + { + print $fileHandle pack('nA*', length($identifier), $identifier); + }; + }; + + +# +# Function: FromBinaryFile +# +# Loads a <SymbolString> or undef from the filehandle and returns it. +# +# Parameters: +# +# fileHandle - The filehandle to read from. +# +# Returns: +# +# The <SymbolString> or undef. +# +# See also: +# +# See <ToBinaryFile()> for format and dependencies. +# +sub FromBinaryFile #(FileHandle fileHandle) + { + my ($self, $fileHandle) = @_; + + my $raw; + + # [UInt8: number of identifiers or 0 if none] + + read($fileHandle, $raw, 1); + my $identifierCount = unpack('C', $raw); + + my @identifiers; + + while ($identifierCount) + { + # [AString16: identifier] [AString16: identifier] ... + + read($fileHandle, $raw, 2); + my $identifierLength = unpack('n', $raw); + + my $identifier; + read($fileHandle, $identifier, $identifierLength); + + push @identifiers, $identifier; + + $identifierCount--; + }; + + if (scalar @identifiers) + { return $self->Join(@identifiers); } + else + { return undef; }; + }; + + +# +# Function: IdentifiersOf +# +# Returns the <SymbolString> as an array of identifiers. +# +sub IdentifiersOf #(SymbolString symbol) + { + my ($self, $symbol) = @_; + return split(/\x1F/, $symbol); + }; + + +# +# Function: Join +# +# Takes a list of identifiers and/or <SymbolStrings> and returns it as a new <SymbolString>. +# +sub Join #(string/SymbolString identifier/symbol, string/SymolString identifier/symbol, ...) + { + my ($self, @pieces) = @_; + + # Can't have undefs screwing everything up. + while (scalar @pieces && !defined $pieces[0]) + { shift @pieces; }; + + # We need to test @pieces first because joining on an empty array returns an empty string rather than undef. + if (scalar @pieces) + { return join("\x1F", @pieces); } + else + { return undef; }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/SymbolTable.pm b/docs/tool/Modules/NaturalDocs/SymbolTable.pm new file mode 100644 index 00000000..02f010f3 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/SymbolTable.pm @@ -0,0 +1,1984 @@ +############################################################################### +# +# Package: NaturalDocs::SymbolTable +# +############################################################################### +# +# A package that handles all the gory details of managing symbols. It handles where they are defined, which files +# reference them, if any are undefined or duplicated, and loading and saving them to a file. +# +# Usage and Dependencies: +# +# - At any time, <RebuildAllIndexes()> can be called. +# +# - <NaturalDocs::Settings>, <NaturalDocs::Languages>, and <NaturalDocs::Project> must be initialized before use. +# +# - <Load()> must be called to initialize the package. At this point, the <Information Functions> will return the symbol +# table as of the last time Natural Docs was run. +# +# - Note that <Load()> and <Save()> only manage <REFERENCE_TEXT> references. All other reference types must be +# managed by their respective classes. They should be readded after <Load()> to recreate the state of the last time +# Natural Docs was run. +# +# - <Purge()> must be called, and then <NaturalDocs::Parser->ParseForInformation()> on all files that have changed so it +# can fully resolve the symbol table via the <Modification Functions>. Afterwards <PurgeResolvingInfo()> can be called +# to reclaim some memory, and the symbol table will reflect the current state of the code. +# +# - <Save()> must be called to commit any changes to the symbol table back to disk. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + + +use NaturalDocs::SymbolTable::Symbol; +use NaturalDocs::SymbolTable::SymbolDefinition; +use NaturalDocs::SymbolTable::Reference; +use NaturalDocs::SymbolTable::File; +use NaturalDocs::SymbolTable::ReferenceTarget; +use NaturalDocs::SymbolTable::IndexElement; + +use strict; +use integer; + +package NaturalDocs::SymbolTable; + + + +############################################################################### +# Group: Variables + +# +# handle: SYMBOLTABLE_FILEHANDLE +# +# The file handle used with <SymbolTable.nd>. +# + +# +# hash: symbols +# +# A hash of all <SymbolStrings>. The keys are the <SymbolStrings> and the values are <NaturalDocs::SymbolTable::Symbol> +# objects. +# +# Prior to <PurgeResolvingInfo()>, both defined symbols and symbols that are merely potential interpretations of references +# will be here. Afterwards, only defined symbols will be here. +# +my %symbols; + +# +# hash: references +# +# A hash of all references in the project. The keys are <ReferenceStrings> and the values are +# <NaturalDocs::SymbolTable::Reference> objects. +# +# Prior to <PurgeResolvingInfo()>, all possible interpretations will be stored for each reference. Afterwards, only the current +# interpretation will be. +# +my %references; + +# +# hash: files +# +# A hash of all the files that define symbols and references in the project. The keys are the <FileNames>, and the values are +# <NaturalDocs::SymbolTable::File> objects. +# +# After <PurgeResolvingInfo()>, this hash will be empty. +# +my %files; + +# +# object: watchedFile +# +# A <NaturalDocs::SymbolTable::File> object of the file being watched for changes. This is compared to the version in <files> +# to see if anything was changed since the last parse. +# +my $watchedFile; + +# +# string: watchedFileName +# +# The <FileName> of the watched file, if any. If there is no watched file, this will be undef. +# +my $watchedFileName; + +# +# hash: watchedFileSymbolDefinitions +# +# A hashref of the symbol definition information for all the <SymbolStrings> in the watched file. The keys are the symbol strings, +# and the values are <NaturalDocs::SymbolTable::SymbolDefinition> objects. +# +my %watchedFileSymbolDefinitions; + + +# +# hash: indexes +# +# A hash of generated symbol indexes. The keys are <TopicTypes> and the values are sorted arrayrefs of +# <NaturalDocs::SymbolTable::IndexElements>, or undef if its empty. +# +my %indexes; + + +# +# hash: indexChanges +# +# A hash of all the indexes that have changed. The keys are the <TopicTypes> and the entries are undef if they have not +# changed, or 1 if they have. The key will not exist if the <TopicType> has not been checked. +# +my %indexChanges; + + +# +# hash: indexSectionsWithContent +# +# A hash of which sections in an index have content. The keys are the <TopicTypes> of each index, and the values are +# arrayrefs of bools where the first represents symbols, the second numbers, and the rest A-Z. If there is no information +# available for an index, it's entry will not exist here. +# +my %indexSectionsWithContent; + + +# +# bool: rebuildIndexes +# +# Whether all indexes should be rebuilt regardless of whether they have been changed. +# +my $rebuildIndexes; + + + +############################################################################### +# Group: Files + + +# +# File: SymbolTable.nd +# +# The storage file for the symbol table. +# +# Format: +# +# > [BINARY_FORMAT] +# > [VersionInt: app version] +# +# The file starts with the standard <BINARY_FORMAT> <VersionInt> header. +# +# The first stage of the file is for symbol definitions, analogous to <symbols>. +# +# > [SymbolString: symbol or undef to end] ... +# > +# > [UInt16: number of definitions] +# > +# > [AString16: global definition file] [AString16: TopicType] +# > [AString16: prototype] [AString16: summary] +# > +# > [AString16: definition file] ... +# > +# > ... +# +# These blocks continue until the <SymbolString> is undef. Only defined symbols will be included in this file, so +# number of definitions will never be zero. The first one is always the global definition. If a symbol does not have a +# prototype or summary, the UInt16 length of the string will be zero. +# +# The second stage is for references, which is analogous to <references>. Only <REFERENCE_TEXT> references are +# stored in this file, and their <Resolving Flags> are implied so they aren't stored either. +# +# > [ReferenceString (no type, resolving flags): reference or undef to end] +# > +# > [UInt8: number of definition files] +# > [AString16: definition file] [AString16: definition file] ... +# +# These blocks continue until the <ReferenceString> is undef. Since there can be multiple using <SymbolStrings>, those +# continue until the number of identifiers is zero. Note that all interpretations are rebuilt rather than stored. +# +# See Also: +# +# <File Format Conventions> +# +# Revisions: +# +# 1.3: +# +# - Symbol <TopicTypes> were changed from UInt8s to AString16s, now that <TopicTypes> are strings instead of +# integer constants. +# +# 1.22: +# +# - File format was completely rebuilt to accommodate the new symbol format and to be in binary. To see the plain text +# format prior to 1.22, check out 1.21's version of this file from CVS. It is too big a change to note here. +# + + +# +# File: IndexInfo.nd +# +# The storage file for information about the indexes. +# +# Format: +# +# > [Standard Header] +# +# The standard binary file header. +# +# > [AString16: index topic name] +# > [uint8: symbols have content (0 or 1)] +# > [uint8: numbers have content (0 or 1)] +# > [uint8: A has content] [uint8: B has content] ... +# > ... +# +# Every index that has information about it is stored with the topic type name first, then 28 uint8s that say whether that +# part of the index has content or not. The first is for symbols, the second is for numbers, and the rest are for A-Z. If an +# index's state is unknown, it won't appear in this file. +# +# Revisions: +# +# 1.4: +# +# - The file is introduced. +# + + + +############################################################################### +# Group: File Functions + + +# +# Function: Load +# +# Loads all data files from disk. +# +sub Load + { + my ($self) = @_; + + $self->LoadSymbolTable(); + $self->LoadIndexInfo(); + }; + + +# +# Function: LoadSymbolTable +# +# Loads <SymbolTable.nd> from disk. +# +sub LoadSymbolTable + { + my ($self) = @_; + + my $fileIsOkay; + + if (!NaturalDocs::Settings->RebuildData() && + open(SYMBOLTABLE_FILEHANDLE, '<' . NaturalDocs::Project->DataFile('SymbolTable.nd')) ) + { + # See if it's binary. + binmode(SYMBOLTABLE_FILEHANDLE); + + my $firstChar; + read(SYMBOLTABLE_FILEHANDLE, $firstChar, 1); + + if ($firstChar == ::BINARY_FORMAT()) + { + my $version = NaturalDocs::Version->FromBinaryFile(\*SYMBOLTABLE_FILEHANDLE); + + # 1.3 is incompatible with previous versions. + + if (NaturalDocs::Version->CheckFileFormat( $version, NaturalDocs::Version->FromString('1.3') )) + { $fileIsOkay = 1; } + else + { close(SYMBOLTABLE_FILEHANDLE); }; + } + + else + { close(SYMBOLTABLE_FILEHANDLE); }; + }; + + + if (!$fileIsOkay) + { + NaturalDocs::Project->ReparseEverything(); + return; + } + + my $raw; + + + # Symbols + + for (;;) + { + # [SymbolString: symbol or undef to end] + + my $symbol = NaturalDocs::SymbolString->FromBinaryFile(\*SYMBOLTABLE_FILEHANDLE); + + if (!defined $symbol) + { last; }; + + my $symbolObject = NaturalDocs::SymbolTable::Symbol->New(); + $symbols{$symbol} = $symbolObject; + + # [UInt16: number of definitions] + + read(SYMBOLTABLE_FILEHANDLE, $raw, 2); + my $definitionCount = unpack('n', $raw); + + do + { + # [AString16: (global?) definition file] + + read(SYMBOLTABLE_FILEHANDLE, $raw, 2); + my $fileLength = unpack('n', $raw); + + my $file; + read(SYMBOLTABLE_FILEHANDLE, $file, $fileLength); + + # [AString16: TopicType] + + read(SYMBOLTABLE_FILEHANDLE, $raw, 2); + my $typeLength = unpack('n', $raw); + + my $type; + read(SYMBOLTABLE_FILEHANDLE, $type, $typeLength); + + # [AString16: prototype] + + read(SYMBOLTABLE_FILEHANDLE, $raw, 2); + my $prototypeLength = unpack('n', $raw); + + my $prototype; + if ($prototypeLength) + { read(SYMBOLTABLE_FILEHANDLE, $prototype, $prototypeLength); }; + + # [AString16: summary] + + read(SYMBOLTABLE_FILEHANDLE, $raw, 2); + my $summaryLength = unpack('n', $raw); + + my $summary; + if ($summaryLength) + { read(SYMBOLTABLE_FILEHANDLE, $summary, $summaryLength); }; + + $symbolObject->AddDefinition($file, $type, $prototype, $summary); + + # Add it. + + if (!exists $files{$file}) + { $files{$file} = NaturalDocs::SymbolTable::File->New(); }; + + $files{$file}->AddSymbol($symbol); + + $definitionCount--; + } + while ($definitionCount); + }; + + + # References + + for (;;) + { + # [ReferenceString (no type, resolving flags): reference or undef to end] + + my $referenceString = NaturalDocs::ReferenceString->FromBinaryFile(\*SYMBOLTABLE_FILEHANDLE, + ::BINARYREF_NOTYPE() | + ::BINARYREF_NORESOLVINGFLAGS(), + ::REFERENCE_TEXT(), undef); + + if (!defined $referenceString) + { last; }; + + my $referenceObject = NaturalDocs::SymbolTable::Reference->New(); + $references{$referenceString} = $referenceObject; + + # [UInt8: number of definition files] + + read(SYMBOLTABLE_FILEHANDLE, $raw, 1); + my $definitionCount = unpack('C', $raw); + do + { + # [AString16: definition file] [AString16: definition file] ... + + read(SYMBOLTABLE_FILEHANDLE, $raw, 2); + my $definitionLength = unpack('n', $raw); + + my $definition; + read(SYMBOLTABLE_FILEHANDLE, $definition, $definitionLength); + + # Add it. + + $referenceObject->AddDefinition($definition); + + if (!exists $files{$definition}) + { $files{$definition} = NaturalDocs::SymbolTable::File->New(); }; + + $files{$definition}->AddReference($referenceString); + + $definitionCount--; + } + while ($definitionCount); + + $self->GenerateInterpretations($referenceString); + $self->InterpretReference($referenceString); + }; + + close(SYMBOLTABLE_FILEHANDLE); + }; + + +# +# Function: LoadIndexInfo +# +# Loads <IndexInfo.nd> from disk. +# +sub LoadIndexInfo + { + my ($self) = @_; + + if (NaturalDocs::Settings->RebuildData()) + { return; }; + + my $version = NaturalDocs::BinaryFile->OpenForReading( NaturalDocs::Project->DataFile('IndexInfo.nd') ); + + if (!defined $version) + { return; } + + # The file format hasn't changed since it was introduced. + if (!NaturalDocs::Version->CheckFileFormat($version)) + { + NaturalDocs::BinaryFile->Close(); + return; + }; + + my $topicTypeName; + while ($topicTypeName = NaturalDocs::BinaryFile->GetAString16()) + { + my $topicType = NaturalDocs::Topics->TypeFromName($topicTypeName); + my $content = [ ]; + + for (my $i = 0; $i < 28; $i++) + { push @$content, NaturalDocs::BinaryFile->GetUInt8(); }; + + if (defined $topicType) # The name in the file could be from a type that was deleted + { $indexSectionsWithContent{$topicType} = $content; }; + }; + + NaturalDocs::BinaryFile->Close(); + }; + + +# +# Function: Purge +# +# Purges the symbol table of all symbols and references from files that no longer have Natural Docs content. +# +sub Purge + { + my ($self) = @_; + + my $filesToPurge = NaturalDocs::Project->FilesToPurge(); + + # We do this in two stages. First we delete all the references, and then we delete all the definitions. This causes us to go + # through the list twice, but it makes sure no purged files get added to the build list. For example, if we deleted all of + # Purge File A's references and definitions, and Purge File B had a reference to one of those symbols, Purge File B + # would be added to the build list because one of its references changed. By removing all the references in all the files + # before removing the definitions, we avoid this. + + foreach my $file (keys %$filesToPurge) + { + if (exists $files{$file}) + { + my @references = $files{$file}->References(); + foreach my $reference (@references) + { $self->DeleteReference($reference, $file); }; + }; + }; + + foreach my $file (keys %$filesToPurge) + { + if (exists $files{$file}) + { + my @symbols = $files{$file}->Symbols(); + foreach my $symbol (@symbols) + { $self->DeleteSymbol($symbol, $file); }; + + delete $files{$file}; + }; + }; + }; + + +# +# Function: Save +# +# Saves all data files to disk. +# +sub Save + { + my ($self) = @_; + + $self->SaveSymbolTable(); + $self->SaveIndexInfo(); + }; + + +# +# Function: SaveSymbolTable +# +# Saves <SymbolTable.nd> to disk. +# +sub SaveSymbolTable + { + my ($self) = @_; + + open (SYMBOLTABLE_FILEHANDLE, '>' . NaturalDocs::Project->DataFile('SymbolTable.nd')) + or die "Couldn't save " . NaturalDocs::Project->DataFile('SymbolTable.nd') . ".\n"; + + binmode(SYMBOLTABLE_FILEHANDLE); + + print SYMBOLTABLE_FILEHANDLE '' . ::BINARY_FORMAT(); + + NaturalDocs::Version->ToBinaryFile(\*SYMBOLTABLE_FILEHANDLE, NaturalDocs::Settings->AppVersion()); + + + # Symbols + + while (my ($symbol, $symbolObject) = each %symbols) + { + # Only existing symbols. + if ($symbolObject->IsDefined()) + { + # [SymbolString: symbol or undef to end] + + NaturalDocs::SymbolString->ToBinaryFile(\*SYMBOLTABLE_FILEHANDLE, $symbol); + + # [UInt16: number of definitions] + + my @definitions = $symbolObject->Definitions(); + print SYMBOLTABLE_FILEHANDLE pack('n', scalar @definitions); + + # [AString16: global definition file] [AString16: TopicType] + + print SYMBOLTABLE_FILEHANDLE pack('nA*nA*', length $symbolObject->GlobalDefinition(), + $symbolObject->GlobalDefinition(), + length $symbolObject->GlobalType(), + $symbolObject->GlobalType()); + + # [AString16: prototype] + + my $prototype = $symbolObject->GlobalPrototype(); + + if (defined $prototype) + { print SYMBOLTABLE_FILEHANDLE pack('nA*', length($prototype), $prototype); } + else + { print SYMBOLTABLE_FILEHANDLE pack('n', 0); }; + + # [AString16: summary] + + my $summary = $symbolObject->GlobalSummary(); + + if (defined $summary) + { print SYMBOLTABLE_FILEHANDLE pack('nA*', length($summary), $summary); } + else + { print SYMBOLTABLE_FILEHANDLE pack('n', 0); }; + + + foreach my $definition (@definitions) + { + if ($definition ne $symbolObject->GlobalDefinition()) + { + # [AString16: definition file] [AString16: TopicType] + + print SYMBOLTABLE_FILEHANDLE pack('nA*nA*', length $definition, $definition, + length $symbolObject->TypeDefinedIn($definition), + $symbolObject->TypeDefinedIn($definition)); + + # [AString16: prototype] + + my $prototype = $symbolObject->PrototypeDefinedIn($definition); + + if (defined $prototype) + { print SYMBOLTABLE_FILEHANDLE pack('nA*', length($prototype), $prototype); } + else + { print SYMBOLTABLE_FILEHANDLE pack('n', 0); }; + + # [AString16: summary] + + my $summary = $symbolObject->SummaryDefinedIn($definition); + + if (defined $summary) + { print SYMBOLTABLE_FILEHANDLE pack('nA*', length($summary), $summary); } + else + { print SYMBOLTABLE_FILEHANDLE pack('n', 0); }; + }; + }; + }; + }; + + # [SymbolString: symbol or undef to end] + + NaturalDocs::SymbolString->ToBinaryFile(\*SYMBOLTABLE_FILEHANDLE, undef); + + + # References + + while (my ($reference, $referenceObject) = each %references) + { + my $type = NaturalDocs::ReferenceString->TypeOf($reference); + + if ($type == ::REFERENCE_TEXT()) + { + # [ReferenceString (no type, resolving flags): reference or undef to end] + + NaturalDocs::ReferenceString->ToBinaryFile(\*SYMBOLTABLE_FILEHANDLE, $reference, + ::BINARYREF_NOTYPE() | ::BINARYREF_NORESOLVINGFLAGS()); + + # [UInt8: number of definition files] + + my @definitions = $referenceObject->Definitions(); + print SYMBOLTABLE_FILEHANDLE pack('C', scalar @definitions); + + # [AString16: definition file] [AString16: definition file] ... + + foreach my $definition (@definitions) + { + print SYMBOLTABLE_FILEHANDLE pack('nA*', length($definition), $definition); + }; + }; + }; + + # [ReferenceString (no type, resolving flags): reference or undef to end] + + NaturalDocs::ReferenceString->ToBinaryFile(\*SYMBOLTABLE_FILEHANDLE, undef, + ::BINARYREF_NOTYPE() | ::BINARYREF_NORESOLVINGFLAGS()); + + close(SYMBOLTABLE_FILEHANDLE); + }; + + +# +# Function: SaveIndexInfo +# +# Saves <IndexInfo.nd> to disk. +# +sub SaveIndexInfo + { + my ($self) = @_; + + NaturalDocs::BinaryFile->OpenForWriting( NaturalDocs::Project->DataFile('IndexInfo.nd') ); + + while (my ($topicType, $content) = each %indexSectionsWithContent) + { + NaturalDocs::BinaryFile->WriteAString16( NaturalDocs::Topics->NameOfType($topicType) ); + + for (my $i = 0; $i < 28; $i++) + { + if ($content->[$i]) + { NaturalDocs::BinaryFile->WriteUInt8(1); } + else + { NaturalDocs::BinaryFile->WriteUInt8(0); }; + }; + }; + + NaturalDocs::BinaryFile->Close(); + }; + + + +############################################################################### +# Group: Modification Functions +# These functions should not be called after <PurgeResolvingInfo()>. + +# +# Function: AddSymbol +# +# Adds a symbol definition to the table, if it doesn't already exist. If the definition changes or otherwise requires the files that +# reference it to be updated, the function will call <NaturalDocs::Project->RebuildFile()> to make sure that they are. +# +# Parameters: +# +# symbol - The <SymbolString>. +# file - The <FileName> where it's defined. +# type - The symbol's <TopicType>. +# prototype - The symbol's prototype, if applicable. +# summary - The symbol's summary, if applicable. +# +sub AddSymbol #(symbol, file, type, prototype, summary) + { + my ($self, $symbol, $file, $type, $prototype, $summary) = @_; + + + # If the symbol doesn't exist... + if (!exists $symbols{$symbol}) + { + # Create the symbol. There are no references that could be interpreted as this or else it would have existed already. + + my $newSymbol = NaturalDocs::SymbolTable::Symbol->New(); + $newSymbol->AddDefinition($file, $type, $prototype, $summary); + + $symbols{$symbol} = $newSymbol; + + $self->OnIndexChange($type); + NaturalDocs::Project->RebuildFile($file); + } + + + # If the symbol already exists... + else + { + my $symbolObject = $symbols{$symbol}; + + # If the symbol isn't defined, i.e. it was a potential interpretation only... + if (!$symbolObject->IsDefined()) + { + $symbolObject->AddDefinition($file, $type, $prototype, $summary); + + # See if this symbol provides a better interpretation of any references. We can assume this symbol has interpretations + # because the object won't exist without either that or definitions. + + my %referencesAndScores = $symbolObject->ReferencesAndScores(); + + while (my ($referenceString, $referenceScore) = each %referencesAndScores) + { + my $referenceObject = $references{$referenceString}; + + if (!$referenceObject->HasCurrentInterpretation() || + $referenceScore > $referenceObject->CurrentScore()) + { + $referenceObject->SetCurrentInterpretation($symbol); + $self->OnInterpretationChange($referenceString); + }; + }; + + $self->OnIndexChange($type); + NaturalDocs::Project->RebuildFile($file); + } + + # If the symbol is defined but not in this file... + elsif (!$symbolObject->IsDefinedIn($file)) + { + $symbolObject->AddDefinition($file, $type, $prototype, $summary); + + $self->OnIndexChange($type); + NaturalDocs::Project->RebuildFile($file); + + # We don't have to check other files because if the symbol is defined it already has a global definiton, + # and everything else is either using that or its own definition, and thus wouldn't be affected by this. + }; + + # If the symbol was already defined in this file, ignore it. + + }; + + + # Add it to the file index. + + if (!exists $files{$file}) + { $files{$file} = NaturalDocs::SymbolTable::File->New(); }; + + $files{$file}->AddSymbol($symbol); + + + # Add it to the watched file, if necessary. + + if (defined $watchedFileName) + { + $watchedFile->AddSymbol($symbol); + + if (!exists $watchedFileSymbolDefinitions{$symbol}) + { + $watchedFileSymbolDefinitions{$symbol} = + NaturalDocs::SymbolTable::SymbolDefinition->New($type, $prototype, $summary); + }; + }; + }; + + +# +# Function: AddReference +# +# Adds a reference to the table, if it doesn't already exist. +# +# Parameters: +# +# type - The <ReferenceType>. +# symbol - The reference <SymbolString>. +# scope - The scope <SymbolString> it appears in. +# using - An arrayref of scope <SymbolStrings> accessible to the reference via "using" statements, or undef if none. +# file - The <FileName> where the reference appears. This is not required unless the type is <REFERENCE_TEXT>. +# resolvingFlags - The <Resolving Flags> of the reference. They will be ignored if the type is <REFERENCE_TEXT>. +# +# Alternate Parameters: +# +# referenceString - The <ReferenceString> to add. +# file - The <FileName> where the reference appears. This is not required unless the type is <REFERENCE_TEXT>. +# +sub AddReference #(type, symbol, scope, using, file, resolvingFlags) or (referenceString, file) + { + my ($self, $referenceString, $file); + + if (scalar @_ <= 3) + { + ($self, $referenceString, $file) = @_; + } + else + { + my ($type, $symbol, $scope, $using, $resolvingFlags); + ($self, $type, $symbol, $scope, $using, $file, $resolvingFlags) = @_; + + $referenceString = NaturalDocs::ReferenceString->MakeFrom($type, $symbol, + NaturalDocs::Languages->LanguageOf($file)->Name(), + $scope, $using, $resolvingFlags); + }; + + + # If the reference doesn't exist... + if (!exists $references{$referenceString}) + { + my $referenceObject = NaturalDocs::SymbolTable::Reference->New(); + + $references{$referenceString} = $referenceObject; + + $self->GenerateInterpretations($referenceString); + $self->InterpretReference($referenceString); + } + + + if (defined $file) + { + $references{$referenceString}->AddDefinition($file); + + + # Add it to the file index. + + if (!exists $files{$file}) + { $files{$file} = NaturalDocs::SymbolTable::File->New(); }; + + $files{$file}->AddReference($referenceString); + + + # Add it to the watched file, if necessary. + + if (defined $watchedFileName) + { $watchedFile->AddReference($referenceString); }; + }; + }; + + +# +# Function: WatchFileForChanges +# +# Tracks a file to see if any symbols or references were changed or deleted in ways that would require other files to be rebuilt. +# Assumes that after this function call, the entire file will be parsed again, and thus every symbol and reference will go through +# <AddSymbol()> and <AddReference()>. Afterwards, call <AnalyzeChanges()> to handle any differences. +# +# Parameters: +# +# file - The <FileName> to watch. +# +sub WatchFileForChanges #(file) + { + my ($self, $file) = @_; + + $watchedFile = NaturalDocs::SymbolTable::File->New(); + $watchedFileName = $file; + %watchedFileSymbolDefinitions = ( ); + }; + + +# +# Function: AnalyzeChanges +# +# Handles any changes found when reparsing a file using <WatchFileForChanges()>. +# +sub AnalyzeChanges + { + my ($self) = @_; + + if (exists $files{$watchedFileName}) + { + + # Go through the references and remove any that were deleted. Ones that were added will have already been added to + # the table in AddReference(). + + my @references = $files{$watchedFileName}->References(); + foreach my $reference (@references) + { + if (!$watchedFile->DefinesReference($reference)) + { $self->DeleteReference($reference, $watchedFileName); }; + }; + }; + + # We have to check if the watched file exists again because DeleteReference() could have removed it. I'm still not sure how a + # file could have references without symbols, but apparently it's happened in the real world because it's crashed on people. + if (exists $files{$watchedFileName}) + { + # Go through the symbols. + + my $rebuildFile; + + my @symbols = $files{$watchedFileName}->Symbols(); + foreach my $symbol (@symbols) + { + # Delete symbols that don't exist. + + if (!$watchedFile->DefinesSymbol($symbol)) + { + $self->DeleteSymbol($symbol, $watchedFileName); + $rebuildFile = 1; + } + + else + { + my $symbolObject = $symbols{$symbol}; + my $newSymbolDef = $watchedFileSymbolDefinitions{$symbol}; + + # Update symbols that changed. + + if ( $symbolObject->TypeDefinedIn($watchedFileName) ne $newSymbolDef->Type() || + $symbolObject->PrototypeDefinedIn($watchedFileName) ne $newSymbolDef->Prototype() || + $symbolObject->SummaryDefinedIn($watchedFileName) ne $newSymbolDef->Summary() ) + { + $self->OnIndexChange($symbolObject->TypeDefinedIn($watchedFileName)); + $self->OnIndexChange($newSymbolDef->Type()); + $rebuildFile = 1; + + $symbolObject->ChangeDefinition($watchedFileName, $newSymbolDef->Type(), $newSymbolDef->Prototype(), + $newSymbolDef->Summary()); + + # If the symbol definition was the global one, we need to update all files that reference it. If it wasn't, the only file + # that could references it is itself, and the only way the symbol definition could change in the first place was if it was + # itself changed. + if ($symbolObject->GlobalDefinition() eq $watchedFileName) + { + # Rebuild the files that have references to this symbol + my @references = $symbolObject->References(); + foreach my $reference (@references) + { + if ($references{$reference}->CurrentInterpretation() eq $symbol) + { $self->OnTargetSymbolChange($reference); }; + }; # While references + }; # If global definition is watched file + }; # If the symbol definition changed + }; # If the symbol still exists + }; # foreach symbol in watched file + + if ($rebuildFile) + { NaturalDocs::Project->RebuildFile($watchedFileName); }; + + }; + + + $watchedFile = undef; + $watchedFileName = undef; + %watchedFileSymbolDefinitions = ( ); + }; + + +# +# Function: DeleteReference +# +# Deletes a reference from the table. +# +# Be careful with this function, as deleting a reference means there are no more of them in the file at all. The tables do not +# keep track of how many times references appear in a file. In these cases you should instead call <WatchFileForChanges()>, +# reparse the file, thus readding all the references, and call <AnalyzeChanges()>. +# +# <REFERENCE_TEXT> references should *always* be managed with <WatchFileForChanges()> and <AnalyzeChanges()>. +# This function should only be used externally for other types of references. +# +# Parameters: +# +# referenceString - The <ReferenceString>. +# file - The <FileName> where the reference is. This is not required unless the type is <REFERENCE_TEXT>. +# +sub DeleteReference #(referenceString, file) + { + my ($self, $referenceString, $file) = @_; + + + # If the reference exists... + if (exists $references{$referenceString}) + { + my $referenceObject = $references{$referenceString}; + + if (defined $file) + { $referenceObject->DeleteDefinition($file); }; + + # If there are no other definitions, or it doesn't use file definitions to begin with... + if (!$referenceObject->IsDefined()) + { + my @interpretations = $referenceObject->Interpretations(); + foreach my $interpretation (@interpretations) + { + $symbols{$interpretation}->DeleteReference($referenceString); + }; + + delete $references{$referenceString}; + }; + + + if (defined $file) + { + # Remove it from the file index. + + $files{$file}->DeleteReference($referenceString); + + if (!$files{$file}->HasAnything()) + { delete $files{$file}; }; + + # We don't need to worry about the watched file, since this function will only be called by AnalyzeChanges() and + # LoadAndPurge(). + }; + }; + }; + + +# +# Function: RebuildAllIndexes +# +# When called, it makes sure all indexes are listed as changed by <IndexChanged()>, regardless of whether they actually did +# or not. +# +# This can be called at any time. +# +sub RebuildAllIndexes + { + my $self = shift; + $rebuildIndexes = 1; + }; + + +# +# Function: PurgeResolvingInfo +# +# Purges unnecessary information from the symbol table after it is fully resolved. This will reduce the memory footprint for the +# build stage. After calling this function, you can only call the <Information Functions> and <Save()>. +# +sub PurgeResolvingInfo + { + my ($self) = @_; + + # Go through the symbols. We don't need to keep around potential symbols anymore, nor do we need what references can + # be interpreted as the defined ones. + + while (my ($symbol, $symbolObject) = each %symbols) + { + if ($symbolObject->IsDefined()) + { $symbolObject->DeleteAllReferences(); } + else + { delete $symbols{$symbol}; }; + }; + + + # Go through the references. We don't need any of the interpretations except for the current. + + foreach my $referenceObject (values %references) + { $referenceObject->DeleteAllInterpretationsButCurrent(); }; + + + # We don't need the information by file at all. + + %files = ( ); + }; + + +# +# Function: PurgeIndexes +# +# Clears all generated indexes. +# +sub PurgeIndexes + { + my ($self) = @_; + %indexes = ( ); + }; + + +############################################################################### +# Group: Information Functions +# These functions should not be called until the symbol table is fully resolved. + + +# +# Function: References +# +# Returns what the passed reference information resolve to, if anything. Note that this only works if the reference had +# been previously added to the table via <AddReference()> with the exact same parameters. +# +# Parameters: +# +# type - The <ReferenceType>. +# symbol - The reference <SymbolString>. +# scope - The scope <SymbolString> the reference appears in, or undef if none. +# using - An arrayref of scope <SymbolStrings> available to the reference via using statements. +# file - The source <FileName> the reference appears in, or undef if none. +# resolvingFlags - The <Resolving Flags> of the reference. Ignored if the type is <REFERENCE_TEXT>. +# +# Alternate Parameters: +# +# referenceString - The <ReferenceString> to resolve. +# file - The source <FileName> the reference appears in, or undef if none. +# +# Returns: +# +# A <NaturalDocs::SymbolTable::ReferenceTarget> object, or undef if the reference doesn't resolve to anything. +# +sub References #(type, symbol, scope, using, file, resolvingFlags) or (referenceString, file) + { + my ($self, $referenceString, $file); + + if (scalar @_ <= 3) + { ($self, $referenceString, $file) = @_; } + else + { + my ($type, $symbol, $scope, $using, $resolvingFlags); + ($self, $type, $symbol, $scope, $using, $file, $resolvingFlags) = @_; + + $referenceString = NaturalDocs::ReferenceString->MakeFrom($type, $symbol, + NaturalDocs::Languages->LanguageOf($file)->Name(), + $scope, $using, $resolvingFlags); + }; + + if (exists $references{$referenceString} && $references{$referenceString}->HasCurrentInterpretation()) + { + my $targetSymbol = $references{$referenceString}->CurrentInterpretation(); + my $targetObject = $symbols{$targetSymbol}; + + my $targetFile; + my $targetType; + my $targetPrototype; + my $targetSummary; + + if (defined $file && $targetObject->IsDefinedIn($file)) + { + $targetFile = $file; + $targetType = $targetObject->TypeDefinedIn($file); + $targetPrototype = $targetObject->PrototypeDefinedIn($file); + $targetSummary = $targetObject->SummaryDefinedIn($file); + } + else + { + $targetFile = $targetObject->GlobalDefinition(); + $targetType = $targetObject->GlobalType(); + $targetPrototype = $targetObject->GlobalPrototype(); + $targetSummary = $targetObject->GlobalSummary(); + }; + + return NaturalDocs::SymbolTable::ReferenceTarget->New($targetSymbol, $targetFile, $targetType, $targetPrototype, + $targetSummary); + } + + else + { return undef; }; + }; + + +# +# Function: Lookup +# +# Returns information on the passed <SymbolString>, if it exists. Note that the symbol must be fully resolved. +# +# Parameters: +# +# symbol - The <SymbolString>. +# file - The source <FileName> the reference appears in, or undef if none. +# +# Returns: +# +# A <NaturalDocs::SymbolTable::ReferenceTarget> object, or undef if the symbol isn't defined. +# +sub Lookup #(symbol, file) + { + my ($self, $symbol, $file) = @_; + + my $symbolObject = $symbols{$symbol}; + + if (defined $symbolObject) + { + my $targetFile; + my $targetType; + my $targetPrototype; + my $targetSummary; + + if (defined $file && $symbolObject->IsDefinedIn($file)) + { + $targetFile = $file; + $targetType = $symbolObject->TypeDefinedIn($file); + $targetPrototype = $symbolObject->PrototypeDefinedIn($file); + $targetSummary = $symbolObject->SummaryDefinedIn($file); + } + else + { + $targetFile = $symbolObject->GlobalDefinition(); + $targetType = $symbolObject->GlobalType(); + $targetPrototype = $symbolObject->GlobalPrototype(); + $targetSummary = $symbolObject->GlobalSummary(); + }; + + return NaturalDocs::SymbolTable::ReferenceTarget->New($symbol, $targetFile, $targetType, $targetPrototype, + $targetSummary); + } + + else + { return undef; }; + }; + + +# +# Function: Index +# +# Returns a symbol index. +# +# Indexes are generated on demand, but they are stored so subsequent calls for the same index will be fast. Call +# <PurgeIndexes()> to clear the generated indexes. +# +# Parameters: +# +# type - The <TopicType> of symbol to limit the index to, or undef for none. +# +# Returns: +# +# An arrayref of sections. The first represents all the symbols, the second the numbers, and the rest A through Z. +# Each section is a sorted arrayref of <NaturalDocs::SymbolTable::IndexElement> objects. If a section has no content, +# it will be undef. +# +sub Index #(type) + { + my ($self, $type) = @_; + + if (!exists $indexes{$type}) + { $indexes{$type} = $self->MakeIndex($type); }; + + return $indexes{$type}; + }; + + +# +# Function: HasIndexes +# +# Determines which indexes out of a list actually have content. +# +# Parameters: +# +# types - An existence hashref of the <TopicTypes> to check for indexes. +# +# Returns: +# +# An existence hashref of all the specified indexes that have content. Will return an empty hashref if none. +# +sub HasIndexes #(types) + { + my ($self, $types) = @_; + + # EliminationHash is a copy of all the types, and the types will be deleted as they are found. This allows us to quit early if + # we've found all the types because the hash will be empty. We'll later return the original hash minus what was left over + # in here, which are the ones that weren't found. + my %eliminationHash = %$types; + + finddefs: + foreach my $symbolObject (values %symbols) + { + foreach my $definition ($symbolObject->Definitions()) + { + delete $eliminationHash{ $symbolObject->TypeDefinedIn($definition) }; + delete $eliminationHash{ ::TOPIC_GENERAL() }; + + if (!scalar keys %eliminationHash) + { last finddefs; }; + }; + }; + + my $result = { %$types }; + + foreach my $type (keys %eliminationHash) + { delete $result->{$type}; }; + + return $result; + }; + + +# +# Function: IndexChanged +# +# Returns whether the specified index has changed. +# +# Parameters: +# +# type - The <TopicType> to limit the index to. +# +sub IndexChanged #(TopicType type) + { + my ($self, $type) = @_; + return ($rebuildIndexes || defined $indexChanges{$type}); + }; + + +# +# Function: IndexSectionsWithContent +# +# Returns an arrayref of whether each section of the specified index has content. The first entry will be for symbols, the second +# for numbers, and the rest A-Z. Do not change the arrayref. +# +sub IndexSectionsWithContent #(TopicType type) + { + my ($self, $type) = @_; + + if (!exists $indexSectionsWithContent{$type}) + { + # This is okay because Index() stores generated indexes. It's not an expensive operation unless the index was never asked + # for before or it will never be asked for otherwise, and this shouldn't be the case. + + my $index = $self->Index($type); + my $content = [ ]; + + for (my $i = 0; $i < 28; $i++) + { + push @$content, (defined $index->[$i] ? 1 : 0); + }; + + $indexSectionsWithContent{$type} = $content; + }; + + return $indexSectionsWithContent{$type}; + }; + + + +############################################################################### +# Group: Event Handlers + + +# +# Function: OnIndexChange +# +# Called whenever a change happens to a symbol that would cause an index to be regenerated. +# +# Parameters: +# +# type - The <TopicType> of the symbol that caused the change. +# +sub OnIndexChange #(TopicType type) + { + my ($self, $type) = @_; + + $indexChanges{$type} = 1; + $indexChanges{::TOPIC_GENERAL()} = 1; + delete $indexSectionsWithContent{$type}; + }; + + +# +# Function: OnInterpretationChange +# +# Called whenever the current interpretation of a reference changes, meaning it switched from one symbol to another. +# +# Parameters: +# +# referenceString - The <ReferenceString> whose current interpretation changed. +# +sub OnInterpretationChange #(referenceString) + { + my ($self, $referenceString) = @_; + my $referenceType = NaturalDocs::ReferenceString->TypeOf($referenceString); + + if ($referenceType == ::REFERENCE_TEXT()) + { + my @referenceDefinitions = $references{$referenceString}->Definitions(); + + foreach my $referenceDefinition (@referenceDefinitions) + { + NaturalDocs::Project->RebuildFile($referenceDefinition); + }; + } + + elsif (NaturalDocs::Constants->IsClassHierarchyReference($referenceType)) + { + NaturalDocs::ClassHierarchy->OnInterpretationChange($referenceString); + }; + }; + + +# +# Function: OnTargetSymbolChange +# +# Called whenever the symbol that serves as the interpretation of a reference changes, but the reference still resolves to +# the same symbol. This would happen if the type, prototype, summary, or which file serves as global definition of the symbol +# changes. +# +# Parameters: +# +# referenceString - The <ReferenceString> whose interpretation's symbol changed. +# +sub OnTargetSymbolChange #(referenceString) + { + my ($self, $referenceString) = @_; + my $referenceType = NaturalDocs::ReferenceString->TypeOf($referenceString); + + if ($referenceType == ::REFERENCE_TEXT()) + { + my @referenceDefinitions = $references{$referenceString}->Definitions(); + + foreach my $referenceDefinition (@referenceDefinitions) + { + NaturalDocs::Project->RebuildFile($referenceDefinition); + }; + } + + elsif (NaturalDocs::Constants->IsClassHierarchyReference($referenceType)) + { + NaturalDocs::ClassHierarchy->OnTargetSymbolChange($referenceString); + }; + }; + + + +############################################################################### +# Group: Support Functions + + +# +# Function: DeleteSymbol +# +# Removes a symbol definition from the table. It will call <OnInterpretationChange()> for all references that have it as their +# current interpretation. +# +# External code should not attempt to delete symbols using this function. Instead it should call <WatchFileFoChanges()>, +# reparse the file, and call <AnalyzeChanges()>. +# +# Parameters: +# +# symbol - The <SymbolString>. +# file - The <FileName> where the definition is. +# +sub DeleteSymbol #(symbol, file) + { + my ($self, $symbol, $file) = @_; + + + # If the symbol and definition exist... + if (exists $symbols{$symbol} && $symbols{$symbol}->IsDefinedIn($file)) + { + my $symbolObject = $symbols{$symbol}; + my $wasGlobal = ($symbolObject->GlobalDefinition() eq $file); + + $self->OnIndexChange($symbolObject->TypeDefinedIn($file)); + + $symbolObject->DeleteDefinition($file); + + # If this was one definition of many... + if ($symbolObject->IsDefined()) + { + + # If this was the global definition... + if ($wasGlobal) + { + # Update every file that referenced the global symbol; i.e. every file that doesn't have its own definition. + + my @references = $symbolObject->References(); + + foreach my $reference (@references) + { + if ($references{$reference}->CurrentInterpretation() eq $symbol) + { + $self->OnTargetSymbolChange($reference); + }; + }; + } + + # If this wasn't the global definition... + else + { + # It's a safe bet that we don't need to do anything here. The only thing that we even need to look for here is if the + # file referenced its own symbol and thus should be rebuilt. However, if the file is having a symbol deleted, it either + # changed or was itself deleted. If it changed and still has other Natural Docs content, it should already be on the + # rebuild list. If it was deleted or no longer has Natural Docs content, we certainly don't want to add it to the rebuild + # list. + }; + } + + # If this is the only definition... + else + { + # If this symbol is the interpretation of any references... + if ($symbolObject->HasReferences()) + { + # If this was the current interpretation of any references, reinterpret them and rebuild their files. + + my @references = $symbolObject->References(); + + foreach my $reference (@references) + { + if ($references{$reference}->CurrentInterpretation() eq $symbol) + { + $self->InterpretReference($reference); + $self->OnInterpretationChange($reference); + }; + }; + } + + # If there are no interpretations of the symbol... + else + { + # Delete the symbol entirely. + delete $symbols{$symbol}; + }; + }; + + # Remove it from the file index. + + $files{$file}->DeleteSymbol($symbol); + + if (!$files{$file}->HasAnything()) + { delete $files{$file}; }; + + + # We don't need to worry about the watched file, since this function will only be called by AnalyzeChanges() and + # LoadAndPurge(). + }; + }; + + +# +# Function: GenerateInterpretations +# +# Generates the list of interpretations for the passed reference. Also creates potential symbols as necessary. +# +# Parameters: +# +# referenceString - The <ReferenceString> to generate the interpretations of. +# +sub GenerateInterpretations #(referenceString) + { + my ($self, $referenceString) = @_; + + my ($type, $symbol, $languageName, $scope, $using, $resolvingFlags) = + NaturalDocs::ReferenceString->InformationOf($referenceString); + + # RESOLVE_NOPLURAL is handled by having @singulars be empty. + my @singulars; + if (!($resolvingFlags & ::RESOLVE_NOPLURAL())) + { @singulars = $self->SingularInterpretationsOf($symbol); }; + + # Since higher scores are better, we'll start at a high number and decrement. + my $score = 50000; + + + # If RESOLVE_RELATIVE is set, we do all the scope relatives before the global. + if ($resolvingFlags & ::RESOLVE_RELATIVE()) + { + $score = $self->GenerateRelativeInterpretations($referenceString, $symbol, \@singulars, $scope, $score); + } + + # If neither RESOLVE_RELATIVE nor RESOLVE_ABSOLUTE is set, we only do the local before the global. + elsif (!($resolvingFlags & ::RESOLVE_ABSOLUTE())) + { + $self->AddInterpretation($referenceString, NaturalDocs::SymbolString->Join($scope, $symbol), $score); + $score--; + + foreach my $singular (@singulars) + { + $self->AddInterpretation($referenceString, NaturalDocs::SymbolString->Join($scope, $singular), $score); + $score--; + }; + }; + + + # Do the global. + + $self->AddInterpretation($referenceString, $symbol, $score); + $score--; + + foreach my $singular (@singulars) + { + $self->AddInterpretation($referenceString, $singular, $score); + $score--; + }; + + + # If neither RESOLVE_RELATIVE nor RESOLVE_ABSOLUTE is set, we need to do the rest of the scope relatives after the global. + if (!($resolvingFlags & ::RESOLVE_RELATIVE()) && !($resolvingFlags & ::RESOLVE_ABSOLUTE())) + { + $score = $self->GenerateRelativeInterpretations($referenceString, $symbol, \@singulars, $scope, $score, 1); + }; + + + # Finally, if RESOLVE_NOUSING isn't set, go through the using scopes. + if (!($resolvingFlags & ::RESOLVE_NOUSING()) && defined $using) + { + foreach my $usingScope (@$using) + { + if ($resolvingFlags & ::RESOLVE_ABSOLUTE()) + { + $self->AddInterpretation($referenceString, NaturalDocs::SymbolString->Join($usingScope, $symbol), $score); + $score--; + + foreach my $singular (@singulars) + { + $self->AddInterpretation($referenceString, NaturalDocs::SymbolString->Join($usingScope, $singular), $score); + $score--; + }; + } + else + { + $score = $self->GenerateRelativeInterpretations($referenceString, $symbol, \@singulars, $usingScope, $score); + }; + }; + }; + }; + + +# +# Function: GenerateRelativeInterpretations +# +# Generates the list of relative interpretations for the passed reference and packages. Also creates potential symbols as +# necessary. +# +# This function will _not_ create global interpretations. It _will_ create a local interpretations (symbol + all packages) unless +# you set dontUseFull. +# +# Parameters: +# +# referenceString - The <ReferenceString> to generate interpretations for. +# symbol - The <SymbolString> to generate interpretations of. +# singulars - A reference to an array of singular <SymbolStrings> to also generate interpretations of. Set to an empty array +# if none. +# package - The package <SymbolString> to use. May be undef. +# score - The starting score to apply. +# dontUseFull - Whether to not generate an interpretation including the full package identifier. If set, generated interpretations +# will start one level down. +# +# Returns: +# +# The next unused score. This is basically the passed score minus the number of interpretations created. +# +sub GenerateRelativeInterpretations #(referenceString, symbol, singulars, package, score, dontUseFull) + { + my ($self, $referenceString, $symbol, $singulars, $package, $score, $dontUseFull) = @_; + + my @packages = NaturalDocs::SymbolString->IdentifiersOf($package); + + # The last package index to include. This number is INCLUSIVE! + my $packageLevel = scalar @packages - 1; + + if ($dontUseFull) + { $packageLevel--; }; + + while ($packageLevel >= 0) + { + $self->AddInterpretation($referenceString, NaturalDocs::SymbolString->Join(@packages[0..$packageLevel], $symbol), + $score); + $score--; + + foreach my $singular (@$singulars) + { + $self->AddInterpretation($referenceString, NaturalDocs::SymbolString->Join(@packages[0..$packageLevel], $singular), + $score); + $score--; + }; + + $packageLevel--; + }; + + return $score; + }; + + +# +# Function: SingularInterpretationsOf +# +# Generates singular interpretations of a <SymbolString> if it can be interpreted as a plural. Not all of them will be valid singular +# forms, but that doesn't matter since it's incredibly unlikely an invalid form would exist as a symbol. What matters is that the +# legimate singular is present on the list. +# +# Parameters: +# +# symbol - The <SymbolString>. +# +# Returns: +# +# An array of potential singular interpretations as <SymbolStrings>, in no particular order. If the symbol can't be interpreted +# as a plural, returns an empty array. +# +sub SingularInterpretationsOf #(symbol) + { + my ($self, $symbol) = @_; + + my @identifiers = NaturalDocs::SymbolString->IdentifiersOf($symbol); + my $lastIdentifier = pop @identifiers; + my $preIdentifiers = NaturalDocs::SymbolString->Join(@identifiers); + + my @results; + + # First cut off any 's or ' at the end, since they can appear after other plural forms. + if ($lastIdentifier =~ s/\'s?$//i) + { + push @results, NaturalDocs::SymbolString->Join($preIdentifiers, $lastIdentifier); + }; + + # See http://www.gsu.edu/~wwwesl/egw/crump.htm for a good list of potential plural forms. There are a couple more than + # listed below, but they're fairly rare and this is already seriously over-engineered. This is split by suffix length to make + # comparisons more efficient. + + # The fact that this will generate some impossible combinations (leaves => leave, leav, leaf, leafe) doesn't matter. It's very + # unlikely that more than one will manage to match a defined symbol. Even if they do (leave, leaf), it's incredibly unlikely + # that someone has defined an impossible one (leav, leafe). So it's not so important that we remove impossible combinations, + # just that we include all the possible ones. + + my @suffixGroups = ( [ 's', undef, # boys => boy + 'i', 'us', # alumni => alumnus + 'a', 'um', # errata => erratum + 'a', 'on' ], # phenomena => phenomenon + + [ 'es', undef, # foxes => fox + 'ae', 'a' ], # amoebae => amoeba + + [ 'ies', 'y', # pennies => penny + 'ves', 'f', # calves => calf + 'ves', 'fe', # knives => knife + 'men', 'man', # women => woman + 'ice', 'ouse', # mice => mouse + 'oes', 'o', # vetoes => veto + 'ces', 'x', # matrices => matrix + 'xen', 'x' ], # oxen => ox + + [ 'ices', 'ex', # indices => index + 'feet', 'foot', # feet => foot + 'eese', 'oose', # geese => goose + 'eeth', 'ooth', # teeth => tooth + 'dren', 'd' ] ); # children => child + + my $suffixLength = 1; + + foreach my $suffixGroup (@suffixGroups) + { + my $identifierSuffix = lc( substr($lastIdentifier, 0 - $suffixLength) ); + my $cutIdentifier = substr($lastIdentifier, 0, 0 - $suffixLength); + + for (my $i = 0; $i + 1 < scalar @$suffixGroup; $i += 2) + { + my $suffix = $suffixGroup->[$i]; + my $replacement = $suffixGroup->[$i + 1]; + + if ($identifierSuffix eq $suffix) + { + if (defined $replacement) + { + push @results, NaturalDocs::SymbolString->Join($preIdentifiers, $cutIdentifier . $replacement); + push @results, NaturalDocs::SymbolString->Join($preIdentifiers, $cutIdentifier . uc($replacement)); + } + else + { + push @results, NaturalDocs::SymbolString->Join($preIdentifiers, $cutIdentifier); + }; + }; + }; + + $suffixLength++; + }; + + return @results; + }; + + +# +# Function: AddInterpretation +# +# Adds an interpretation to an existing reference. Creates potential symbols as necessary. +# +# Parameters: +# +# referenceString - The <ReferenceString> to add the interpretation to. +# symbol - The <SymbolString> the reference can be interpreted as. +# score - The score of the interpretation. +# +sub AddInterpretation #(referenceString, symbol, score) + { + my ($self, $referenceString, $symbol, $score) = @_; + + $references{$referenceString}->AddInterpretation($symbol, $score); + + # Create a potential symbol if it doesn't exist. + + if (!exists $symbols{$symbol}) + { $symbols{$symbol} = NaturalDocs::SymbolTable::Symbol->New(); }; + + $symbols{$symbol}->AddReference($referenceString, $score); + }; + + +# +# Function: InterpretReference +# +# Interprets the passed reference, matching it to the defined symbol with the highest score. If the symbol is already +# interpreted, it will reinterpret it. If there are no matches, it will make it an undefined reference. +# +# Parameters: +# +# referenceString - The <ReferenceString> to interpret. +# +sub InterpretReference #(referenceString) + { + my ($self, $referenceString) = @_; + + my $interpretation; + my $currentInterpretation; + my $score; + my $currentScore = -1; + + my $referenceObject = $references{$referenceString}; + + my %interpretationsAndScores = $referenceObject->InterpretationsAndScores(); + while ( ($interpretation, $score) = each %interpretationsAndScores ) + { + if ($score > $currentScore && $symbols{$interpretation}->IsDefined()) + { + $currentScore = $score; + $currentInterpretation = $interpretation; + }; + }; + + if ($currentScore > -1) + { $referenceObject->SetCurrentInterpretation($currentInterpretation); } + else + { $referenceObject->SetCurrentInterpretation(undef); }; + }; + + +# +# Function: MakeIndex +# +# Generates a symbol index. +# +# Parameters: +# +# type - The <TopicType> to limit the index to. +# +# Returns: +# +# An arrayref of sections. The first represents all the symbols, the second the numbers, and the rest A through Z. +# Each section is a sorted arrayref of <NaturalDocs::SymbolTable::IndexElement> objects. If a section has no content, +# it will be undef. +# +sub MakeIndex #(type) + { + my ($self, $type) = @_; + + + # Go through the symbols and generate IndexElements for any that belong in the index. + + # Keys are the symbol strings, values are IndexElements. + my %indexSymbols; + + while (my ($symbolString, $object) = each %symbols) + { + my ($symbol, $package) = $self->SplitSymbolForIndex($symbolString, $object->GlobalType()); + my @definitions = $object->Definitions(); + + foreach my $definition (@definitions) + { + my $definitionType = $object->TypeDefinedIn($definition); + + if ($type eq ::TOPIC_GENERAL() || $type eq $definitionType) + { + if (!exists $indexSymbols{$symbol}) + { + $indexSymbols{$symbol} = + NaturalDocs::SymbolTable::IndexElement->New($symbol, $package, $definition, $definitionType, + $object->PrototypeDefinedIn($definition), + $object->SummaryDefinedIn($definition) ); + } + else + { + $indexSymbols{$symbol}->Merge($package, $definition, $definitionType, + $object->PrototypeDefinedIn($definition), + $object->SummaryDefinedIn($definition) ); + }; + }; # If type matches + }; # Each definition + }; # Each symbol + + + # Generate sortable symbols for each IndexElement, sort them internally, and divide them into sections. + + my $sections = [ ]; + + foreach my $indexElement (values %indexSymbols) + { + $indexElement->Sort(); + $indexElement->MakeSortableSymbol(); + + my $sectionNumber; + + if ($indexElement->SortableSymbol() =~ /^([a-z])/i) + { $sectionNumber = ord(lc($1)) - ord('a') + 2; } + elsif ($indexElement->SortableSymbol() =~ /^[0-9]/) + { $sectionNumber = 1; } + else + { $sectionNumber = 0; }; + + if (!defined $sections->[$sectionNumber]) + { $sections->[$sectionNumber] = [ ]; }; + + push @{$sections->[$sectionNumber]}, $indexElement; + }; + + + # Sort each section. + + for (my $i = 0; $i < scalar @$sections; $i++) + { + if (defined $sections->[$i]) + { + @{$sections->[$i]} = sort + { + my $result = ::StringCompare($a->SortableSymbol(), $b->SortableSymbol()); + + if ($result == 0) + { $result = ::StringCompare($a->IgnoredPrefix(), $b->IgnoredPrefix()); }; + + return $result; + } + @{$sections->[$i]}; + }; + }; + + return $sections; + }; + + +# +# Function: SplitSymbolForIndex +# +# Splits a <SymbolString> into its symbol and package portions for indexing. +# +# Parameters: +# +# symbol - The <SymbolString>. +# type - Its <TopicType>. +# +# Returns: +# +# The array ( symbol, package ), which are both <SymbolStrings>. If the symbol is global, package will be undef. +# +sub SplitSymbolForIndex #(symbol, type) + { + my ($self, $symbol, $type) = @_; + + my $scope = NaturalDocs::Topics->TypeInfo($type)->Scope(); + + if ($scope == ::SCOPE_START() || $scope == ::SCOPE_ALWAYS_GLOBAL()) + { return ( $symbol, undef ); } + else + { + my @identifiers = NaturalDocs::SymbolString->IdentifiersOf($symbol); + + $symbol = pop @identifiers; + my $package = NaturalDocs::SymbolString->Join(@identifiers); + + return ( $symbol, $package ); + }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/SymbolTable/File.pm b/docs/tool/Modules/NaturalDocs/SymbolTable/File.pm new file mode 100644 index 00000000..71f93645 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/SymbolTable/File.pm @@ -0,0 +1,186 @@ +############################################################################### +# +# Package: NaturalDocs::SymbolTable::File +# +############################################################################### +# +# A class representing a file, keeping track of what symbols and references are defined in it. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::SymbolTable::File; + + +############################################################################### +# Group: Implementation + +# +# Constants: Members +# +# The class is implemented as a blessed arrayref. The following constants are its members. +# +# SYMBOLS - An existence hashref of the <SymbolStrings> it defines. +# REFERENCES - An existence hashref of the <ReferenceStrings> in the file. +# + +# DEPENDENCY: New() depends on the order of these constants. If they change, New() has to be updated. +use constant SYMBOLS => 0; +use constant REFERENCES => 1; + + +############################################################################### +# Group: Modification Functions + + +# +# Function: New +# +# Creates and returns a new object. +# +sub New + { + my $package = shift; + + # Let's make it safe, since normally you can pass values to New. Having them just be ignored would be an obscure error. + if (scalar @_) + { die "You can't pass values to NaturalDocs::SymbolTable::File->New()\n"; }; + + # DEPENDENCY: This code depends on the order of the member constants. + my $object = [ { }, { } ]; + bless $object, $package; + + return $object; + }; + + +# +# Function: AddSymbol +# +# Adds a <SymbolString> definition. +# +# Parameters: +# +# symbol - The <SymbolString> being added. +# +sub AddSymbol #(symbol) + { + my ($self, $symbol) = @_; + $self->[SYMBOLS]{$symbol} = 1; + }; + + +# +# Function: DeleteSymbol +# +# Removes a <SymbolString> definition. +# +# Parameters: +# +# symbol - The <SymbolString> to delete. +# +sub DeleteSymbol #(symbol) + { + my ($self, $symbol) = @_; + delete $self->[SYMBOLS]{$symbol}; + }; + + +# +# Function: AddReference +# +# Adds a reference definition. +# +# Parameters: +# +# referenceString - The <ReferenceString> being added. +# +sub AddReference #(referenceString) + { + my ($self, $referenceString) = @_; + $self->[REFERENCES]{$referenceString} = 1; + }; + + +# +# Function: DeleteReference +# +# Removes a reference definition. +# +# Parameters: +# +# referenceString - The <ReferenceString> to delete. +# +sub DeleteReference #(referenceString) + { + my ($self, $referenceString) = @_; + delete $self->[REFERENCES]{$referenceString}; + }; + + + +############################################################################### +# Group: Information Functions + + +# +# Function: HasAnything +# +# Returns whether the file has any symbol or reference definitions at all. +# +sub HasAnything + { + return (scalar keys %{$_[0]->[SYMBOLS]} || scalar keys %{$_[0]->[REFERENCES]}); + }; + +# +# Function: Symbols +# +# Returns an array of all the <SymbolStrings> defined in this file. If none, returns an empty array. +# +sub Symbols + { + return keys %{$_[0]->[SYMBOLS]}; + }; + + +# +# Function: References +# +# Returns an array of all the <ReferenceStrings> defined in this file. If none, returns an empty array. +# +sub References + { + return keys %{$_[0]->[REFERENCES]}; + }; + + +# +# Function: DefinesSymbol +# +# Returns whether the file defines the passed <SymbolString> or not. +# +sub DefinesSymbol #(symbol) + { + my ($self, $symbol) = @_; + return exists $self->[SYMBOLS]{$symbol}; + }; + + +# +# Function: DefinesReference +# +# Returns whether the file defines the passed <ReferenceString> or not. +# +sub DefinesReference #(referenceString) + { + my ($self, $referenceString) = @_; + return exists $self->[REFERENCES]{$referenceString}; + }; + +1; diff --git a/docs/tool/Modules/NaturalDocs/SymbolTable/IndexElement.pm b/docs/tool/Modules/NaturalDocs/SymbolTable/IndexElement.pm new file mode 100644 index 00000000..e54ce44c --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/SymbolTable/IndexElement.pm @@ -0,0 +1,522 @@ +############################################################################### +# +# Class: NaturalDocs::SymbolTable::IndexElement +# +############################################################################### +# +# A class representing part of an indexed symbol. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use Tie::RefHash; + +use strict; +use integer; + + +package NaturalDocs::SymbolTable::IndexElement; + + +# +# Topic: How IndexElements Work +# +# This is a little tricky, so make sure you understand this. Indexes are sorted by symbol, then packages, then file. If there is only +# one package for a symbol, or one file definition for a package/symbol, they are added inline to the entry. However, if there are +# multiple packages or files, the function for it returns an arrayref of IndexElements instead. Which members are defined and +# undefined should follow common sense. For example, if a symbol is defined in multiple packages, the symbol's IndexElement +# will not define <File()>, <Type()>, or <Prototype()>; those will be defined in child elements. Similarly, the child elements will +# not define <Symbol()> since it's redundant. +# +# Diagrams may be clearer. If a member isn't listed for an element, it isn't defined. +# +# A symbol that only has one package and file: +# > [Element] +# > - Symbol +# > - Package +# > - File +# > - Type +# > - Prototype +# > - Summary +# +# A symbol that is defined by multiple packages, each with only one file: +# > [Element] +# > - Symbol +# > - Package +# > [Element] +# > - Package +# > - File +# > - Type +# > - Prototype +# > - Summary +# > [Element] +# > - ... +# +# A symbol that is defined by one package, but has multiple files +# > [Element] +# > - Symbol +# > - Package +# > - File +# > [Element] +# > - File +# > - Type +# > - Protype +# > - Summary +# > [Element] +# > - ... +# +# A symbol that is defined by multiple packages which have multiple files: +# > [Element] +# > - Symbol +# > - Package +# > [Element] +# > - Package +# > - File +# > [Element] +# > - File +# > - Type +# > - Prototype +# > - Summary +# > [Element] +# > - ... +# > [Element] +# > - ... +# +# Why is it done this way?: +# +# Because it makes it easier to generate nice indexes since all the splitting and combining is done for you. If a symbol +# has only one package, you just want to link to it, you don't want to break out a subindex for just one package. However, if +# it has multiple package, you do want the subindex and to link to each one individually. Use <HasMultiplePackages()> and +# <HasMultipleFiles()> to determine whether you need to add a subindex for it. +# +# +# Combining Properties: +# +# All IndexElements also have combining properties set. +# +# CombinedType - The general <TopicType> of the entry. Conflicts combine into <TOPIC_GENERAL>. +# PackageSeparator - The package separator symbol of the entry. Conflicts combine into a dot. +# +# So if an IndexElement only has one definition, <CombinedType()> is the same as the <TopicType> and <PackageSeparator()> +# is that of the definition's language. If other definitions are added and they have the same properties, the combined properties +# will remain the same. However, if they're different, they switch values as noted above. +# +# +# Sortable Symbol: +# +# <SortableSymbol()> is a pseudo-combining property. There were a few options for dealing with multiple languages defining +# the same symbol but stripping different prefixes off it, but ultimately I decided to go with whatever the language does that +# has the most definitions. There's not likely to be many conflicts here in the real world; probably the only thing would be +# defining it in a text file and forgetting to specify the prefixes to strip there too. So this works. +# +# Ties are broken pretty much randomly, except that text files always lose if its one of the options. +# +# It's a pseudo-combining property because it's done after the IndexElements are all filled in and only stored in the top-level +# ones. +# + + +############################################################################### +# Group: Implementation + +# +# Constants: Members +# +# The class is implemented as a blessed arrayref. The following constants are its members. +# +# SYMBOL - The <SymbolString> without the package portion. +# PACKAGE - The package <SymbolString>. Will be a package <SymbolString>, undef for global, or an arrayref of +# <NaturalDocs::SymbolTable::IndexElement> objects if multiple packages define the symbol. +# FILE - The <FileName> the package/symbol is defined in. Will be the file name or an arrayref of +# <NaturalDocs::SymbolTable::IndexElements> if multiple files define the package/symbol. +# TYPE - The package/symbol/file <TopicType>. +# PROTOTYPE - The package/symbol/file prototype, or undef if not applicable. +# SUMMARY - The package/symbol/file summary, or undef if not applicable. +# COMBINED_TYPE - The combined <TopicType> of the element. +# PACKAGE_SEPARATOR - The combined package separator symbol of the element. +# SORTABLE_SYMBOL - The sortable symbol as a text string. +# IGNORED_PREFIX - The part of the symbol that was stripped off to make the sortable symbol. +# +use NaturalDocs::DefineMembers 'SYMBOL', 'Symbol()', + 'PACKAGE', 'Package()', + 'FILE', 'File()', + 'TYPE', 'Type()', + 'PROTOTYPE', 'Prototype()', + 'SUMMARY', 'Summary()', + 'COMBINED_TYPE', 'CombinedType()', + 'PACKAGE_SEPARATOR', 'PackageSeparator()', + 'SORTABLE_SYMBOL', 'SortableSymbol()', + 'IGNORED_PREFIX', 'IgnoredPrefix()'; +# DEPENDENCY: New() depends on the order of these constants and that there is no inheritance.. + + +############################################################################### +# Group: Modification Functions + +# +# Function: New +# +# Returns a new object. +# +# This should only be used for creating an entirely new symbol. You should *not* pass arrayrefs as package or file parameters +# if you are calling this externally. Use <Merge()> instead. +# +# Parameters: +# +# symbol - The <SymbolString> without the package portion. +# package - The package <SymbolString>, or undef for global. +# file - The symbol's definition file. +# type - The symbol's <TopicType>. +# prototype - The symbol's prototype, if applicable. +# summary - The symbol's summary, if applicable. +# +# Optional Parameters: +# +# These parameters don't need to be specified. You should ignore them when calling this externally. +# +# combinedType - The symbol's combined <TopicType>. +# packageSeparator - The symbol's combined package separator symbol. +# +sub New #(symbol, package, file, type, prototype, summary, combinedType, packageSeparator) + { + # DEPENDENCY: This depends on the parameter list being in the same order as the constants. + + my $self = shift; + + my $object = [ @_ ]; + bless $object, $self; + + if (!defined $object->[COMBINED_TYPE]) + { $object->[COMBINED_TYPE] = $object->[TYPE]; }; + + if (!defined $object->[PACKAGE_SEPARATOR]) + { + if ($object->[TYPE] eq ::TOPIC_FILE()) + { $object->[PACKAGE_SEPARATOR] = '.'; } + else + { + $object->[PACKAGE_SEPARATOR] = NaturalDocs::Languages->LanguageOf($object->[FILE])->PackageSeparator(); + }; + }; + + return $object; + }; + + +# +# Function: Merge +# +# Adds another definition of the same symbol. Perhaps it has a different package or defining file. +# +# Parameters: +# +# package - The package <SymbolString>, or undef for global. +# file - The symbol's definition file. +# type - The symbol's <TopicType>. +# prototype - The symbol's protoype if applicable. +# summary - The symbol's summary if applicable. +# +sub Merge #(package, file, type, prototype, summary) + { + my ($self, $package, $file, $type, $prototype, $summary) = @_; + + # If there's only one package... + if (!$self->HasMultiplePackages()) + { + # If there's one package and it's the same as the new one... + if ($package eq $self->Package()) + { + $self->MergeFile($file, $type, $prototype, $summary); + } + + # If there's one package and the new one is different... + else + { + my $selfDefinition = NaturalDocs::SymbolTable::IndexElement->New(undef, $self->Package(), $self->File(), + $self->Type(), $self->Prototype(), + $self->Summary(), $self->CombinedType(), + $self->PackageSeparator()); + my $newDefinition = NaturalDocs::SymbolTable::IndexElement->New(undef, $package, $file, $type, $prototype, + $summary); + + $self->[PACKAGE] = [ $selfDefinition, $newDefinition ]; + $self->[FILE] = undef; + $self->[TYPE] = undef; + $self->[PROTOTYPE] = undef; + $self->[SUMMARY] = undef; + + if ($newDefinition->Type() ne $self->CombinedType()) + { $self->[COMBINED_TYPE] = ::TOPIC_GENERAL(); }; + if ($newDefinition->PackageSeparator() ne $self->PackageSeparator()) + { $self->[PACKAGE_SEPARATOR] = '.'; }; + }; + } + + # If there's more than one package... + else + { + # See if the new package is one of them. + my $selfPackages = $self->Package(); + my $matchingPackage; + + foreach my $testPackage (@$selfPackages) + { + if ($package eq $testPackage->Package()) + { + $testPackage->MergeFile($file, $type, $prototype, $summary);; + return; + }; + }; + + my $newDefinition = NaturalDocs::SymbolTable::IndexElement->New(undef, $package, $file, $type, $prototype, + $summary); + push @{$self->[PACKAGE]}, $newDefinition; + + if ($newDefinition->Type() ne $self->CombinedType()) + { $self->[COMBINED_TYPE] = ::TOPIC_GENERAL(); }; + if ($newDefinition->PackageSeparator() ne $self->PackageSeparator()) + { $self->[PACKAGE_SEPARATOR] = '.'; }; + }; + }; + + +# +# Function: Sort +# +# Sorts the package and file lists of the symbol. +# +sub Sort + { + my $self = shift; + + if ($self->HasMultipleFiles()) + { + @{$self->[FILE]} = sort { ::StringCompare($a->File(), $b->File()) } @{$self->File()}; + } + + elsif ($self->HasMultiplePackages()) + { + @{$self->[PACKAGE]} = sort { ::StringCompare( $a->Package(), $b->Package()) } @{$self->[PACKAGE]}; + + foreach my $packageElement ( @{$self->[PACKAGE]} ) + { + if ($packageElement->HasMultipleFiles()) + { $packageElement->Sort(); }; + }; + }; + }; + + +# +# Function: MakeSortableSymbol +# +# Generates <SortableSymbol()> and <IgnoredPrefix()>. Should only be called after everything is merged. +# +sub MakeSortableSymbol + { + my $self = shift; + + my $finalLanguage; + + if ($self->HasMultiplePackages() || $self->HasMultipleFiles()) + { + # Collect all the files that define this symbol. + + my @files; + + if ($self->HasMultipleFiles()) + { + my $fileElements = $self->File(); + + foreach my $fileElement (@$fileElements) + { push @files, $fileElement->File(); }; + } + else # HasMultiplePackages + { + my $packages = $self->Package(); + + foreach my $package (@$packages) + { + if ($package->HasMultipleFiles()) + { + my $fileElements = $package->File(); + + foreach my $fileElement (@$fileElements) + { push @files, $fileElement->File(); }; + } + else + { push @files, $package->File(); }; + }; + }; + + + # Determine which language defines it the most. + + # Keys are language objects, values are counts. + my %languages; + tie %languages, 'Tie::RefHash'; + + foreach my $file (@files) + { + my $language = NaturalDocs::Languages->LanguageOf($file); + + if (exists $languages{$language}) + { $languages{$language}++; } + else + { $languages{$language} = 1; }; + }; + + my $topCount = 0; + my @topLanguages; + + while (my ($language, $count) = each %languages) + { + if ($count > $topCount) + { + $topCount = $count; + @topLanguages = ( $language ); + } + elsif ($count == $topCount) + { + push @topLanguages, $language; + }; + }; + + if (scalar @topLanguages == 1) + { $finalLanguage = $topLanguages[0]; } + else + { + if ($topLanguages[0]->Name() ne 'Text File') + { $finalLanguage = $topLanguages[0]; } + else + { $finalLanguage = $topLanguages[1]; }; + }; + } + + else # !hasMultiplePackages && !hasMultipleFiles + { $finalLanguage = NaturalDocs::Languages->LanguageOf($self->File()); }; + + my $textSymbol = NaturalDocs::SymbolString->ToText($self->Symbol(), $self->PackageSeparator()); + my $ignoredPrefixLength = $finalLanguage->IgnoredPrefixLength($textSymbol, $self->CombinedType()); + + if ($ignoredPrefixLength) + { + $self->[IGNORED_PREFIX] = substr($textSymbol, 0, $ignoredPrefixLength); + $self->[SORTABLE_SYMBOL] = substr($textSymbol, $ignoredPrefixLength); + } + else + { $self->[SORTABLE_SYMBOL] = $textSymbol; }; + }; + + + +############################################################################### +# +# Functions: Information Functions +# +# Symbol - Returns the <SymbolString> without the package portion. +# Package - If <HasMultiplePackages()> is true, returns an arrayref of <NaturalDocs::SymbolTable::IndexElement> objects. +# Otherwise returns the package <SymbolString>, or undef if global. +# File - If <HasMultipleFiles()> is true, returns an arrayref of <NaturalDocs::SymbolTable::IndexElement> objects. Otherwise +# returns the name of the definition file. +# Type - Returns the <TopicType> of the package/symbol/file, if applicable. +# Prototype - Returns the prototype of the package/symbol/file, if applicable. +# Summary - Returns the summary of the package/symbol/file, if applicable. +# CombinedType - Returns the combined <TopicType> of the element. +# PackageSeparator - Returns the combined package separator symbol of the element. +# SortableSymbol - Returns the sortable symbol as a text string. Only available after calling <MakeSortableSymbol()>. +# IgnoredPrefix - Returns the part of the symbol that was stripped off to make the <SortableSymbol()>, or undef if none. +# Only available after calling <MakeSortableSymbol()>. +# + +# Function: HasMultiplePackages +# Returns whether <Packages()> is broken out into more elements. +sub HasMultiplePackages + { return ref($_[0]->[PACKAGE]); }; + +# Function: HasMultipleFiles +# Returns whether <File()> is broken out into more elements. +sub HasMultipleFiles + { return ref($_[0]->[FILE]); }; + + + + + + +############################################################################### +# Group: Support Functions + +# +# Function: MergeFile +# +# Adds another definition of the same package/symbol. Perhaps the file is different. +# +# Parameters: +# +# file - The package/symbol's definition file. +# type - The package/symbol's <TopicType>. +# prototype - The package/symbol's protoype if applicable. +# summary - The package/symbol's summary if applicable. +# +sub MergeFile #(file, type, prototype, summary) + { + my ($self, $file, $type, $prototype, $summary) = @_; + + # If there's only one file... + if (!$self->HasMultipleFiles()) + { + # If there's one file and it's the different from the new one... + if ($file ne $self->File()) + { + my $selfDefinition = NaturalDocs::SymbolTable::IndexElement->New(undef, undef, $self->File(), $self->Type(), + $self->Prototype(), $self->Summary(), + $self->CombinedType(), + $self->PackageSeparator()); + my $newDefinition = NaturalDocs::SymbolTable::IndexElement->New(undef, undef, $file, $type, $prototype, + $summary); + + $self->[FILE] = [ $selfDefinition, $newDefinition ]; + $self->[TYPE] = undef; + $self->[PROTOTYPE] = undef; + $self->[SUMMARY] = undef; + + if ($newDefinition->Type() ne $self->CombinedType()) + { $self->[COMBINED_TYPE] = ::TOPIC_GENERAL(); }; + if ($newDefinition->PackageSeparator() ne $self->PackageSeparator()) + { $self->[PACKAGE_SEPARATOR] = '.'; }; + } + + # If the file was the same, just ignore the duplicate in the index. + } + + # If there's more than one file... + else + { + # See if the new file is one of them. + my $files = $self->File(); + + foreach my $testElement (@$files) + { + if ($testElement->File() eq $file) + { + # If the new file's already in the index, ignore the duplicate. + return; + }; + }; + + my $newDefinition = NaturalDocs::SymbolTable::IndexElement->New(undef, undef, $file, $type, $prototype, + $summary); + push @{$self->[FILE]}, $newDefinition; + + if ($newDefinition->Type() ne $self->CombinedType()) + { $self->[COMBINED_TYPE] = ::TOPIC_GENERAL(); }; + if ($newDefinition->PackageSeparator() ne $self->PackageSeparator()) + { $self->[PACKAGE_SEPARATOR] = '.'; }; + }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/SymbolTable/Reference.pm b/docs/tool/Modules/NaturalDocs/SymbolTable/Reference.pm new file mode 100644 index 00000000..ed8c3059 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/SymbolTable/Reference.pm @@ -0,0 +1,273 @@ +############################################################################### +# +# Package: NaturalDocs::SymbolTable::Reference +# +############################################################################### +# +# A class representing a symbol or a potential symbol. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::SymbolTable::Reference; + + +############################################################################### +# Group: Implementation + +# +# Constants: Members +# +# The class is implemented as a blessed arrayref. The following constants are its members. +# +# DEFINITIONS - An existence hashref of the <FileNames> that define this reference. +# INTERPRETATIONS - A hashref of the possible interpretations of this reference. The keys are the <SymbolStrings> +# and the values are the scores. +# CURRENT_INTERPRETATION - The interpretation currently used as the reference target. It will be the interpretation with +# the highest score that is actually defined. If none are defined, this item will be undef. +# + +# DEPENDENCY: New() depends on the order of these constants. If they change, New() has to be updated. +use constant DEFINITIONS => 0; +use constant INTERPRETATIONS => 1; +use constant CURRENT_INTERPRETATION => 2; + + +############################################################################### +# Group: Modification Functions + + +# +# Function: New +# +# Creates and returns a new object. +# +sub New + { + my $package = shift; + + # Let's make it safe, since normally you can pass values to New. Having them just be ignored would be an obscure error. + if (scalar @_) + { die "You can't pass values to NaturalDocs::SymbolTable::Reference->New()\n"; }; + + # DEPENDENCY: This code depends on the order of the member constants. + my $object = [ { }, { }, undef ]; + bless $object, $package; + + return $object; + }; + + +# +# Function: AddDefinition +# +# Adds a reference definition. +# +# Parameters: +# +# file - The <FileName> that defines the reference. +# +sub AddDefinition #(file) + { + my ($self, $file) = @_; + + $self->[DEFINITIONS]{$file} = 1; + }; + + +# +# Function: DeleteDefinition +# +# Removes a reference definition. +# +# Parameters: +# +# file - The <FileName> which has the definition to delete. +# +sub DeleteDefinition #(file) + { + my ($self, $file) = @_; + + delete $self->[DEFINITIONS]{$file}; + }; + + +# +# Function: AddInterpretation +# +# Adds a symbol that this reference can be interpreted as. +# +# Parameters: +# +# symbol - The <SymbolString>. +# score - The score of this interpretation. +# +sub AddInterpretation #(symbol, score) + { + my ($self, $symbol, $score) = @_; + + $self->[INTERPRETATIONS]{$symbol} = $score; + }; + + +# +# Function: DeleteInterpretation +# +# Deletes a symbol that this reference can be interpreted as. +# +# Parameters: +# +# symbol - The <SymbolString> to delete. +# +sub DeleteInterpretation #(symbol) + { + my ($self, $symbol) = @_; + + delete $self->[INTERPRETATIONS]{$symbol}; + }; + + +# +# Function: DeleteAllInterpretationsButCurrent +# +# Deletes all interpretations except for the current one. +# +sub DeleteAllInterpretationsButCurrent + { + my $self = shift; + + if ($self->HasCurrentInterpretation()) + { + my $score = $self->CurrentScore(); + + # Fastest way to clear a hash except for one item? Make a new hash with just that item. + %{$self->[INTERPRETATIONS]} = ( $self->[CURRENT_INTERPRETATION] => $score ); + }; + }; + + +# +# Function: SetCurrentInterpretation +# +# Changes the current interpretation. The new one must already have been added via <AddInterpretation()>. +# +# Parameters: +# +# symbol - The <SymbolString>l to make the current interpretation. Can be set to undef to clear it. +# +sub SetCurrentInterpretation #(symbol) + { + my ($self, $symbol) = @_; + + $self->[CURRENT_INTERPRETATION] = $symbol; + }; + + +############################################################################### +# Group: Information Functions + + +# +# Function: Definitions +# +# Returns an array of all the <FileNames> that define this reference. If none do, returns an empty array. +# +sub Definitions + { + return keys %{$_[0]->[DEFINITIONS]}; + }; + + +# +# Function: IsDefined +# +# Returns whether the reference has any definitions or not. +# +sub IsDefined + { + return scalar keys %{$_[0]->[DEFINITIONS]}; + }; + + +# +# Function: IsDefinedIn +# +# Returns whether the reference is defined in the passed <FileName>. +# +sub IsDefinedIn #(file) + { + my ($self, $file) = @_; + + return exists $self->[DEFINITIONS]{$file}; + }; + + +# +# Function: Interpretations +# +# Returns an array of all the <SymbolStrings> that this reference can be interpreted as. If none, returns an empty array. +# +sub Interpretations + { + return keys %{$_[0]->[INTERPRETATIONS]}; + }; + + +# +# Function: InterpretationsAndScores +# +# Returns a hash of all the <SymbolStrings> that this reference can be interpreted as and their scores. The keys are the <SymbolStrings> +# and the values are the scores. If none, returns an empty hash. +# +sub InterpretationsAndScores + { + return %{$_[0]->[INTERPRETATIONS]}; + }; + + +# +# Function: HasCurrentInterpretation +# +# Returns whether the reference has a current interpretation or not. +# +sub HasCurrentInterpretation + { + return defined $_[0]->[CURRENT_INTERPRETATION]; + }; + + +# +# Function: CurrentInterpretation +# +# Returns the <SymbolString> of the current interpretation, or undef if none. +# +sub CurrentInterpretation + { + return $_[0]->[CURRENT_INTERPRETATION]; + }; + + +# +# Function: CurrentScore +# +# Returns the score of the current interpretation, or undef if none. +# +sub CurrentScore + { + my $self = shift; + + if (defined $self->[CURRENT_INTERPRETATION]) + { + return $self->[INTERPRETATIONS]{ $self->[CURRENT_INTERPRETATION] }; + } + else + { return undef; }; + }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/SymbolTable/ReferenceTarget.pm b/docs/tool/Modules/NaturalDocs/SymbolTable/ReferenceTarget.pm new file mode 100644 index 00000000..a1b2b0a5 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/SymbolTable/ReferenceTarget.pm @@ -0,0 +1,97 @@ +############################################################################### +# +# Class: NaturalDocs::SymbolTable::ReferenceTarget +# +############################################################################### +# +# A class for storing information about a reference target. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::SymbolTable::ReferenceTarget; + + +############################################################################### +# Group: Implementation + +# +# Constants: Members +# +# The class is implemented as a blessed arrayref. The following constants are its members. +# +# SYMBOL - The target <SymbolString>. +# FILE - The <FileName> the target is defined in. +# TYPE - The target <TopicType>. +# PROTOTYPE - The target's prototype, or undef if none. +# SUMMARY - The target's summary, or undef if none. +# + +# DEPENDENCY: New() depends on the order of these constants. If they change, New() has to be updated. +use constant SYMBOL => 0; +use constant FILE => 1; +use constant TYPE => 2; +use constant PROTOTYPE => 3; +use constant SUMMARY => 4; + +############################################################################### +# Group: Functions + + +# +# Function: New +# +# Creates and returns a new object. +# +# Parameters: +# +# symbol - The target <SymbolString>. +# file - The <FileName> the target is defined in. +# type - The <TopicType> of the target symbol. +# prototype - The target's prototype. Set to undef if not defined or not applicable. +# summary - The target's summary. Set to undef if not defined or not applicable. +# +sub New #(symbol, file, type, prototype, summary) + { + # DEPENDENCY: This code depends on the order of the member constants. + + my $package = shift; + + my $object = [ @_ ]; + bless $object, $package; + + return $object; + }; + + +# Function: Symbol +# Returns the target's <SymbolString>. +sub Symbol + { return $_[0]->[SYMBOL]; }; + +# Function: File +# Returns the <FileName> the target is defined in. +sub File + { return $_[0]->[FILE]; }; + +# Function: Type +# Returns the target's <TopicType>. +sub Type + { return $_[0]->[TYPE]; }; + +# Function: Prototype +# Returns the target's prototype, or undef if not defined or not applicable. +sub Prototype + { return $_[0]->[PROTOTYPE]; }; + +# Function: Summary +# Returns the target's summary, or undef if not defined or not applicable. +sub Summary + { return $_[0]->[SUMMARY]; }; + +1; diff --git a/docs/tool/Modules/NaturalDocs/SymbolTable/Symbol.pm b/docs/tool/Modules/NaturalDocs/SymbolTable/Symbol.pm new file mode 100644 index 00000000..f12858df --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/SymbolTable/Symbol.pm @@ -0,0 +1,428 @@ +############################################################################### +# +# Package: NaturalDocs::SymbolTable::Symbol +# +############################################################################### +# +# A class representing a symbol or a potential symbol. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::SymbolTable::Symbol; + + +############################################################################### +# Group: Implementation + +# +# Constants: Members +# +# The class is implemented as a blessed arrayref. The following constants are its members. +# +# DEFINITIONS - A hashref of all the files which define this symbol. The keys are the <FileNames>, and the values are +# <NaturalDocs::SymbolTable::SymbolDefinition> objects. If no files define this symbol, this item will +# be undef. +# GLOBAL_DEFINITION - The <FileName> which defines the global version of the symbol, which is what is used if +# a file references the symbol but does not have its own definition. If there are no definitions, this +# item will be undef. +# REFERENCES - A hashref of the references that can be interpreted as this symbol. This doesn't mean these +# references necessarily are. The keys are the reference strings, and the values are the scores of +# the interpretations. If no references can be interpreted as this symbol, this item will be undef. +# +use constant DEFINITIONS => 0; +use constant GLOBAL_DEFINITION => 1; +use constant REFERENCES => 2; + + +############################################################################### +# Group: Modification Functions + +# +# Function: New +# +# Creates and returns a new object. +# +sub New + { + my $package = shift; + + # Let's make it safe, since normally you can pass values to New. Having them just be ignored would be an obscure error. + if (scalar @_) + { die "You can't pass values to NaturalDocs::SymbolTable::Symbol->New()\n"; }; + + my $object = [ undef, undef, undef ]; + bless $object, $package; + + return $object; + }; + +# +# Function: AddDefinition +# +# Adds a symbol definition. If this is the first definition for this symbol, it will become the global definition. If the definition +# already exists for the file, it will be ignored. +# +# Parameters: +# +# file - The <FileName> that defines the symbol. +# type - The <TopicType> of the definition. +# prototype - The prototype of the definition, if applicable. Undef otherwise. +# summary - The summary for the definition, if applicable. Undef otherwise. +# +# Returns: +# +# Whether this provided the first definition for this symbol. +# +sub AddDefinition #(file, type, prototype, summary) + { + my ($self, $file, $type, $prototype, $summary) = @_; + + my $isFirst; + + if (!defined $self->[DEFINITIONS]) + { + $self->[DEFINITIONS] = { }; + $self->[GLOBAL_DEFINITION] = $file; + $isFirst = 1; + }; + + if (!exists $self->[DEFINITIONS]{$file}) + { + $self->[DEFINITIONS]{$file} = NaturalDocs::SymbolTable::SymbolDefinition->New($type, $prototype, $summary); + }; + + return $isFirst; + }; + + +# +# Function: ChangeDefinition +# +# Changes the information about an existing definition. +# +# Parameters: +# +# file - The <FileName> that defines the symbol. Must exist. +# type - The new <TopicType> of the definition. +# prototype - The new prototype of the definition, if applicable. Undef otherwise. +# summary - The new summary of the definition, if applicable. Undef otherwise. +# +sub ChangeDefinition #(file, type, prototype, summary) + { + my ($self, $file, $type, $prototype, $summary) = @_; + + if (defined $self->[DEFINITIONS] && + exists $self->[DEFINITIONS]{$file}) + { + $self->[DEFINITIONS]{$file}->SetType($type); + $self->[DEFINITIONS]{$file}->SetPrototype($prototype); + $self->[DEFINITIONS]{$file}->SetSummary($summary); + }; + }; + + +# +# Function: DeleteDefinition +# +# Removes a symbol definition. If the definition served as the global definition, a new one will be selected. +# +# Parameters: +# +# file - The <FileName> which contains definition to delete. +# +# Returns: +# +# Whether that was the only definition, and the symbol is now undefined. +# +sub DeleteDefinition #(file) + { + my ($self, $file) = @_; + + # If there are no definitions... + if (!defined $self->[DEFINITIONS]) + { return undef; }; + + delete $self->[DEFINITIONS]{$file}; + + # If there are no more definitions... + if (!scalar keys %{$self->[DEFINITIONS]}) + { + $self->[DEFINITIONS] = undef; + + # If definitions was previously defined, and now is empty, we can safely assume that the global definition was just deleted + # without checking it against $file. + + $self->[GLOBAL_DEFINITION] = undef; + + return 1; + } + + # If there are more definitions and the global one was just deleted... + elsif ($self->[GLOBAL_DEFINITION] eq $file) + { + # Which one becomes global is pretty much random. + $self->[GLOBAL_DEFINITION] = (keys %{$self->[DEFINITIONS]})[0]; + return undef; + }; + }; + + +# +# Function: AddReference +# +# Adds a reference that can be interpreted as this symbol. It can be, but not necessarily is. +# +# Parameters: +# +# referenceString - The string of the reference. +# score - The score of this interpretation. +# +sub AddReference #(referenceString, score) + { + my ($self, $referenceString, $score) = @_; + + if (!defined $self->[REFERENCES]) + { $self->[REFERENCES] = { }; }; + + $self->[REFERENCES]{$referenceString} = $score; + }; + + +# +# Function: DeleteReference +# +# Deletes a reference that can be interpreted as this symbol. +# +# Parameters: +# +# referenceString - The string of the reference to delete. +# +sub DeleteReference #(referenceString) + { + my ($self, $referenceString) = @_; + + # If there are no definitions... + if (!defined $self->[REFERENCES]) + { return; }; + + delete $self->[REFERENCES]{$referenceString}; + + # If there are no more definitions... + if (!scalar keys %{$self->[REFERENCES]}) + { + $self->[REFERENCES] = undef; + }; + }; + + +# +# Function: DeleteAllReferences +# +# Removes all references that can be interpreted as this symbol. +# +sub DeleteAllReferences + { + $_[0]->[REFERENCES] = undef; + }; + + +############################################################################### +# Group: Information Functions + +# +# Function: IsDefined +# +# Returns whether the symbol is defined anywhere or not. If it's not, that means it's just a potential interpretation of a +# reference. +# +sub IsDefined + { + return defined $_[0]->[GLOBAL_DEFINITION]; + }; + +# +# Function: IsDefinedIn +# +# Returns whether the symbol is defined in the passed <FileName>. +# +sub IsDefinedIn #(file) + { + my ($self, $file) = @_; + return ($self->IsDefined() && exists $self->[DEFINITIONS]{$file}); + }; + + +# +# Function: Definitions +# +# Returns an array of all the <FileNames> that define this symbol. If none do, will return an empty array. +# +sub Definitions + { + my $self = shift; + + if ($self->IsDefined()) + { return keys %{$self->[DEFINITIONS]}; } + else + { return ( ); }; + }; + + +# +# Function: GlobalDefinition +# +# Returns the <FileName> that contains the global definition of this symbol, or undef if the symbol isn't defined. +# +sub GlobalDefinition + { + return $_[0]->[GLOBAL_DEFINITION]; + }; + + +# +# Function: TypeDefinedIn +# +# Returns the <TopicType> of the symbol defined in the passed <FileName>, or undef if it's not defined in that file. +# +sub TypeDefinedIn #(file) + { + my ($self, $file) = @_; + + if ($self->IsDefined()) + { return $self->[DEFINITIONS]{$file}->Type(); } + else + { return undef; }; + }; + + +# +# Function: GlobalType +# +# Returns the <TopicType> of the global definition, or undef if the symbol isn't defined. +# +sub GlobalType + { + my $self = shift; + + my $globalDefinition = $self->GlobalDefinition(); + + if (!defined $globalDefinition) + { return undef; } + else + { return $self->[DEFINITIONS]{$globalDefinition}->Type(); }; + }; + + +# +# Function: PrototypeDefinedIn +# +# Returns the prototype of symbol defined in the passed <FileName>, or undef if it doesn't exist or is not defined in that file. +# +sub PrototypeDefinedIn #(file) + { + my ($self, $file) = @_; + + if ($self->IsDefined()) + { return $self->[DEFINITIONS]{$file}->Prototype(); } + else + { return undef; }; + }; + + +# +# Function: GlobalPrototype +# +# Returns the prototype of the global definition. Will be undef if it doesn't exist or the symbol isn't defined. +# +sub GlobalPrototype + { + my $self = shift; + + my $globalDefinition = $self->GlobalDefinition(); + + if (!defined $globalDefinition) + { return undef; } + else + { return $self->[DEFINITIONS]{$globalDefinition}->Prototype(); }; + }; + + +# +# Function: SummaryDefinedIn +# +# Returns the summary of symbol defined in the passed <FileName>, or undef if it doesn't exist or is not defined in that file. +# +sub SummaryDefinedIn #(file) + { + my ($self, $file) = @_; + + if ($self->IsDefined()) + { return $self->[DEFINITIONS]{$file}->Summary(); } + else + { return undef; }; + }; + + +# +# Function: GlobalSummary +# +# Returns the summary of the global definition. Will be undef if it doesn't exist or the symbol isn't defined. +# +sub GlobalSummary + { + my $self = shift; + + my $globalDefinition = $self->GlobalDefinition(); + + if (!defined $globalDefinition) + { return undef; } + else + { return $self->[DEFINITIONS]{$globalDefinition}->Summary(); }; + }; + + +# +# Function: HasReferences +# +# Returns whether the symbol can be interpreted as any references. +# +sub HasReferences + { + return defined $_[0]->[REFERENCES]; + }; + +# +# Function: References +# +# Returns an array of all the reference strings that can be interpreted as this symbol. If none, will return an empty array. +# +sub References + { + if (defined $_[0]->[REFERENCES]) + { return keys %{$_[0]->[REFERENCES]}; } + else + { return ( ); }; + }; + + +# +# Function: ReferencesAndScores +# +# Returns a hash of all the references that can be interpreted as this symbol and their scores. The keys are the reference +# strings, and the values are the scores. If none, will return an empty hash. +# +sub ReferencesAndScores + { + if (defined $_[0]->[REFERENCES]) + { return %{$_[0]->[REFERENCES]}; } + else + { return ( ); }; + }; + +1; diff --git a/docs/tool/Modules/NaturalDocs/SymbolTable/SymbolDefinition.pm b/docs/tool/Modules/NaturalDocs/SymbolTable/SymbolDefinition.pm new file mode 100644 index 00000000..3992e1d1 --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/SymbolTable/SymbolDefinition.pm @@ -0,0 +1,96 @@ +############################################################################### +# +# Package: NaturalDocs::SymbolTable::SymbolDefinition +# +############################################################################### +# +# A class representing a symbol definition. This does not store the definition symbol, class, or file. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::SymbolTable::SymbolDefinition; + + +############################################################################### +# Group: Implementation + +# +# Constants: Members +# +# The class is implemented as a blessed arrayref. The following constants are its members. +# +# TYPE - The symbol <TopicType>. +# PROTOTYPE - The symbol's prototype, if applicable. Will be undef otherwise. +# SUMMARY - The symbol's summary, if applicable. Will be undef otherwise. +# +use constant TYPE => 0; +use constant PROTOTYPE => 1; +use constant SUMMARY => 2; +# New depends on the order of the constants. + + +############################################################################### +# Group: Functions + +# +# Function: New +# +# Creates and returns a new object. +# +# Parameters: +# +# type - The symbol <TopicType>. +# prototype - The symbol prototype, if applicable. Undef otherwise. +# summary - The symbol's summary, if applicable. Undef otherwise. +# +sub New #(type, prototype, summary) + { + # This depends on the parameter list being the same as the constant order. + + my $package = shift; + + my $object = [ @_ ]; + bless $object, $package; + + return $object; + }; + + +# Function: Type +# Returns the definition's <TopicType>. +sub Type + { return $_[0]->[TYPE]; }; + +# Function: SetType +# Changes the <TopicType>. +sub SetType #(type) + { $_[0]->[TYPE] = $_[1]; }; + +# Function: Prototype +# Returns the definition's prototype, or undef if it doesn't have one. +sub Prototype + { return $_[0]->[PROTOTYPE]; }; + +# Function: SetPrototype +# Changes the prototype. +sub SetPrototype #(prototype) + { $_[0]->[PROTOTYPE] = $_[1]; }; + +# Function: Summary +# Returns the definition's summary, or undef if it doesn't have one. +sub Summary + { return $_[0]->[SUMMARY]; }; + +# Function: SetSummary +# Changes the summary. +sub SetSummary #(summary) + { $_[0]->[SUMMARY] = $_[1]; }; + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Topics.pm b/docs/tool/Modules/NaturalDocs/Topics.pm new file mode 100644 index 00000000..ea42f57b --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Topics.pm @@ -0,0 +1,1319 @@ +############################################################################### +# +# Package: NaturalDocs::Topics +# +############################################################################### +# +# The topic constants and functions to convert them to and from strings used throughout the script. All constants are exported +# by default. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use Text::Wrap ( ); +use Tie::RefHash ( ); + +use strict; +use integer; + +use NaturalDocs::Topics::Type; + +package NaturalDocs::Topics; + +use base 'Exporter'; +our @EXPORT = ( 'TOPIC_GENERAL', 'TOPIC_GENERIC', 'TOPIC_GROUP', 'TOPIC_CLASS', 'TOPIC_FILE', 'TOPIC_FUNCTION', + 'TOPIC_VARIABLE', 'TOPIC_PROPERTY', 'TOPIC_TYPE', 'TOPIC_ENUMERATION', 'TOPIC_CONSTANT', + 'TOPIC_INTERFACE', 'TOPIC_EVENT', 'TOPIC_DELEGATE', 'TOPIC_SECTION' ); + + + +############################################################################### +# Group: Types + + +# +# Type: TopicType +# +# A string representing a topic type as defined in <Topics.txt>. It's format should be treated as opaque; use <MakeTopicType()> +# to get them from topic names. However, they can be compared for equality with string functions. +# + + +# +# Constants: Default TopicTypes +# +# Exported constants of the default <TopicTypes>, so you don't have to go through <TypeFromName()> every time. +# +# TOPIC_GENERAL - The general <TopicType>, which has the special meaning of none in particular. +# TOPIC_GENERIC - Generic <TopicType>. +# TOPIC_GROUP - Group <TopicType>. +# TOPIC_CLASS - Class <TopicType>. +# TOPIC_INTERFACE - Interface <TopicType>. +# TOPIC_FILE - File <TopicType>. +# TOPIC_SECTION - Section <TopicType>. +# TOPIC_FUNCTION - Function <TopicType>. +# TOPIC_VARIABLE - Variable <TopicType>. +# TOPIC_PROPERTY - Property <TopicType>. +# TOPIC_TYPE - Type <TopicType>. +# TOPIC_CONSTANT - Constant <TopicType>. +# TOPIC_ENUMERATION - Enum <TopicType>. +# TOPIC_DELEGATE - Delegate <TopicType>. +# TOPIC_EVENT - Event <TopicType>. +# +use constant TOPIC_GENERAL => 'general'; +use constant TOPIC_GENERIC => 'generic'; +use constant TOPIC_GROUP => 'group'; +use constant TOPIC_CLASS => 'class'; +use constant TOPIC_INTERFACE => 'interface'; +use constant TOPIC_FILE => 'file'; +use constant TOPIC_SECTION => 'section'; +use constant TOPIC_FUNCTION => 'function'; +use constant TOPIC_VARIABLE => 'variable'; +use constant TOPIC_PROPERTY => 'property'; +use constant TOPIC_TYPE => 'type'; +use constant TOPIC_CONSTANT => 'constant'; +use constant TOPIC_ENUMERATION => 'enumeration'; +use constant TOPIC_DELEGATE => 'delegate'; +use constant TOPIC_EVENT => 'event'; +# Dependency: The values of these constants must match what is generated by MakeTopicType(). +# Dependency: These types must be added to requiredTypeNames so that they always exist. + + + + +############################################################################### +# Group: Variables + + +# +# handle: FH_TOPICS +# +# The file handle used when writing to <Topics.txt>. +# + + +# +# hash: types +# +# A hashref that maps <TopicTypes> to <NaturalDocs::Topics::Type>s. +# +my %types; + + +# +# hash: names +# +# A hashref that maps various forms of the all-lowercase type names to <TopicTypes>. All are in the same hash because +# two names that reduce to the same thing it would cause big problems, and we need to catch that. Keys include +# +# - Topic names +# - Plural topic names +# - Alphanumeric-only topic names +# - Alphanumeric-only plural topic names +# +my %names; + + +# +# hash: keywords +# +# A hashref that maps all-lowercase keywords to their <TopicTypes>. Must not have any of the same keys as +# <pluralKeywords>. +# +my %keywords; + + +# +# hash: pluralKeywords +# +# A hashref that maps all-lowercase plural keywords to their <TopicTypes>. Must not have any of the same keys as +# <keywords>. +# +my %pluralKeywords; + + +# +# hash: indexable +# +# An existence hash of all the indexable <TopicTypes>. +# +my %indexable; + + +# +# array: requiredTypeNames +# +# An array of the <TopicType> names which are required to be defined in the main file. Are in the order they should appear +# when reformatting. +# +my @requiredTypeNames = ( 'Generic', 'Class', 'Interface', 'Section', 'File', 'Group', 'Function', 'Variable', 'Property', 'Type', + 'Constant', 'Enumeration', 'Event', 'Delegate' ); + + +# +# array: legacyTypes +# +# An array that converts the legacy topic types, which were numeric constants prior to 1.3, to the current <TopicTypes>. +# The legacy types are used as an index into the array. Note that this does not support list type values. +# +my @legacyTypes = ( TOPIC_GENERAL, TOPIC_CLASS, TOPIC_SECTION, TOPIC_FILE, TOPIC_GROUP, TOPIC_FUNCTION, + TOPIC_VARIABLE, TOPIC_GENERIC, TOPIC_TYPE, TOPIC_CONSTANT, TOPIC_PROPERTY ); + + +# +# array: mainTopicNames +# +# An array of the <TopicType> names that are defined in the main <Topics.txt>. +# +my @mainTopicNames; + + + +############################################################################### +# Group: Files + + +# +# File: Topics.txt +# +# The configuration file that defines or overrides the topic definitions for Natural Docs. One version sits in Natural Docs' +# configuration directory, and another can be in a project directory to add to or override them. +# +# > # [comments] +# +# Everything after a # symbol is ignored. +# +# Except when specifying topic names, everything below is case-insensitive. +# +# > Format: [version] +# +# Specifies the file format version of the file. +# +# +# Sections: +# +# > Ignore[d] Keyword[s]: [keyword], [keyword] ... +# > [keyword] +# > [keyword], [keyword] +# > ... +# +# Ignores the keywords so that they're not recognized as Natural Docs topics anymore. Can be specified as a list on the same +# line and/or following like a normal Keywords section. +# +# > Topic Type: [name] +# > Alter Topic Type: [name] +# +# Creates a new topic type or alters an existing one. The name can only contain <CFChars> and isn't case sensitive, although +# the original case is remembered for presentation. +# +# The name General is reserved. There are a number of default types that must be defined in the main file as well, but those +# are governed by <NaturalDocs::Topics> and are not included here. The default types can have their keywords or behaviors +# changed, though, either by editing the default file or by overriding them in the user file. +# +# Enumeration is a special type. It is indexed with Types and its definition list members are listed with Constants according +# to the rules in <Languages.txt>. +# +# +# Topic Type Sections: +# +# > Plural: [name] +# +# Specifies the plural name of the topic type. Defaults to the singular name. Has the same restrictions as the topic type +# name. +# +# > Index: [yes|no] +# +# Whether the topic type gets an index. Defaults to yes. +# +# > Scope: [normal|start|end|always global] +# +# How the topic affects scope. Defaults to normal. +# +# normal - The topic stays within the current scope. +# start - The topic starts a new scope for all the topics beneath it, like class topics. +# end - The topic resets the scope back to global for all the topics beneath it, like section topics. +# always global - The topic is defined as a global symbol, but does not change the scope for any other topics. +# +# > Class Hierarchy: [yes|no] +# +# Whether the topic is part of the class hierarchy. Defaults to no. +# +# > Page Title if First: [yes|no] +# +# Whether the title of this topic becomes the page title if it is the first topic in a file. Defaults to no. +# +# > Break Lists: [yes|no] +# +# Whether list topics should be broken into individual topics in the output. Defaults to no. +# +# > Can Group With: [topic type], [topic type], ... +# +# The list of <TopicTypes> the topic can possibly be grouped with. +# +# > [Add] Keyword[s]: +# > [keyword] +# > [keyword], [plural keyword] +# > ... +# +# A list of the topic type's keywords. Each line after the heading is the keyword and optionally its plural form. This continues +# until the next line in "keyword: value" format. "Add" isn't required. +# +# - Keywords can only have letters and numbers. No punctuation or spaces are allowed. +# - Keywords are not case sensitive. +# - Subsequent keyword sections add to the list. They don't replace it. +# - Keywords can be redefined by other keyword sections. +# +# +# Revisions: +# +# 1.3: +# +# The initial version of this file. +# + + +############################################################################### +# Group: File Functions + + +# +# Function: Load +# +# Loads both the master and the project version of <Topics.txt>. +# +sub Load + { + my $self = shift; + + # Add the special General topic type. + + $types{::TOPIC_GENERAL()} = NaturalDocs::Topics::Type->New('General', 'General', 1, ::SCOPE_NORMAL(), undef); + $names{'general'} = ::TOPIC_GENERAL(); + $indexable{::TOPIC_GENERAL()} = 1; + # There are no keywords for the general topic. + + + $self->LoadFile(1); # Main + + # Dependency: All the default topic types must be checked for existence. + + # Check to see if the required types are defined. + foreach my $name (@requiredTypeNames) + { + if (!exists $names{lc($name)}) + { NaturalDocs::ConfigFile->AddError('The ' . $name . ' topic type must be defined in the main topics file.'); }; + }; + + my $errorCount = NaturalDocs::ConfigFile->ErrorCount(); + + if ($errorCount) + { + NaturalDocs::ConfigFile->PrintErrorsAndAnnotateFile(); + NaturalDocs::Error->SoftDeath('There ' . ($errorCount == 1 ? 'is an error' : 'are ' . $errorCount . ' errors') + . ' in ' . NaturalDocs::Project->MainConfigFile('Topics.txt')); + } + + + $self->LoadFile(); # User + + $errorCount = NaturalDocs::ConfigFile->ErrorCount(); + + if ($errorCount) + { + NaturalDocs::ConfigFile->PrintErrorsAndAnnotateFile(); + NaturalDocs::Error->SoftDeath('There ' . ($errorCount == 1 ? 'is an error' : 'are ' . $errorCount . ' errors') + . ' in ' . NaturalDocs::Project->UserConfigFile('Topics.txt')); + } + }; + + +# +# Function: LoadFile +# +# Loads a particular version of <Topics.txt>. +# +# Parameters: +# +# isMain - Whether the file is the main file or not. +# +sub LoadFile #(isMain) + { + my ($self, $isMain) = @_; + + my ($file, $status); + + if ($isMain) + { + $file = NaturalDocs::Project->MainConfigFile('Topics.txt'); + $status = NaturalDocs::Project->MainConfigFileStatus('Topics.txt'); + } + else + { + $file = NaturalDocs::Project->UserConfigFile('Topics.txt'); + $status = NaturalDocs::Project->UserConfigFileStatus('Topics.txt'); + }; + + my $version; + + if ($version = NaturalDocs::ConfigFile->Open($file)) + { + # The format hasn't changed since the file was introduced. + + if ($status == ::FILE_CHANGED()) + { NaturalDocs::Project->ReparseEverything(); }; + + my ($topicTypeKeyword, $topicTypeName, $topicType, $topicTypeObject, $inKeywords, $inIgnoredKeywords); + + # Keys are topic type objects, values are unparsed strings. + my %canGroupWith; + tie %canGroupWith, 'Tie::RefHash'; + + while (my ($keyword, $value) = NaturalDocs::ConfigFile->GetLine()) + { + if ($keyword) + { + $inKeywords = 0; + $inIgnoredKeywords = 0; + }; + + if ($keyword eq 'topic type') + { + $topicTypeKeyword = $keyword; + $topicTypeName = $value; + + # Resolve conflicts and create the type if necessary. + + $topicType = $self->MakeTopicType($topicTypeName); + my $lcTopicTypeName = lc($topicTypeName); + + my $lcTopicTypeAName = $lcTopicTypeName; + $lcTopicTypeAName =~ tr/a-z0-9//cd; + + if (!NaturalDocs::ConfigFile->HasOnlyCFChars($topicTypeName)) + { + NaturalDocs::ConfigFile->AddError('Topic names can only have ' . NaturalDocs::ConfigFile->CFCharNames() . '.'); + } + elsif ($topicType eq ::TOPIC_GENERAL()) + { + NaturalDocs::ConfigFile->AddError('You cannot define a General topic type.'); + } + elsif (defined $types{$topicType} || defined $names{$lcTopicTypeName} || defined $names{$lcTopicTypeAName}) + { + NaturalDocs::ConfigFile->AddError('Topic type ' . $topicTypeName . ' is already defined or its name is too ' + . 'similar to an existing name. Use Alter Topic Type if you meant to override ' + . 'its settings.'); + } + else + { + $topicTypeObject = NaturalDocs::Topics::Type->New($topicTypeName, $topicTypeName, 1, ::SCOPE_NORMAL(), + 0, 0); + + $types{$topicType} = $topicTypeObject; + $names{$lcTopicTypeName} = $topicType; + $names{$lcTopicTypeAName} = $topicType; + + $indexable{$topicType} = 1; + + if ($isMain) + { push @mainTopicNames, $topicTypeName; }; + }; + } + + elsif ($keyword eq 'alter topic type') + { + $topicTypeKeyword = $keyword; + $topicTypeName = $value; + + # Resolve conflicts and create the type if necessary. + + $topicType = $names{lc($topicTypeName)}; + + if (!defined $topicType) + { NaturalDocs::ConfigFile->AddError('Topic type ' . $topicTypeName . ' doesn\'t exist.'); } + elsif ($topicType eq ::TOPIC_GENERAL()) + { NaturalDocs::ConfigFile->AddError('You cannot alter the General topic type.'); } + else + { + $topicTypeObject = $types{$topicType}; + }; + } + + elsif ($keyword =~ /^ignored? keywords?$/) + { + $inIgnoredKeywords = 1; + + my @ignoredKeywords = split(/ ?, ?/, lc($value)); + + foreach my $ignoredKeyword (@ignoredKeywords) + { + delete $keywords{$ignoredKeyword}; + delete $pluralKeywords{$ignoredKeyword}; + }; + } + + # We continue even if there are errors in the topic type line so that we can find any other errors in the file as well. We'd + # rather them all show up at once instead of them showing up one at a time between Natural Docs runs. So we just ignore + # the settings if $topicTypeObject is undef. + + + elsif ($keyword eq 'plural') + { + my $pluralName = $value; + my $lcPluralName = lc($pluralName); + + my $lcPluralAName = $lcPluralName; + $lcPluralAName =~ tr/a-zA-Z0-9//cd; + + if (!NaturalDocs::ConfigFile->HasOnlyCFChars($pluralName)) + { + NaturalDocs::ConfigFile->AddError('Plural names can only have ' + . NaturalDocs::ConfigFile->CFCharNames() . '.'); + } + elsif ($lcPluralAName eq 'general') + { + NaturalDocs::ConfigFile->AddError('You cannot use General as a plural name for ' . $topicTypeName . '.'); + } + elsif ( (defined $names{$lcPluralName} && $names{$lcPluralName} ne $topicType) || + (defined $names{$lcPluralAName} && $names{$lcPluralAName} ne $topicType) ) + { + NaturalDocs::ConfigFile->AddError($topicTypeName . "'s plural name, " . $pluralName + . ', is already defined or is too similar to an existing name.'); + } + + elsif (defined $topicTypeObject) + { + $topicTypeObject->SetPluralName($pluralName); + + $names{$lcPluralName} = $topicType; + $names{$lcPluralAName} = $topicType; + }; + } + + elsif ($keyword eq 'index') + { + $value = lc($value); + + if ($value eq 'yes') + { + if (defined $topicTypeObject) + { + $topicTypeObject->SetIndex(1); + $indexable{$topicType} = 1; + }; + } + elsif ($value eq 'no') + { + if (defined $topicTypeObject) + { + $topicTypeObject->SetIndex(0); + delete $indexable{$topicType}; + }; + } + else + { + NaturalDocs::ConfigFile->AddError('Index lines can only be "yes" or "no".'); + }; + } + + elsif ($keyword eq 'class hierarchy') + { + $value = lc($value); + + if ($value eq 'yes') + { + if (defined $topicTypeObject) + { $topicTypeObject->SetClassHierarchy(1); }; + } + elsif ($value eq 'no') + { + if (defined $topicTypeObject) + { $topicTypeObject->SetClassHierarchy(0); }; + } + else + { + NaturalDocs::ConfigFile->AddError('Class Hierarchy lines can only be "yes" or "no".'); + }; + } + + elsif ($keyword eq 'scope') + { + $value = lc($value); + + if ($value eq 'normal') + { + if (defined $topicTypeObject) + { $topicTypeObject->SetScope(::SCOPE_NORMAL()); }; + } + elsif ($value eq 'start') + { + if (defined $topicTypeObject) + { $topicTypeObject->SetScope(::SCOPE_START()); }; + } + elsif ($value eq 'end') + { + if (defined $topicTypeObject) + { $topicTypeObject->SetScope(::SCOPE_END()); }; + } + elsif ($value eq 'always global') + { + if (defined $topicTypeObject) + { $topicTypeObject->SetScope(::SCOPE_ALWAYS_GLOBAL()); }; + } + else + { + NaturalDocs::ConfigFile->AddError('Scope lines can only be "normal", "start", "end", or "always global".'); + }; + } + + elsif ($keyword eq 'page title if first') + { + $value = lc($value); + + if ($value eq 'yes') + { + if (defined $topicTypeObject) + { $topicTypeObject->SetPageTitleIfFirst(1); }; + } + elsif ($value eq 'no') + { + if (defined $topicTypeObject) + { $topicTypeObject->SetPageTitleIfFirst(undef); }; + } + else + { + NaturalDocs::ConfigFile->AddError('Page Title if First lines can only be "yes" or "no".'); + }; + } + + elsif ($keyword eq 'break lists') + { + $value = lc($value); + + if ($value eq 'yes') + { + if (defined $topicTypeObject) + { $topicTypeObject->SetBreakLists(1); }; + } + elsif ($value eq 'no') + { + if (defined $topicTypeObject) + { $topicTypeObject->SetBreakLists(undef); }; + } + else + { + NaturalDocs::ConfigFile->AddError('Break Lists lines can only be "yes" or "no".'); + }; + } + + elsif ($keyword eq 'can group with') + { + if (defined $topicTypeObject) + { $canGroupWith{$topicTypeObject} = lc($value); }; + } + + elsif ($keyword =~ /^(?:add )?keywords?$/) + { + $inKeywords = 1; + } + + elsif (defined $keyword) + { NaturalDocs::ConfigFile->AddError($keyword . ' is not a valid keyword.'); } + + elsif (!$inKeywords && !$inIgnoredKeywords) + { + NaturalDocs::ConfigFile->AddError('All lines in ' . $topicTypeKeyword . ' sections must begin with a keyword.'); + } + + else # No keyword but in keyword section. + { + $value = lc($value); + + if ($value =~ /^([a-z0-9 ]*[a-z0-9]) ?, ?([a-z0-9 ]+)$/) + { + my ($singular, $plural) = ($1, $2); + + if ($inIgnoredKeywords) + { + delete $keywords{$singular}; + delete $keywords{$plural}; + delete $pluralKeywords{$singular}; + delete $pluralKeywords{$plural}; + } + elsif (defined $topicTypeObject) + { + $keywords{$singular} = $topicType; + delete $pluralKeywords{$singular}; + + $pluralKeywords{$plural} = $topicType; + delete $keywords{$plural}; + }; + } + elsif ($value =~ /^[a-z0-9 ]+$/) + { + if ($inIgnoredKeywords) + { + delete $keywords{$value}; + delete $pluralKeywords{$value}; + } + elsif (defined $topicType) + { + $keywords{$value} = $topicType; + delete $pluralKeywords{$value}; + }; + } + else + { + NaturalDocs::ConfigFile->AddError('Keywords can only have letters, numbers, and spaces. ' + . 'Plurals must be separated by a comma.'); + }; + }; + }; + + NaturalDocs::ConfigFile->Close(); + + + # Parse out the Can Group With lines now that everything's defined. + + while (my ($typeObject, $value) = each %canGroupWith) + { + my @values = split(/ ?, ?/, $value); + my @types; + + foreach my $value (@values) + { + # We're just going to ignore invalid items. + if (exists $names{$value}) + { push @types, $names{$value}; }; + }; + + if (scalar @types) + { $typeObject->SetCanGroupWith(\@types); }; + }; + } + + else # couldn't open file + { + if ($isMain) + { die "Couldn't open topics file " . $file . "\n"; } + else + { NaturalDocs::Project->ReparseEverything(); }; + }; + }; + + +# +# Function: Save +# +# Saves the main and user versions of <Topics.txt>. +# +sub Save + { + my $self = shift; + + $self->SaveFile(1); # Main + $self->SaveFile(0); # User + }; + + +# +# Function: SaveFile +# +# Saves a particular version of <Topics.txt>. +# +# Parameters: +# +# isMain - Whether the file is the main file or not. +# +sub SaveFile #(isMain) + { + my ($self, $isMain) = @_; + + my $file; + + if ($isMain) + { + if (NaturalDocs::Project->MainConfigFileStatus('Topics.txt') == ::FILE_SAME()) + { return; }; + $file = NaturalDocs::Project->MainConfigFile('Topics.txt'); + } + else + { + # We have to check the main one two because this lists the topics defined in it. + if (NaturalDocs::Project->UserConfigFileStatus('Topics.txt') == ::FILE_SAME() && + NaturalDocs::Project->MainConfigFileStatus('Topics.txt') == ::FILE_SAME()) + { return; }; + $file = NaturalDocs::Project->UserConfigFile('Topics.txt'); + }; + + + # Array of topic type names in the order they appear in the file. If Alter Topic Type is used, the name will end with an asterisk. + my @topicTypeOrder; + + # Keys are topic type names, values are property hashrefs. Hashref keys are the property names, values the value. + # For keywords, the key is Keywords and the values are arrayrefs of singular and plural pairs. If no plural is defined, the entry + # will be undef. + my %properties; + + # List of ignored keywords specified as Ignore Keywords: [keyword], [keyword], ... + my @inlineIgnoredKeywords; + + # List of ignored keywords specified in [keyword], [plural keyword] lines. Done in pairs, like for regular keywords. + my @separateIgnoredKeywords; + + my $inIgnoredKeywords; + + if (NaturalDocs::ConfigFile->Open($file)) + { + # We can assume the file is valid. + + my ($keyword, $value, $topicTypeName); + + while (($keyword, $value) = NaturalDocs::ConfigFile->GetLine()) + { + $keyword = lc($keyword); + + if ($keyword eq 'topic type' || $keyword eq 'alter topic type') + { + $topicTypeName = $types{ $names{lc($value)} }->Name(); + + if ($keyword eq 'alter topic type') + { $topicTypeName .= '*'; }; + + push @topicTypeOrder, $topicTypeName; + + if (!exists $properties{$topicTypeName}) + { $properties{$topicTypeName} = { 'keywords' => [ ] }; }; + } + + elsif ($keyword eq 'plural') + { + $properties{$topicTypeName}->{$keyword} = $value; + } + + elsif ($keyword eq 'index' || + $keyword eq 'scope' || + $keyword eq 'page title if first' || + $keyword eq 'class hierarchy' || + $keyword eq 'break lists' || + $keyword eq 'can group with') + { + $properties{$topicTypeName}->{$keyword} = lc($value); + } + + elsif ($keyword =~ /^(?:add )?keywords?$/) + { + $inIgnoredKeywords = 0; + } + + elsif ($keyword =~ /^ignored? keywords?$/) + { + $inIgnoredKeywords = 1; + if ($value) + { push @inlineIgnoredKeywords, split(/ ?, ?/, $value); }; + } + + elsif (!$keyword) + { + my ($singular, $plural) = split(/ ?, ?/, lc($value)); + + if ($inIgnoredKeywords) + { push @separateIgnoredKeywords, $singular, $plural; } + else + { push @{$properties{$topicTypeName}->{'keywords'}}, $singular, $plural; }; + }; + }; + + NaturalDocs::ConfigFile->Close(); + }; + + + if (!open(FH_TOPICS, '>' . $file)) + { + # The main file may be on a shared volume or some other place the user doesn't have write access to. Since this is only to + # reformat the file, we can ignore the failure. + if ($isMain) + { return; } + else + { die "Couldn't save " . $file; }; + }; + + print FH_TOPICS 'Format: ' . NaturalDocs::Settings->TextAppVersion() . "\n\n"; + + # Remember the 80 character limit. + + if ($isMain) + { + print FH_TOPICS + "# This is the main Natural Docs topics file. If you change anything here, it\n" + . "# will apply to EVERY PROJECT you use Natural Docs on. If you'd like to\n" + . "# change something for just one project, edit the Topics.txt in its project\n" + . "# directory instead.\n"; + } + else + { + print FH_TOPICS + "# This is the Natural Docs topics file for this project. If you change anything\n" + . "# here, it will apply to THIS PROJECT ONLY. If you'd like to change something\n" + . "# for all your projects, edit the Topics.txt in Natural Docs' Config directory\n" + . "# instead.\n\n\n"; + + if (scalar @inlineIgnoredKeywords || scalar @separateIgnoredKeywords) + { + if (scalar @inlineIgnoredKeywords == 1 && !scalar @separateIgnoredKeywords) + { + print FH_TOPICS 'Ignore Keyword: ' . $inlineIgnoredKeywords[0] . "\n"; + } + else + { + print FH_TOPICS + 'Ignore Keywords: ' . join(', ', @inlineIgnoredKeywords) . "\n"; + + for (my $i = 0; $i < scalar @separateIgnoredKeywords; $i += 2) + { + print FH_TOPICS ' ' . $separateIgnoredKeywords[$i]; + + if (defined $separateIgnoredKeywords[$i + 1]) + { print FH_TOPICS ', ' . $separateIgnoredKeywords[$i + 1]; }; + + print FH_TOPICS "\n"; + }; + }; + } + else + { + print FH_TOPICS + "# If you'd like to prevent keywords from being recognized by Natural Docs, you\n" + . "# can do it like this:\n" + . "# Ignore Keywords: [keyword], [keyword], ...\n" + . "#\n" + . "# Or you can use the list syntax like how they are defined:\n" + . "# Ignore Keywords:\n" + . "# [keyword]\n" + . "# [keyword], [plural keyword]\n" + . "# ...\n"; + }; + }; + + print FH_TOPICS # [CFChars] + "\n\n" + . "#-------------------------------------------------------------------------------\n" + . "# SYNTAX:\n" + . "#\n"; + + if ($isMain) + { + print FH_TOPICS + "# Topic Type: [name]\n" + . "# Creates a new topic type. Each type gets its own index and behavior\n" + . "# settings. Its name can have letters, numbers, spaces, and these\n" + . "# charaters: - / . '\n" + . "#\n" + . "# The Enumeration type is special. It's indexed with Types but its members\n" + . "# are indexed with Constants according to the rules in Languages.txt.\n" + . "#\n" + } + else + { + print FH_TOPICS + "# Topic Type: [name]\n" + . "# Alter Topic Type: [name]\n" + . "# Creates a new topic type or alters one from the main file. Each type gets\n" + . "# its own index and behavior settings. Its name can have letters, numbers,\n" + . "# spaces, and these charaters: - / . '\n" + . "#\n"; + }; + + print FH_TOPICS + "# Plural: [name]\n" + . "# Sets the plural name of the topic type, if different.\n" + . "#\n" + . "# Keywords:\n" + . "# [keyword]\n" + . "# [keyword], [plural keyword]\n" + . "# ...\n"; + + if ($isMain) + { + print FH_TOPICS + "# Defines a list of keywords for the topic type. They may only contain\n" + . "# letters, numbers, and spaces and are not case sensitive. Plural keywords\n" + . "# are used for list topics.\n"; + } + else + { + print FH_TOPICS + "# Defines or adds to the list of keywords for the topic type. They may only\n" + . "# contain letters, numbers, and spaces and are not case sensitive. Plural\n" + . "# keywords are used for list topics. You can redefine keywords found in the\n" + . "# main topics file.\n"; + } + + print FH_TOPICS + "#\n" + . "# Index: [yes|no]\n" + . "# Whether the topics get their own index. Defaults to yes. Everything is\n" + . "# included in the general index regardless of this setting.\n" + . "#\n" + . "# Scope: [normal|start|end|always global]\n" + . "# How the topics affects scope. Defaults to normal.\n" + . "# normal - Topics stay within the current scope.\n" + . "# start - Topics start a new scope for all the topics beneath it,\n" + . "# like class topics.\n" + . "# end - Topics reset the scope back to global for all the topics\n" + . "# beneath it.\n" + . "# always global - Topics are defined as global, but do not change the scope\n" + . "# for any other topics.\n" + . "#\n" + . "# Class Hierarchy: [yes|no]\n" + . "# Whether the topics are part of the class hierarchy. Defaults to no.\n" + . "#\n" + . "# Page Title If First: [yes|no]\n" + . "# Whether the topic's title becomes the page title if it's the first one in\n" + . "# a file. Defaults to no.\n" + . "#\n" + . "# Break Lists: [yes|no]\n" + . "# Whether list topics should be broken into individual topics in the output.\n" + . "# Defaults to no.\n" + . "#\n" + . "# Can Group With: [type], [type], ...\n" + . "# Defines a list of topic types that this one can possibly be grouped with.\n" + . "# Defaults to none.\n" + . "#-------------------------------------------------------------------------------\n\n"; + + my $listToPrint; + + if ($isMain) + { + print FH_TOPICS + "# The following topics MUST be defined in this file:\n" + . "#\n"; + $listToPrint = \@requiredTypeNames; + } + else + { + print FH_TOPICS + "# The following topics are defined in the main file, if you'd like to alter\n" + . "# their behavior or add keywords:\n" + . "#\n"; + $listToPrint = \@mainTopicNames; + } + + print FH_TOPICS + Text::Wrap::wrap('# ', '# ', join(', ', @$listToPrint)) . "\n" + . "\n" + . "# If you add something that you think would be useful to other developers\n" + . "# and should be included in Natural Docs by default, please e-mail it to\n" + . "# topics [at] naturaldocs [dot] org.\n"; + + # Existence hash. We do this because we want the required ones to go first by adding them to @topicTypeOrder, but we don't + # want them to appear twice. + my %doneTopicTypes; + my ($altering, $numberOfProperties); + + if ($isMain) + { unshift @topicTypeOrder, @requiredTypeNames; }; + + my @propertyOrder = ('Plural', 'Index', 'Scope', 'Class Hierarchy', 'Page Title If First', 'Break Lists'); + + foreach my $topicType (@topicTypeOrder) + { + if (!exists $doneTopicTypes{$topicType}) + { + if (substr($topicType, -1) eq '*') + { + print FH_TOPICS "\n\n" + . 'Alter Topic Type: ' . substr($topicType, 0, -1) . "\n\n"; + + $altering = 1; + $numberOfProperties = 0; + } + else + { + print FH_TOPICS "\n\n" + . 'Topic Type: ' . $topicType . "\n\n"; + + $altering = 0; + $numberOfProperties = 0; + }; + + foreach my $property (@propertyOrder) + { + if (exists $properties{$topicType}->{lc($property)}) + { + print FH_TOPICS + ' ' . $property . ': ' . ucfirst( $properties{$topicType}->{lc($property)} ) . "\n"; + + $numberOfProperties++; + }; + }; + + if (exists $properties{$topicType}->{'can group with'}) + { + my @typeStrings = split(/ ?, ?/, lc($properties{$topicType}->{'can group with'})); + my @types; + + foreach my $typeString (@typeStrings) + { + if (exists $names{$typeString}) + { push @types, $names{$typeString}; }; + }; + + if (scalar @types) + { + for (my $i = 0; $i < scalar @types; $i++) + { + my $name = NaturalDocs::Topics->NameOfType($types[$i], 1); + + if ($i == 0) + { print FH_TOPICS ' Can Group With: ' . $name; } + else + { print FH_TOPICS ', ' . $name; }; + }; + + print FH_TOPICS "\n"; + $numberOfProperties++; + }; + }; + + if (scalar @{$properties{$topicType}->{'keywords'}}) + { + if ($numberOfProperties > 1) + { print FH_TOPICS "\n"; }; + + print FH_TOPICS + ' ' . ($altering ? 'Add ' : '') . 'Keywords:' . "\n"; + + my $keywords = $properties{$topicType}->{'keywords'}; + + for (my $i = 0; $i < scalar @$keywords; $i += 2) + { + print FH_TOPICS ' ' . $keywords->[$i]; + + if (defined $keywords->[$i + 1]) + { print FH_TOPICS ', ' . $keywords->[$i + 1]; }; + + print FH_TOPICS "\n"; + }; + }; + + $doneTopicTypes{$topicType} = 1; + }; + }; + + close(FH_TOPICS); + }; + + + +############################################################################### +# Group: Functions + + +# +# Function: KeywordInfo +# +# Returns information about a topic keyword. +# +# Parameters: +# +# keyword - The keyword, which may be plural. +# +# Returns: +# +# The array ( topicType, info, isPlural ), or an empty array if the keyword doesn't exist. +# +# topicType - The <TopicType> of the keyword. +# info - The <NaturalDocs::Topics::Type> of its type. +# isPlural - Whether the keyword was plural or not. +# +sub KeywordInfo #(keyword) + { + my ($self, $keyword) = @_; + + $keyword = lc($keyword); + + my $type = $keywords{$keyword}; + + if (defined $type) + { return ( $type, $types{$type}, undef ); }; + + $type = $pluralKeywords{$keyword}; + + if (defined $type) + { return ( $type, $types{$type}, 1 ); }; + + return ( ); + }; + + +# +# Function: NameInfo +# +# Returns information about a topic name. +# +# Parameters: +# +# name - The topic type name, which can be plural and/or alphanumeric only. +# +# Returns: +# +# The array ( topicType, info ), or an empty array if the name doesn't exist. Note that unlike <KeywordInfo()>, this +# does *not* tell you whether the name is plural or not. +# +# topicType - The <TopicType> of the name. +# info - The <NaturalDocs::Topics::Type> of the type. +# +sub NameInfo #(name) + { + my ($self, $name) = @_; + + my $type = $names{lc($name)}; + + if (defined $type) + { return ( $type, $types{$type} ); } + else + { return ( ); }; + }; + + +# +# Function: TypeInfo +# +# Returns information about a <TopicType>. +# +# Parameters: +# +# type - The <TopicType>. +# +# Returns: +# +# The <NaturalDocs::Topics::Type> of the type, or undef if it didn't exist. +# +sub TypeInfo #(type) + { + my ($self, $type) = @_; + return $types{$type}; + }; + + +# +# Function: NameOfType +# +# Returns the name of the passed <TopicType>, or undef if it doesn't exist. +# +# Parameters: +# +# topicType - The <TopicType>. +# plural - Whether to return the plural instead of the singular. +# alphanumericOnly - Whether to strips everything but alphanumeric characters out. Case isn't modified. +# +# Returns: +# +# The topic type name, according to what was specified in the parameters, or undef if it doesn't exist. +# +sub NameOfType #(topicType, plural, alphanumericOnly) + { + my ($self, $topicType, $plural, $alphanumericOnly) = @_; + + my $topicObject = $types{$topicType}; + + if (!defined $topicObject) + { return undef; }; + + my $topicName = ($plural ? $topicObject->PluralName() : $topicObject->Name()); + + if ($alphanumericOnly) + { $topicName =~ tr/a-zA-Z0-9//cd; }; + + return $topicName; + }; + + +# +# Function: TypeFromName +# +# Returns a <TopicType> for the passed topic name. +# +# Parameters: +# +# topicName - The name of the topic, which can be plural and/or alphanumeric only. +# +# Returns: +# +# The <TopicType>. It does not specify whether the name was plural or not. +# +sub TypeFromName #(topicName) + { + my ($self, $topicName) = @_; + + return $names{lc($topicName)}; + }; + + +# +# Function: IsValidType +# +# Returns whether the passed <TopicType> is defined. +# +sub IsValidType #(type) + { + my ($self, $type) = @_; + return exists $types{$type}; + }; + + +# +# Function: TypeFromLegacy +# +# Returns a <TopicType> for the passed legacy topic type integer. <TopicTypes> were changed from integer constants to +# strings in 1.3. +# +sub TypeFromLegacy #(legacyInt) + { + my ($self, $int) = @_; + return $legacyTypes[$int]; + }; + + +# +# Function: AllIndexableTypes +# +# Returns an array of all possible indexable <TopicTypes>. +# +sub AllIndexableTypes + { + my ($self) = @_; + return keys %indexable; + }; + + + +############################################################################### +# Group: Support Functions + + +# +# Function: MakeTopicType +# +# Returns a <TopicType> for the passed topic name. It does not check to see if it exists already. +# +# Parameters: +# +sub MakeTopicType #(topicName) + { + my ($self, $topicName) = @_; + + # Dependency: The values of the default topic type constants must match what is generated here. + + # Turn everything to lowercase and strip non-alphanumeric characters. + $topicName = lc($topicName); + $topicName =~ tr/a-z0-9//cd; + + return $topicName; + }; + + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Topics/Type.pm b/docs/tool/Modules/NaturalDocs/Topics/Type.pm new file mode 100644 index 00000000..b807fb1e --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Topics/Type.pm @@ -0,0 +1,151 @@ +############################################################################### +# +# Package: NaturalDocs::Topics::Type +# +############################################################################### +# +# A class storing information about a <TopicType>. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + + +package NaturalDocs::Topics::Type; + +use NaturalDocs::DefineMembers 'NAME', 'Name()', + 'PLURAL_NAME', 'PluralName()', 'SetPluralName()', + 'INDEX', 'Index()', 'SetIndex()', + 'SCOPE', 'Scope()', 'SetScope()', + 'PAGE_TITLE_IF_FIRST', 'PageTitleIfFirst()', 'SetPageTitleIfFirst()', + 'BREAK_LISTS', 'BreakLists()', 'SetBreakLists()', + 'CLASS_HIERARCHY', 'ClassHierarchy()', 'SetClassHierarchy()', + 'CAN_GROUP_WITH'; + +# Dependency: New() depends on the order of these and that there are no parent classes. + +use base 'Exporter'; +our @EXPORT = ('SCOPE_NORMAL', 'SCOPE_START', 'SCOPE_END', 'SCOPE_ALWAYS_GLOBAL'); + +# +# Constants: Members +# +# The object is implemented as a blessed arrayref, with the following constants as its indexes. +# +# NAME - The topic's name. +# PLURAL_NAME - The topic's plural name. +# INDEX - Whether the topic is indexed. +# SCOPE - The topic's <ScopeType>. +# PAGE_TITLE_IF_FIRST - Whether the topic becomes the page title if it's first in a file. +# BREAK_LISTS - Whether list topics should be broken into individual topics in the output. +# CLASS_HIERARCHY - Whether the topic is part of the class hierarchy. +# CAN_GROUP_WITH - The existence hashref of <TopicTypes> the type can be grouped with. +# + + + +############################################################################### +# Group: Types + + +# +# Constants: ScopeType +# +# The possible values for <Scope()>. +# +# SCOPE_NORMAL - The topic stays in the current scope without affecting it. +# SCOPE_START - The topic starts a scope. +# SCOPE_END - The topic ends a scope, returning it to global. +# SCOPE_ALWAYS_GLOBAL - The topic is always global, but it doesn't affect the current scope. +# +use constant SCOPE_NORMAL => 1; +use constant SCOPE_START => 2; +use constant SCOPE_END => 3; +use constant SCOPE_ALWAYS_GLOBAL => 4; + + + +############################################################################### +# Group: Functions + + +# +# Function: New +# +# Creates and returns a new object. +# +# Parameters: +# +# name - The topic name. +# pluralName - The topic's plural name. +# index - Whether the topic is indexed. +# scope - The topic's <ScopeType>. +# pageTitleIfFirst - Whether the topic becomes the page title if it's the first one in a file. +# breakLists - Whether list topics should be broken into individual topics in the output. +# +sub New #(name, pluralName, index, scope, pageTitleIfFirst, breakLists) + { + my ($self, @params) = @_; + + # Dependency: Depends on the parameter order matching the member order and that there are no parent classes. + + my $object = [ @params ]; + bless $object, $self; + + return $object; + }; + + +# +# Functions: Accessors +# +# Name - Returns the topic name. +# PluralName - Returns the topic's plural name. +# SetPluralName - Replaces the topic's plural name. +# Index - Whether the topic is indexed. +# SetIndex - Sets whether the topic is indexed. +# Scope - Returns the topic's <ScopeType>. +# SetScope - Replaces the topic's <ScopeType>. +# PageTitleIfFirst - Returns whether the topic becomes the page title if it's first in the file. +# SetPageTitleIfFirst - Sets whether the topic becomes the page title if it's first in the file. +# BreakLists - Returns whether list topics should be broken into individual topics in the output. +# SetBreakLists - Sets whether list topics should be broken into individual topics in the output. +# ClassHierarchy - Returns whether the topic is part of the class hierarchy. +# SetClassHierarchy - Sets whether the topic is part of the class hierarchy. +# + + +# +# Function: CanGroupWith +# +# Returns whether the type can be grouped with the passed <TopicType>. +# +sub CanGroupWith #(TopicType type) -> bool + { + my ($self, $type) = @_; + return ( defined $self->[CAN_GROUP_WITH] && exists $self->[CAN_GROUP_WITH]->{$type} ); + }; + + +# +# Function: SetCanGroupWith +# +# Sets the list of <TopicTypes> the type can be grouped with. +# +sub SetCanGroupWith #(TopicType[] types) + { + my ($self, $types) = @_; + + $self->[CAN_GROUP_WITH] = { }; + + foreach my $type (@$types) + { $self->[CAN_GROUP_WITH]->{$type} = 1; }; + }; + + + +1; diff --git a/docs/tool/Modules/NaturalDocs/Version.pm b/docs/tool/Modules/NaturalDocs/Version.pm new file mode 100644 index 00000000..36d2d88c --- /dev/null +++ b/docs/tool/Modules/NaturalDocs/Version.pm @@ -0,0 +1,384 @@ +############################################################################### +# +# Package: NaturalDocs::Version +# +############################################################################### +# +# A package for handling version information. What? That's right. Although it should be easy and obvious, version numbers +# need to be dealt with in a variety of formats, plus there's compatibility with older releases which handled it differently. I +# wanted to centralize the code after it started getting complicated. So there ya go. +# +############################################################################### + +# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure +# Natural Docs is licensed under the GPL + +use strict; +use integer; + +package NaturalDocs::Version; + + +############################################################################### +# Group: Functions + + +# +# Function: ToString +# +# Converts a <VersionInt> to a string. +# +sub ToString #(VersionInt version) => string + { + my ($self, $version) = @_; + + my ($major, $minor, $month, $day, $year) = $self->ToValues($version); + + if ($minor % 10 == 0) + { $minor /= 10; }; + + if ($day) + { return sprintf('Development Release %02d-%02d-%d (%d.%d base)', $month, $day, $year, $major, $minor); } + else + { return $major . '.' . $minor; }; + }; + + +# +# Function: FromString +# +# Converts a version string to a <VersionInt>. +# +sub FromString #(string string) => VersionInt + { + my ($self, $string) = @_; + + if ($string eq '1') + { + return $self->FromValues(0, 91, 0, 0, 0); # 0.91 + } + else + { + my ($major, $minor, $month, $day, $year); + + if ($string =~ /^(\d{1,2})\.(\d{1,2})$/) + { + ($major, $minor) = ($1, $2); + ($month, $day, $year) = (0, 0, 0); + } + elsif ($string =~ /^Development Release (\d{1,2})-(\d{1,2})-(\d\d\d\d) \((\d{1,2})\.(\d{1,2}) base\)$/) + { + ($month, $day, $year, $major, $minor) = ($1, $2, $3, $4, $5); + + # We have to do sanity checking because these can come from user-editable text files. The version numbers should + # already be constrained simply by being forced to have only two digits. + + if ($month > 12 || $month < 1 || $day > 31 || $day < 1 || $year > 2255 || $year < 2000) + { die 'The version string ' . $string . " doesn't have a valid date.\n"; }; + } + else + { + die 'The version string ' . $string . " isn't in a recognized format.\n"; + }; + + if (length $minor == 1) + { $minor *= 10; }; + + return $self->FromValues($major, $minor, $month, $day, $year); + }; + }; + + +# +# Function: ToTextFile +# +# Writes a <VersionInt> to a text file. +# +# Parameters: +# +# fileHandle - The handle of the file to write it to. It should be at the correct location. +# version - The <VersionInt> to write. +# +sub ToTextFile #(handle fileHandle, VersionInt version) + { + my ($self, $fileHandle, $version) = @_; + + print $fileHandle $self->ToString($version) . "\n"; + }; + + +# +# Function: FromTextFile +# +# Retrieves a <VersionInt> from a text file. +# +# Parameters: +# +# fileHandle - The handle of the file to read it from. It should be at the correct location. +# +# Returns: +# +# The <VersionInt>. +# +sub FromTextFile #(handle fileHandle) => VersionInt + { + my ($self, $fileHandle) = @_; + + my $version = <$fileHandle>; + ::XChomp(\$version); + + return $self->FromString($version); + }; + + +# +# Function: ToBinaryFile +# +# Writes a <VersionInt> to a binary file. +# +# Parameters: +# +# fileHandle - The handle of the file to write it to. It should be at the correct location. +# version - The <VersionInt> to write. +# +sub ToBinaryFile #(handle fileHandle, VersionInt version) + { + my ($self, $fileHandle, $version) = @_; + + my ($major, $minor, $month, $day, $year) = $self->ToValues($version); + + # 1.35 development releases are encoded as 1.36. Everything else is literal. + if ($day && $major == 1 && $minor == 35) + { $minor = 36; }; + + print $fileHandle pack('CC', $major, $minor); + + # Date fields didn't exist with 1.35 stable and earlier. 1.35 development releases are encoded as 1.36, so this works. + if ($major > 1 || ($major == 1 && $minor > 35)) + { + if ($day) + { $year -= 2000; }; + + print $fileHandle pack('CCC', $month, $day, $year); + }; + }; + + +# +# Function: FromBinaryFile +# +# Retrieves a <VersionInt> from a binary file. +# +# Parameters: +# +# fileHandle - The handle of the file to read it from. It should be at the correct location. +# +# Returns: +# +# The <VersionInt>. +# +sub FromBinaryFile #(handle fileHandle) => VersionInt + { + my ($self, $fileHandle) = @_; + + my ($major, $minor, $month, $day, $year); + + my $raw; + read($fileHandle, $raw, 2); + + ($major, $minor) = unpack('CC', $raw); + + # 1.35 stable is the last release without the date fields. 1.35 development releases are encoded as 1.36, so this works. + if ($major > 1 || ($major == 1 && $minor > 35)) + { + read($fileHandle, $raw, 3); + ($month, $day, $year) = unpack('CCC', $raw); + + if ($day) + { $year += 2000; }; + } + else + { ($month, $day, $year) = (0, 0, 0); }; + + # Fix the 1.35 development release special encoding. + if ($major == 1 && $minor == 36) + { $minor = 35; }; + + + return $self->FromValues($major, $minor, $month, $day, $year); + }; + + +# +# Function: ToValues +# +# Converts a <VersionInt> to the array ( major, minor, month, day, year ). The minor version will be in two digit form, so x.2 +# will return 20. The date fields will be zero for stable releases. +# +sub ToValues #(VersionInt version) => ( int, int, int, int, int ) + { + my ($self, $version) = @_; + + my $major = ($version & 0x00003F80) >> 7; + my $minor = ($version & 0x0000007F); + my $month = ($version & 0x00780000) >> 19; + my $day = ($version & 0x0007C000) >> 14; + my $year = ($version & 0x7F800000) >> 23; + + if ($year) + { $year += 2000; }; + + return ( $major, $minor, $month, $day, $year ); + }; + + +# +# Function: FromValues +# +# Returns a <VersionInt> created from the passed values. +# +# Parameters: +# +# major - The major version number. For development releases, it should be the stable version it's based off of. +# minor - The minor version number. It should always be two digits, so x.2 should pass 20. For development +# releases, it should be the stable version it's based off of. +# month - The numeric month of the development release. For stable releases it should be zero. +# day - The day of the development release. For stable releases it should be zero. +# year - The year of the development release. For stable releases it should be zero. +# +# Returns: +# +# The <VersionInt>. +# +sub FromValues #(int major, int minor, int month, int day, int year) => VersionInt + { + my ($self, $major, $minor, $month, $day, $year) = @_; + + if ($day) + { $year -= 2000; }; + + return ($major << 7) + ($minor) + ($month << 19) + ($day << 14) + ($year << 23); + }; + + +# +# Function: CheckFileFormat +# +# Checks if a file's format is compatible with the current release. +# +# - If the application is a development release or the file is from one, this only returns true if they are from the exact same +# development release. +# - If neither of them are development releases, this only returns true if the file is from a release between the minimum specified +# and the current version. If there's no minimum it just checks that it's below the current version. +# +# Parameters: +# +# fileVersion - The <VersionInt> of the file format. +# minimumVersion - The minimum <VersionInt> required of the file format. May be undef. +# +# Returns: +# +# Whether the file's format is compatible per the above rules. +# +sub CheckFileFormat #(VersionInt fileVersion, optional VersionInt minimumVersion) => bool + { + my ($self, $fileVersion, $minimumVersion) = @_; + + my $appVersion = NaturalDocs::Settings->AppVersion(); + + if ($self->IsDevelopmentRelease($appVersion) || $self->IsDevelopmentRelease($fileVersion)) + { return ($appVersion == $fileVersion); } + elsif ($minimumVersion && $fileVersion < $minimumVersion) + { return 0; } + else + { return ($fileVersion <= $appVersion); }; + }; + + +# +# Function: IsDevelopmentRelease +# +# Returns whether the passed <VersionInt> is for a development release. +# +sub IsDevelopmentRelease #(VersionInt version) => bool + { + my ($self, $version) = @_; + + # Return if any of the date fields are set. + return ($version & 0x7FFFC000); + }; + + + +############################################################################### +# Group: Implementation + +# +# About: String Format +# +# Full Releases: +# +# Full releases are in the common major.minor format. Either part can be up to two digits. The minor version is interpreted +# as decimal places, so 1.3 > 1.22. There are no leading or trailing zeroes. +# +# Development Releases: +# +# Development releases are in the format "Development Release mm-dd-yyyy (vv.vv base)" where vv.vv is the version +# number of the full release it's based off of. The month and day will have leading zeroes where applicable. Example: +# "Development Release 07-09-2006 (1.35 base)". +# +# 0.91 and Earlier: +# +# Text files from releases prior to 0.95 had a separate file format version number that was used instead of the application +# version. These were never changed between 0.85 and 0.91, so they are simply "1". Text version numbers that are "1" +# instead of "1.0" will be interpreted as 0.91. +# + +# +# About: Integer Format +# +# <VersionInts> are 32-bit values with the bit distribution below. +# +# > s yyyyyyyy mmmm ddddd vvvvvvv xxxxxxx +# > [syyy|yyyy] [ymmm|mddd] [ddvv|vvvv] [vxxx|xxxx] +# +# s - The sign bit. Always zero, so it's always interpreted as positive. +# y - The year bits if it's a development release, zero otherwise. 2000 is added to the value, so the range is from 2000 to 2255. +# m - The month bits if it's a development release, zero otherwise. +# d - The day bits if it's a development release, zero otherwise. +# v - The major version bits. For development releases, it's the last stable version it was based off of. +# x - The minor version bits. It's always stored as two decimals, so x.2 would store 20 here. For development releases, it's the +# last stable version it was based off of. +# +# It's stored with the development release date at a higher significance than the version because we want a stable release to +# always treat a development release as higher than itself, and thus not attempt to read any of the data files. I'm not tracking +# data file formats at the development release level. +# + +# +# About: Binary File Format +# +# Current: +# +# Five 8-bit unsigned values, appearing major, minor, month, day, year. Minor is always stored with two digits, so x.2 would +# store 20. Year is stored minus 2000, so 2006 is stored 6. Stable releases store zero for all the date fields. +# +# 1.35 Development Releases: +# +# 1.35-based development releases are stored the same as current releases, but with 1.36 as the version number. This is +# done so previous versions of Natural Docs that didn't include the date fields would still know it's a higher version. There is +# no actual 1.36 release. +# +# 1.35 and Earlier: +# +# Two 8-bit unsigned values, appearing major then minor. Minor is always stored with two digits, so x.2 would store 20. +# + +# +# About: Text File Format +# +# In text files, versions are the <String Format> followed by a native line break. +# + + +1; |