ddnet/docs/tool/Modules/NaturalDocs/Menu.pm
2008-08-02 08:21:29 +00:00

3407 lines
116 KiB
Perl

###############################################################################
#
# Package: NaturalDocs::Menu
#
###############################################################################
#
# A package handling the menu's contents and state.
#
# Usage and Dependencies:
#
# - The <Event Handlers> can be called by <NaturalDocs::Project> immediately.
#
# - Prior to initialization, <NaturalDocs::Project> must be initialized, and all files that have been changed must be run
# through <NaturalDocs::Parser->ParseForInformation()>.
#
# - To initialize, call <LoadAndUpdate()>. Afterwards, all other functions are available. Also, <LoadAndUpdate()> will
# call <NaturalDocs::Settings->GenerateDirectoryNames()>.
#
# - To save the changes back to disk, call <Save()>.
#
###############################################################################
# This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
# Natural Docs is licensed under the GPL
use Tie::RefHash;
use NaturalDocs::Menu::Entry;
use strict;
use integer;
package NaturalDocs::Menu;
#
# Constants: Constants
#
# MAXFILESINGROUP - The maximum number of file entries that can be present in a group before it becomes a candidate for
# sub-grouping.
# MINFILESINNEWGROUP - The minimum number of file entries that must be present in a group before it will be automatically
# created. This is *not* the number of files that must be in a group before it's deleted.
#
use constant MAXFILESINGROUP => 6;
use constant MINFILESINNEWGROUP => 3;
###############################################################################
# Group: Variables
#
# bool: hasChanged
#
# Whether the menu changed or not, regardless of why.
#
my $hasChanged;
#
# Object: menu
#
# The parsed menu file. Is stored as a <MENU_GROUP> <NaturalDocs::Menu::Entry> object, with the top-level entries being
# stored as the group's content. This is done because it makes a number of functions simpler to implement, plus it allows group
# flags to be set on the top-level. However, it is exposed externally via <Content()> as an arrayref.
#
# This structure will only contain objects for <MENU_FILE>, <MENU_GROUP>, <MENU_TEXT>, <MENU_LINK>, and
# <MENU_INDEX> entries. Other types, such as <MENU_TITLE>, are stored in variables such as <title>.
#
my $menu;
#
# hash: defaultTitlesChanged
#
# An existence hash of default titles that have changed, since <OnDefaultTitleChange()> will be called before
# <LoadAndUpdate()>. Collects them to be applied later. The keys are the <FileNames>.
#
my %defaultTitlesChanged;
#
# String: title
#
# The title of the menu.
#
my $title;
#
# String: subTitle
#
# The sub-title of the menu.
#
my $subTitle;
#
# String: footer
#
# The footer for the documentation.
#
my $footer;
#
# String: timestampText
#
# The timestamp for the documentation, stored as the final output text.
#
my $timestampText;
#
# String: timestampCode
#
# The timestamp for the documentation, storted as the symbolic code.
#
my $timestampCode;
#
# hash: indexes
#
# An existence hash of all the defined index <TopicTypes> appearing in the menu.
#
my %indexes;
#
# hash: previousIndexes
#
# An existence hash of all the index <TopicTypes> that appeared in the menu last time.
#
my %previousIndexes;
#
# hash: bannedIndexes
#
# An existence hash of all the index <TopicTypes> that the user has manually deleted, and thus should not be added back to
# the menu automatically.
#
my %bannedIndexes;
###############################################################################
# Group: Files
#
# File: Menu.txt
#
# The file used to generate the menu.
#
# Format:
#
# The file is plain text. Blank lines can appear anywhere and are ignored. Tags and their content must be completely
# contained on one line with the exception of Group's braces. All values in brackets below are encoded with entity characters.
#
# > # [comment]
#
# The file supports single-line comments via #. They can appear alone on a line or after content.
#
# > Format: [version]
# > Title: [title]
# > SubTitle: [subtitle]
# > Footer: [footer]
# > Timestamp: [timestamp code]
#
# The file format version, menu title, subtitle, footer, and timestamp are specified as above. Each can only be specified once,
# with subsequent ones being ignored. Subtitle is ignored if Title is not present. Format must be the first entry in the file. If
# it's not present, it's assumed the menu is from version 0.95 or earlier, since it was added with 1.0.
#
# The timestamp code is as follows.
#
# m - Single digit month, where applicable. January is "1".
# mm - Always double digit month. January is "01".
# mon - Short month word. January is "Jan".
# month - Long month word. January is "January".
# d - Single digit day, where applicable. 1 is "1".
# dd - Always double digit day. 1 is "01".
# day - Day with text extension. 1 is "1st".
# yy - Double digit year. 2006 is "06".
# yyyy - Four digit year. 2006 is "2006".
# year - Four digit year. 2006 is "2006".
#
# Anything else is left literal in the output.
#
# > File: [title] ([file name])
# > File: [title] (auto-title, [file name])
# > File: [title] (no auto-title, [file name])
#
# Files are specified as above. If there is only one input directory, file names are relative. Otherwise they are absolute.
# If "no auto-title" is specified, the title on the line is used. If not, the title is ignored and the
# default file title is used instead. Auto-title defaults to on, so specifying "auto-title" is for compatibility only.
#
# > Group: [title]
# > Group: [title] { ... }
#
# Groups are specified as above. If no braces are specified, the group's content is everything that follows until the end of the
# file, the next group (braced or unbraced), or the closing brace of a parent group. Group braces are the only things in this
# file that can span multiple lines.
#
# There is no limitations on where the braces can appear. The opening brace can appear after the group tag, on its own line,
# or preceding another tag on a line. Similarly, the closing brace can appear after another tag or on its own line. Being
# bitchy here would just get in the way of quick and dirty editing; the package will clean it up automatically when it writes it
# back to disk.
#
# > Text: [text]
#
# Arbitrary text is specified as above. As with other tags, everything must be contained on the same line.
#
# > Link: [URL]
# > Link: [title] ([URL])
#
# External links can be specified as above. If the titled form is not used, the URL is used as the title.
#
# > Index: [name]
# > [topic type name] Index: [name]
#
# Indexes are specified as above. The topic type names can be either singular or plural. General is assumed if not specified.
#
# > Don't Index: [topic type name]
# > Don't Index: [topic type name], [topic type name], ...
#
# The option above prevents indexes that exist but are not on the menu from being automatically added.
#
# > Data: [number]([obscured data])
#
# Used to store non-user editable data.
#
# > Data: 1([obscured: [directory name]///[input directory]])
#
# When there is more than one directory, these lines store the input directories used in the last run and their names. This
# allows menu files to be shared across machines since the names will be consistent and the directories can be used to convert
# filenames to the local machine's paths. We don't want this user-editable because they may think changing it changes the
# input directories, when it doesn't. Also, changing it without changing all the paths screws up resolving.
#
# > Data: 2([obscured: [directory name])
#
# When there is only one directory and its name is not "default", this stores the name.
#
#
# Entities:
#
# &amp; - Ampersand.
# &lparen; - Left parenthesis.
# &rparen; - Right parenthesis.
# &lbrace; - Left brace.
# &rbrace; - Right brace.
#
#
# Revisions:
#
# 1.4:
#
# - Added Timestamp property.
# - Values are now encoded with entity characters.
#
# 1.3:
#
# - File names are now relative again if there is only one input directory.
# - Data: 2(...) added.
# - Can't use synonyms like "copyright" for "footer" or "sub-title" for "subtitle".
# - "Don't Index" line now requires commas to separate them, whereas it tolerated just spaces before.
#
# 1.16:
#
# - File names are now absolute instead of relative. Prior to 1.16 only one input directory was allowed, so they could be
# relative.
# - Data keywords introduced to store input directories and their names.
#
# 1.14:
#
# - Renamed this file from NaturalDocs_Menu.txt to Menu.txt.
#
# 1.1:
#
# - Added the "don't index" line.
#
# This is also the point where indexes were automatically added and removed, so all index entries from prior revisions
# were manually added and are not guaranteed to contain anything.
#
# 1.0:
#
# - Added the format line.
# - Added the "no auto-title" attribute.
# - Changed the file entry default to auto-title.
#
# This is also the point where auto-organization and better auto-titles were introduced. All groups in prior revisions were
# manually added, with the exception of a top-level Other group where new files were automatically added if there were
# groups defined.
#
# Break in support:
#
# Releases prior to 1.0 are no longer supported. Why?
#
# - They don't have a Format: line, which is required by <NaturalDocs::ConfigFile>, although I could work around this
# if I needed to.
# - No significant number of downloads for pre-1.0 releases.
# - Code simplification. I don't have to bridge the conversion from manual-only menu organization to automatic.
#
# 0.9:
#
# - Added index entries.
#
#
# File: PreviousMenuState.nd
#
# The file used to store the previous state of the menu so as to detect changes.
#
#
# Format:
#
# > [BINARY_FORMAT]
# > [VersionInt: app version]
#
# First is the standard <BINARY_FORMAT> <VersionInt> header.
#
# > [UInt8: 0 (end group)]
# > [UInt8: MENU_FILE] [UInt8: noAutoTitle] [AString16: title] [AString16: target]
# > [UInt8: MENU_GROUP] [AString16: title]
# > [UInt8: MENU_INDEX] [AString16: title] [AString16: topic type]
# > [UInt8: MENU_LINK] [AString16: title] [AString16: url]
# > [UInt8: MENU_TEXT] [AString16: text]
#
# The first UInt8 of each following line is either zero or one of the <Menu Entry Types>. What follows is contextual.
#
# There are no entries for title, subtitle, or footer. Only the entries present in <menu>.
#
# See Also:
#
# <File Format Conventions>
#
# Dependencies:
#
# - Because the type is represented by a UInt8, the <Menu Entry Types> must all be <= 255.
#
# Revisions:
#
# 1.3:
#
# - The topic type following the <MENU_INDEX> entries were changed from UInt8s to AString16s, since <TopicTypes>
# were switched from integer constants to strings. You can still convert the old to the new via
# <NaturalDocs::Topics->TypeFromLegacy()>.
#
# 1.16:
#
# - The file targets are now absolute. Prior to 1.16, they were relative to the input directory since only one was allowed.
#
# 1.14:
#
# - The file was renamed from NaturalDocs.m to PreviousMenuState.nd and moved into the Data subdirectory.
#
# 1.0:
#
# - The file's format was completely redone. Prior to 1.0, the file was a text file consisting of the app version and a line
# which was a tab-separated list of the indexes present in the menu. * meant the general index.
#
# Break in support:
#
# Pre-1.0 files are no longer supported. There was no significant number of downloads for pre-1.0 releases, and this
# eliminates a separate code path for them.
#
# 0.95:
#
# - Change the file version to match the app version. Prior to 0.95, the version line was 1. Test for "1" instead of "1.0" to
# distinguish.
#
# 0.9:
#
# - The file was added to the project. Prior to 0.9, it didn't exist.
#
###############################################################################
# Group: File Functions
#
# Function: LoadAndUpdate
#
# Loads the menu file from disk and updates it. Will add, remove, rearrange, and remove auto-titling from entries as
# necessary. Will also call <NaturalDocs::Settings->GenerateDirectoryNames()>.
#
sub LoadAndUpdate
{
my ($self) = @_;
my ($inputDirectoryNames, $relativeFiles, $onlyDirectoryName) = $self->LoadMenuFile();
my $errorCount = NaturalDocs::ConfigFile->ErrorCount();
if ($errorCount)
{
NaturalDocs::ConfigFile->PrintErrorsAndAnnotateFile();
NaturalDocs::Error->SoftDeath('There ' . ($errorCount == 1 ? 'is an error' : 'are ' . $errorCount . ' errors')
. ' in ' . NaturalDocs::Project->UserConfigFile('Menu.txt'));
};
# If the menu has a timestamp and today is a different day than the last time Natural Docs was run, we have to count it as the
# menu changing.
if (defined $timestampCode)
{
my (undef, undef, undef, $currentDay, $currentMonth, $currentYear) = localtime();
my (undef, undef, undef, $lastDay, $lastMonth, $lastYear) =
localtime( (stat( NaturalDocs::Project->DataFile('PreviousMenuState.nd') ))[9] );
# This should be okay if the previous menu state file doesn't exist.
if ($currentDay != $lastDay || $currentMonth != $lastMonth || $currentYear != $lastYear)
{ $hasChanged = 1; };
};
if ($relativeFiles)
{
my $inputDirectory = $self->ResolveRelativeInputDirectories($onlyDirectoryName);
if ($onlyDirectoryName)
{ $inputDirectoryNames = { $inputDirectory => $onlyDirectoryName }; };
}
else
{ $self->ResolveInputDirectories($inputDirectoryNames); };
NaturalDocs::Settings->GenerateDirectoryNames($inputDirectoryNames);
my $filesInMenu = $self->FilesInMenu();
my ($previousMenu, $previousIndexes, $previousFiles) = $self->LoadPreviousMenuStateFile();
if (defined $previousIndexes)
{ %previousIndexes = %$previousIndexes; };
if (defined $previousFiles)
{ $self->LockUserTitleChanges($previousFiles); };
# Don't need these anymore. We keep this level of detail because it may be used more in the future.
$previousMenu = undef;
$previousFiles = undef;
$previousIndexes = undef;
# We flag title changes instead of actually performing them at this point for two reasons. First, contents of groups are still
# subject to change, which would affect the generated titles. Second, we haven't detected the sort order yet. Changing titles
# could make groups appear unalphabetized when they were beforehand.
my $updateAllTitles;
# If the menu file changed, we can't be sure which groups changed and which didn't without a comparison, which really isn't
# worth the trouble. So we regenerate all the titles instead.
if (NaturalDocs::Project->UserConfigFileStatus('Menu.txt') == ::FILE_CHANGED())
{ $updateAllTitles = 1; }
else
{ $self->FlagAutoTitleChanges(); };
# We add new files before deleting old files so their presence still affects the grouping. If we deleted old files first, it could
# throw off where to place the new ones.
$self->AutoPlaceNewFiles($filesInMenu);
my $numberRemoved = $self->RemoveDeadFiles();
$self->CheckForTrashedMenu(scalar keys %$filesInMenu, $numberRemoved);
# Don't ban indexes if they deleted Menu.txt. They may have not deleted PreviousMenuState.nd and we don't want everything
# to be banned because of it.
if (NaturalDocs::Project->UserConfigFileStatus('Menu.txt') != ::FILE_DOESNTEXIST())
{ $self->BanAndUnbanIndexes(); };
# Index groups need to be detected before adding new ones.
$self->DetectIndexGroups();
$self->AddAndRemoveIndexes();
# We wait until after new files are placed to remove dead groups because a new file may save a group.
$self->RemoveDeadGroups();
$self->CreateDirectorySubGroups();
# We detect the sort before regenerating the titles so it doesn't get thrown off by changes. However, we do it after deleting
# dead entries and moving things into subgroups because their removal may bump it into a stronger sort category (i.e.
# SORTFILESANDGROUPS instead of just SORTFILES.) New additions don't factor into the sort.
$self->DetectOrder($updateAllTitles);
$self->GenerateAutoFileTitles($updateAllTitles);
$self->ResortGroups($updateAllTitles);
# Don't need this anymore.
%defaultTitlesChanged = ( );
};
#
# Function: Save
#
# Writes the changes to the menu files.
#
sub Save
{
my ($self) = @_;
if ($hasChanged)
{
$self->SaveMenuFile();
$self->SavePreviousMenuStateFile();
};
};
###############################################################################
# Group: Information Functions
#
# Function: HasChanged
#
# Returns whether the menu has changed or not.
#
sub HasChanged
{ return $hasChanged; };
#
# Function: Content
#
# Returns the parsed menu as an arrayref of <NaturalDocs::Menu::Entry> objects. Do not change the arrayref.
#
# The arrayref will only contain <MENU_FILE>, <MENU_GROUP>, <MENU_INDEX>, <MENU_TEXT>, and <MENU_LINK>
# entries. Entries such as <MENU_TITLE> are parsed out and are only accessible via functions such as <Title()>.
#
sub Content
{ return $menu->GroupContent(); };
#
# Function: Title
#
# Returns the title of the menu, or undef if none.
#
sub Title
{ return $title; };
#
# Function: SubTitle
#
# Returns the sub-title of the menu, or undef if none.
#
sub SubTitle
{ return $subTitle; };
#
# Function: Footer
#
# Returns the footer of the documentation, or undef if none.
#
sub Footer
{ return $footer; };
#
# Function: TimeStamp
#
# Returns the timestamp text of the documentation, or undef if none.
#
sub TimeStamp
{ return $timestampText; };
#
# Function: Indexes
#
# Returns an existence hashref of all the index <TopicTypes> appearing in the menu. Do not change the hashref.
#
sub Indexes
{ return \%indexes; };
#
# Function: PreviousIndexes
#
# Returns an existence hashref of all the index <TopicTypes> that previously appeared in the menu. Do not change the
# hashref.
#
sub PreviousIndexes
{ return \%previousIndexes; };
#
# Function: FilesInMenu
#
# Returns a hashref of all the files present in the menu. The keys are the <FileNames>, and the values are references to their
# <NaturalDocs::Menu::Entry> objects.
#
sub FilesInMenu
{
my ($self) = @_;
my @groupStack = ( $menu );
my $filesInMenu = { };
while (scalar @groupStack)
{
my $currentGroup = pop @groupStack;
my $currentGroupContent = $currentGroup->GroupContent();
foreach my $entry (@$currentGroupContent)
{
if ($entry->Type() == ::MENU_GROUP())
{ push @groupStack, $entry; }
elsif ($entry->Type() == ::MENU_FILE())
{ $filesInMenu->{ $entry->Target() } = $entry; };
};
};
return $filesInMenu;
};
###############################################################################
# Group: Event Handlers
#
# These functions are called by <NaturalDocs::Project> only. You don't need to worry about calling them. For example, when
# changing the default menu title of a file, you only need to call <NaturalDocs::Project->SetDefaultMenuTitle()>. That function
# will handle calling <OnDefaultTitleChange()>.
#
# Function: OnDefaultTitleChange
#
# Called by <NaturalDocs::Project> if the default menu title of a source file has changed.
#
# Parameters:
#
# file - The source <FileName> that had its default menu title changed.
#
sub OnDefaultTitleChange #(file)
{
my ($self, $file) = @_;
# Collect them for later. We'll deal with them in LoadAndUpdate().
$defaultTitlesChanged{$file} = 1;
};
###############################################################################
# Group: Support Functions
#
# Function: LoadMenuFile
#
# Loads and parses the menu file <Menu.txt>. This will fill <menu>, <title>, <subTitle>, <footer>, <timestampText>,
# <timestampCode>, <indexes>, and <bannedIndexes>. If there are any errors in the file, they will be recorded with
# <NaturalDocs::ConfigFile->AddError()>.
#
# Returns:
#
# The array ( inputDirectories, relativeFiles, onlyDirectoryName ) or an empty array if the file doesn't exist.
#
# inputDirectories - A hashref of all the input directories and their names stored in the menu file. The keys are the
# directories and the values are their names. Undef if none.
# relativeFiles - Whether the menu uses relative file names.
# onlyDirectoryName - The name of the input directory if there is only one.
#
sub LoadMenuFile
{
my ($self) = @_;
my $inputDirectories = { };
my $relativeFiles;
my $onlyDirectoryName;
# A stack of Menu::Entry object references as we move through the groups.
my @groupStack;
$menu = NaturalDocs::Menu::Entry->New(::MENU_GROUP(), undef, undef, undef);
my $currentGroup = $menu;
# Whether we're currently in a braceless group, since we'd have to find the implied end rather than an explicit one.
my $inBracelessGroup;
# Whether we're right after a group token, which is the only place there can be an opening brace.
my $afterGroupToken;
my $version;
if ($version = NaturalDocs::ConfigFile->Open(NaturalDocs::Project->UserConfigFile('Menu.txt'), 1))
{
# We don't check if the menu file is from a future version because we can't just throw it out and regenerate it like we can
# with other data files. So we just keep going regardless. Any syntactic differences will show up as errors.
while (my ($keyword, $value, $comment) = NaturalDocs::ConfigFile->GetLine())
{
# Check for an opening brace after a group token. This has to be separate from the rest of the code because the flag
# needs to be reset after every line.
if ($afterGroupToken)
{
$afterGroupToken = undef;
if ($keyword eq '{')
{
$inBracelessGroup = undef;
next;
}
else
{ $inBracelessGroup = 1; };
};
# Now on to the real code.
if ($keyword eq 'file')
{
my $flags = 0;
if ($value =~ /^(.+)\(([^\(]+)\)$/)
{
my ($title, $file) = ($1, $2);
$title =~ s/ +$//;
# Check for auto-title modifier.
if ($file =~ /^((?:no )?auto-title, ?)(.+)$/i)
{
my $modifier;
($modifier, $file) = ($1, $2);
if ($modifier =~ /^no/i)
{ $flags |= ::MENU_FILE_NOAUTOTITLE(); };
};
my $entry = NaturalDocs::Menu::Entry->New(::MENU_FILE(), $self->RestoreAmpChars($title),
$self->RestoreAmpChars($file), $flags);
$currentGroup->PushToGroup($entry);
}
else
{ NaturalDocs::ConfigFile->AddError('File lines must be in the format "File: [title] ([location])"'); };
}
elsif ($keyword eq 'group')
{
# End a braceless group, if we were in one.
if ($inBracelessGroup)
{
$currentGroup = pop @groupStack;
$inBracelessGroup = undef;
};
my $entry = NaturalDocs::Menu::Entry->New(::MENU_GROUP(), $self->RestoreAmpChars($value), undef, undef);
$currentGroup->PushToGroup($entry);
push @groupStack, $currentGroup;
$currentGroup = $entry;
$afterGroupToken = 1;
}
elsif ($keyword eq '{')
{
NaturalDocs::ConfigFile->AddError('Opening braces are only allowed after Group tags.');
}
elsif ($keyword eq '}')
{
# End a braceless group, if we were in one.
if ($inBracelessGroup)
{
$currentGroup = pop @groupStack;
$inBracelessGroup = undef;
};
# End a braced group too.
if (scalar @groupStack)
{ $currentGroup = pop @groupStack; }
else
{ NaturalDocs::ConfigFile->AddError('Unmatched closing brace.'); };
}
elsif ($keyword eq 'title')
{
if (!defined $title)
{ $title = $self->RestoreAmpChars($value); }
else
{ NaturalDocs::ConfigFile->AddError('Title can only be defined once.'); };
}
elsif ($keyword eq 'subtitle')
{
if (defined $title)
{
if (!defined $subTitle)
{ $subTitle = $self->RestoreAmpChars($value); }
else
{ NaturalDocs::ConfigFile->AddError('SubTitle can only be defined once.'); };
}
else
{ NaturalDocs::ConfigFile->AddError('Title must be defined before SubTitle.'); };
}
elsif ($keyword eq 'footer')
{
if (!defined $footer)
{ $footer = $self->RestoreAmpChars($value); }
else
{ NaturalDocs::ConfigFile->AddError('Footer can only be defined once.'); };
}
elsif ($keyword eq 'timestamp')
{
if (!defined $timestampCode)
{
$timestampCode = $self->RestoreAmpChars($value);
$self->GenerateTimestampText();
}
else
{ NaturalDocs::ConfigFile->AddError('Timestamp can only be defined once.'); };
}
elsif ($keyword eq 'text')
{
$currentGroup->PushToGroup( NaturalDocs::Menu::Entry->New(::MENU_TEXT(), $self->RestoreAmpChars($value),
undef, undef) );
}
elsif ($keyword eq 'link')
{
my ($title, $url);
if ($value =~ /^([^\(\)]+?) ?\(([^\)]+)\)$/)
{
($title, $url) = ($1, $2);
}
elsif (defined $comment)
{
$value .= $comment;
if ($value =~ /^([^\(\)]+?) ?\(([^\)]+)\) ?(?:#.*)?$/)
{
($title, $url) = ($1, $2);
};
};
if ($title)
{
$currentGroup->PushToGroup( NaturalDocs::Menu::Entry->New(::MENU_LINK(), $self->RestoreAmpChars($title),
$self->RestoreAmpChars($url), undef) );
}
else
{ NaturalDocs::ConfigFile->AddError('Link lines must be in the format "Link: [title] ([url])"'); };
}
elsif ($keyword eq 'data')
{
$value =~ /^(\d)\((.*)\)$/;
my ($number, $data) = ($1, $2);
$data = NaturalDocs::ConfigFile->Unobscure($data);
# The input directory naming convention changed with version 1.32, but NaturalDocs::Settings will handle that
# automatically.
if ($number == 1)
{
my ($dirName, $inputDir) = split(/\/\/\//, $data, 2);
$inputDirectories->{$inputDir} = $dirName;
}
elsif ($number == 2)
{ $onlyDirectoryName = $data; };
# Ignore other numbers because it may be from a future format and we don't want to make the user delete it
# manually.
}
elsif ($keyword eq "don't index")
{
my @indexes = split(/, ?/, $value);
foreach my $index (@indexes)
{
my $indexType = NaturalDocs::Topics->TypeFromName( $self->RestoreAmpChars($index) );
if (defined $indexType)
{ $bannedIndexes{$indexType} = 1; };
};
}
elsif ($keyword eq 'index')
{
my $entry = NaturalDocs::Menu::Entry->New(::MENU_INDEX(), $self->RestoreAmpChars($value),
::TOPIC_GENERAL(), undef);
$currentGroup->PushToGroup($entry);
$indexes{::TOPIC_GENERAL()} = 1;
}
elsif (substr($keyword, -6) eq ' index')
{
my $index = substr($keyword, 0, -6);
my ($indexType, $indexInfo) = NaturalDocs::Topics->NameInfo( $self->RestoreAmpChars($index) );
if (defined $indexType)
{
if ($indexInfo->Index())
{
$indexes{$indexType} = 1;
$currentGroup->PushToGroup(
NaturalDocs::Menu::Entry->New(::MENU_INDEX(), $self->RestoreAmpChars($value), $indexType, undef) );
}
else
{
# If it's on the menu but isn't indexable, the topic setting may have changed out from under it.
$hasChanged = 1;
};
}
else
{
NaturalDocs::ConfigFile->AddError($index . ' is not a valid index type.');
};
}
else
{
NaturalDocs::ConfigFile->AddError(ucfirst($keyword) . ' is not a valid keyword.');
};
};
# End a braceless group, if we were in one.
if ($inBracelessGroup)
{
$currentGroup = pop @groupStack;
$inBracelessGroup = undef;
};
# Close up all open groups.
my $openGroups = 0;
while (scalar @groupStack)
{
$currentGroup = pop @groupStack;
$openGroups++;
};
if ($openGroups == 1)
{ NaturalDocs::ConfigFile->AddError('There is an unclosed group.'); }
elsif ($openGroups > 1)
{ NaturalDocs::ConfigFile->AddError('There are ' . $openGroups . ' unclosed groups.'); };
if (!scalar keys %$inputDirectories)
{
$inputDirectories = undef;
$relativeFiles = 1;
};
NaturalDocs::ConfigFile->Close();
return ($inputDirectories, $relativeFiles, $onlyDirectoryName);
}
else
{ return ( ); };
};
#
# Function: SaveMenuFile
#
# Saves the current menu to <Menu.txt>.
#
sub SaveMenuFile
{
my ($self) = @_;
open(MENUFILEHANDLE, '>' . NaturalDocs::Project->UserConfigFile('Menu.txt'))
or die "Couldn't save menu file " . NaturalDocs::Project->UserConfigFile('Menu.txt') . "\n";
print MENUFILEHANDLE
"Format: " . NaturalDocs::Settings->TextAppVersion() . "\n\n\n";
my $inputDirs = NaturalDocs::Settings->InputDirectories();
if (defined $title)
{
print MENUFILEHANDLE 'Title: ' . $self->ConvertAmpChars($title) . "\n";
if (defined $subTitle)
{
print MENUFILEHANDLE 'SubTitle: ' . $self->ConvertAmpChars($subTitle) . "\n";
}
else
{
print MENUFILEHANDLE
"\n"
. "# You can also add a sub-title to your menu like this:\n"
. "# SubTitle: [subtitle]\n";
};
}
else
{
print MENUFILEHANDLE
"# You can add a title and sub-title to your menu like this:\n"
. "# Title: [project name]\n"
. "# SubTitle: [subtitle]\n";
};
print MENUFILEHANDLE "\n";
if (defined $footer)
{
print MENUFILEHANDLE 'Footer: ' . $self->ConvertAmpChars($footer) . "\n";
}
else
{
print MENUFILEHANDLE
"# You can add a footer to your documentation like this:\n"
. "# Footer: [text]\n"
. "# If you want to add a copyright notice, this would be the place to do it.\n";
};
if (defined $timestampCode)
{
print MENUFILEHANDLE 'Timestamp: ' . $self->ConvertAmpChars($timestampCode) . "\n";
}
else
{
print MENUFILEHANDLE
"\n"
. "# You can add a timestamp to your documentation like one of these:\n"
. "# Timestamp: Generated on month day, year\n"
. "# Timestamp: Updated mm/dd/yyyy\n"
. "# Timestamp: Last updated mon day\n"
. "#\n";
};
print MENUFILEHANDLE
qq{# m - One or two digit month. January is "1"\n}
. qq{# mm - Always two digit month. January is "01"\n}
. qq{# mon - Short month word. January is "Jan"\n}
. qq{# month - Long month word. January is "January"\n}
. qq{# d - One or two digit day. 1 is "1"\n}
. qq{# dd - Always two digit day. 1 is "01"\n}
. qq{# day - Day with letter extension. 1 is "1st"\n}
. qq{# yy - Two digit year. 2006 is "06"\n}
. qq{# yyyy - Four digit year. 2006 is "2006"\n}
. qq{# year - Four digit year. 2006 is "2006"\n}
. "\n";
if (scalar keys %bannedIndexes)
{
print MENUFILEHANDLE
"# These are indexes you deleted, so Natural Docs will not add them again\n"
. "# unless you remove them from this line.\n"
. "\n"
. "Don't Index: ";
my $first = 1;
foreach my $index (keys %bannedIndexes)
{
if (!$first)
{ print MENUFILEHANDLE ', '; }
else
{ $first = undef; };
print MENUFILEHANDLE $self->ConvertAmpChars( NaturalDocs::Topics->NameOfType($index, 1), CONVERT_COMMAS() );
};
print MENUFILEHANDLE "\n\n";
};
# Remember to keep lines below eighty characters.
print MENUFILEHANDLE
"\n"
. "# --------------------------------------------------------------------------\n"
. "# \n"
. "# Cut and paste the lines below to change the order in which your files\n"
. "# appear on the menu. Don't worry about adding or removing files, Natural\n"
. "# Docs will take care of that.\n"
. "# \n"
. "# You can further organize the menu by grouping the entries. Add a\n"
. "# \"Group: [name] {\" line to start a group, and add a \"}\" to end it.\n"
. "# \n"
. "# You can add text and web links to the menu by adding \"Text: [text]\" and\n"
. "# \"Link: [name] ([URL])\" lines, respectively.\n"
. "# \n"
. "# The formatting and comments are auto-generated, so don't worry about\n"
. "# neatness when editing the file. Natural Docs will clean it up the next\n"
. "# time it is run. When working with groups, just deal with the braces and\n"
. "# forget about the indentation and comments.\n"
. "# \n";
if (scalar @$inputDirs > 1)
{
print MENUFILEHANDLE
"# You can use this file on other computers even if they use different\n"
. "# directories. As long as the command line points to the same source files,\n"
. "# Natural Docs will be able to correct the locations automatically.\n"
. "# \n";
};
print MENUFILEHANDLE
"# --------------------------------------------------------------------------\n"
. "\n\n";
$self->WriteMenuEntries($menu->GroupContent(), \*MENUFILEHANDLE, undef, (scalar @$inputDirs == 1));
if (scalar @$inputDirs > 1)
{
print MENUFILEHANDLE
"\n\n##### Do not change or remove these lines. #####\n";
foreach my $inputDir (@$inputDirs)
{
print MENUFILEHANDLE
'Data: 1(' . NaturalDocs::ConfigFile->Obscure( NaturalDocs::Settings->InputDirectoryNameOf($inputDir)
. '///' . $inputDir ) . ")\n";
};
}
elsif (lc(NaturalDocs::Settings->InputDirectoryNameOf($inputDirs->[0])) != 1)
{
print MENUFILEHANDLE
"\n\n##### Do not change or remove this line. #####\n"
. 'Data: 2(' . NaturalDocs::ConfigFile->Obscure( NaturalDocs::Settings->InputDirectoryNameOf($inputDirs->[0]) ) . ")\n";
}
close(MENUFILEHANDLE);
};
#
# Function: WriteMenuEntries
#
# A recursive function to write the contents of an arrayref of <NaturalDocs::Menu::Entry> objects to disk.
#
# Parameters:
#
# entries - The arrayref of menu entries to write.
# fileHandle - The handle to the output file.
# indentChars - The indentation _characters_ to add before each line. It is not the number of characters, it is the characters
# themselves. Use undef for none.
# relativeFiles - Whether to use relative file names.
#
sub WriteMenuEntries #(entries, fileHandle, indentChars, relativeFiles)
{
my ($self, $entries, $fileHandle, $indentChars, $relativeFiles) = @_;
my $lastEntryType;
foreach my $entry (@$entries)
{
if ($entry->Type() == ::MENU_FILE())
{
my $fileName;
if ($relativeFiles)
{ $fileName = (NaturalDocs::Settings->SplitFromInputDirectory($entry->Target()))[1]; }
else
{ $fileName = $entry->Target(); };
print $fileHandle $indentChars . 'File: ' . $self->ConvertAmpChars( $entry->Title(), CONVERT_PARENTHESIS() )
. ' (' . ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE() ? 'no auto-title, ' : '')
. $self->ConvertAmpChars($fileName) . ")\n";
}
elsif ($entry->Type() == ::MENU_GROUP())
{
if (defined $lastEntryType && $lastEntryType != ::MENU_GROUP())
{ print $fileHandle "\n"; };
print $fileHandle $indentChars . 'Group: ' . $self->ConvertAmpChars( $entry->Title() ) . " {\n\n";
$self->WriteMenuEntries($entry->GroupContent(), $fileHandle, ' ' . $indentChars, $relativeFiles);
print $fileHandle ' ' . $indentChars . '} # Group: ' . $self->ConvertAmpChars( $entry->Title() ) . "\n\n";
}
elsif ($entry->Type() == ::MENU_TEXT())
{
print $fileHandle $indentChars . 'Text: ' . $self->ConvertAmpChars( $entry->Title() ) . "\n";
}
elsif ($entry->Type() == ::MENU_LINK())
{
print $fileHandle $indentChars . 'Link: ' . $self->ConvertAmpChars( $entry->Title() ) . ' '
. '(' . $self->ConvertAmpChars( $entry->Target(), CONVERT_PARENTHESIS() ) . ')' . "\n";
}
elsif ($entry->Type() == ::MENU_INDEX())
{
my $type;
if ($entry->Target() ne ::TOPIC_GENERAL())
{
$type = NaturalDocs::Topics->NameOfType($entry->Target()) . ' ';
};
print $fileHandle $indentChars . $self->ConvertAmpChars($type, CONVERT_COLONS()) . 'Index: '
. $self->ConvertAmpChars( $entry->Title() ) . "\n";
};
$lastEntryType = $entry->Type();
};
};
#
# Function: LoadPreviousMenuStateFile
#
# Loads and parses the previous menu state file.
#
# Returns:
#
# The array ( previousMenu, previousIndexes, previousFiles ) or an empty array if there was a problem with the file.
#
# previousMenu - A <MENU_GROUP> <NaturalDocs::Menu::Entry> object, similar to <menu>, which contains the entire
# previous menu.
# previousIndexes - An existence hashref of the index <TopicTypes> present in the previous menu.
# previousFiles - A hashref of the files present in the previous menu. The keys are the <FileNames>, and the entries are
# references to its object in previousMenu.
#
sub LoadPreviousMenuStateFile
{
my ($self) = @_;
my $fileIsOkay;
my $version;
my $previousStateFileName = NaturalDocs::Project->DataFile('PreviousMenuState.nd');
if (open(PREVIOUSSTATEFILEHANDLE, '<' . $previousStateFileName))
{
# See if it's binary.
binmode(PREVIOUSSTATEFILEHANDLE);
my $firstChar;
read(PREVIOUSSTATEFILEHANDLE, $firstChar, 1);
if ($firstChar == ::BINARY_FORMAT())
{
$version = NaturalDocs::Version->FromBinaryFile(\*PREVIOUSSTATEFILEHANDLE);
# Only the topic type format has changed since switching to binary, and we support both methods.
if (NaturalDocs::Version->CheckFileFormat($version))
{ $fileIsOkay = 1; }
else
{ close(PREVIOUSSTATEFILEHANDLE); };
}
else # it's not in binary
{ close(PREVIOUSSTATEFILEHANDLE); };
};
if ($fileIsOkay)
{
if (NaturalDocs::Project->UserConfigFileStatus('Menu.txt') == ::FILE_CHANGED())
{ $hasChanged = 1; };
my $menu = NaturalDocs::Menu::Entry->New(::MENU_GROUP(), undef, undef, undef);
my $indexes = { };
my $files = { };
my @groupStack;
my $currentGroup = $menu;
my $raw;
# [UInt8: type or 0 for end group]
while (read(PREVIOUSSTATEFILEHANDLE, $raw, 1))
{
my ($type, $flags, $title, $titleLength, $target, $targetLength);
$type = unpack('C', $raw);
if ($type == 0)
{ $currentGroup = pop @groupStack; }
elsif ($type == ::MENU_FILE())
{
# [UInt8: noAutoTitle] [AString16: title] [AString16: target]
read(PREVIOUSSTATEFILEHANDLE, $raw, 3);
(my $noAutoTitle, $titleLength) = unpack('Cn', $raw);
if ($noAutoTitle)
{ $flags = ::MENU_FILE_NOAUTOTITLE(); };
read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength);
read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
$targetLength = unpack('n', $raw);
read(PREVIOUSSTATEFILEHANDLE, $target, $targetLength);
}
elsif ($type == ::MENU_GROUP())
{
# [AString16: title]
read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
$titleLength = unpack('n', $raw);
read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength);
}
elsif ($type == ::MENU_INDEX())
{
# [AString16: title]
read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
$titleLength = unpack('n', $raw);
read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength);
if ($version >= NaturalDocs::Version->FromString('1.3'))
{
# [AString16: topic type]
read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
$targetLength = unpack('n', $raw);
read(PREVIOUSSTATEFILEHANDLE, $target, $targetLength);
}
else
{
# [UInt8: topic type (0 for general)]
read(PREVIOUSSTATEFILEHANDLE, $raw, 1);
$target = unpack('C', $raw);
$target = NaturalDocs::Topics->TypeFromLegacy($target);
};
}
elsif ($type == ::MENU_LINK())
{
# [AString16: title] [AString16: url]
read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
$titleLength = unpack('n', $raw);
read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength);
read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
$targetLength = unpack('n', $raw);
read(PREVIOUSSTATEFILEHANDLE, $target, $targetLength);
}
elsif ($type == ::MENU_TEXT())
{
# [AString16: text]
read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
$titleLength = unpack('n', $raw);
read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength);
};
# The topic type of the index may have been removed.
if ( !($type == ::MENU_INDEX() && !NaturalDocs::Topics->IsValidType($target)) )
{
my $entry = NaturalDocs::Menu::Entry->New($type, $title, $target, ($flags || 0));
$currentGroup->PushToGroup($entry);
if ($type == ::MENU_FILE())
{
$files->{$target} = $entry;
}
elsif ($type == ::MENU_GROUP())
{
push @groupStack, $currentGroup;
$currentGroup = $entry;
}
elsif ($type == ::MENU_INDEX())
{
$indexes->{$target} = 1;
};
};
};
close(PREVIOUSSTATEFILEHANDLE);
return ($menu, $indexes, $files);
}
else
{
$hasChanged = 1;
return ( );
};
};
#
# Function: SavePreviousMenuStateFile
#
# Saves changes to <PreviousMenuState.nd>.
#
sub SavePreviousMenuStateFile
{
my ($self) = @_;
open (PREVIOUSSTATEFILEHANDLE, '>' . NaturalDocs::Project->DataFile('PreviousMenuState.nd'))
or die "Couldn't save " . NaturalDocs::Project->DataFile('PreviousMenuState.nd') . ".\n";
binmode(PREVIOUSSTATEFILEHANDLE);
print PREVIOUSSTATEFILEHANDLE '' . ::BINARY_FORMAT();
NaturalDocs::Version->ToBinaryFile(\*PREVIOUSSTATEFILEHANDLE, NaturalDocs::Settings->AppVersion());
$self->WritePreviousMenuStateEntries($menu->GroupContent(), \*PREVIOUSSTATEFILEHANDLE);
close(PREVIOUSSTATEFILEHANDLE);
};
#
# Function: WritePreviousMenuStateEntries
#
# A recursive function to write the contents of an arrayref of <NaturalDocs::Menu::Entry> objects to disk.
#
# Parameters:
#
# entries - The arrayref of menu entries to write.
# fileHandle - The handle to the output file.
#
sub WritePreviousMenuStateEntries #(entries, fileHandle)
{
my ($self, $entries, $fileHandle) = @_;
foreach my $entry (@$entries)
{
if ($entry->Type() == ::MENU_FILE())
{
# We need to do length manually instead of using n/A in the template because it's not supported in earlier versions
# of Perl.
# [UInt8: MENU_FILE] [UInt8: noAutoTitle] [AString16: title] [AString16: target]
print $fileHandle pack('CCnA*nA*', ::MENU_FILE(), ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE() ? 1 : 0),
length($entry->Title()), $entry->Title(),
length($entry->Target()), $entry->Target());
}
elsif ($entry->Type() == ::MENU_GROUP())
{
# [UInt8: MENU_GROUP] [AString16: title]
print $fileHandle pack('CnA*', ::MENU_GROUP(), length($entry->Title()), $entry->Title());
$self->WritePreviousMenuStateEntries($entry->GroupContent(), $fileHandle);
print $fileHandle pack('C', 0);
}
elsif ($entry->Type() == ::MENU_INDEX())
{
# [UInt8: MENU_INDEX] [AString16: title] [AString16: topic type]
print $fileHandle pack('CnA*nA*', ::MENU_INDEX(), length($entry->Title()), $entry->Title(),
length($entry->Target()), $entry->Target());
}
elsif ($entry->Type() == ::MENU_LINK())
{
# [UInt8: MENU_LINK] [AString16: title] [AString16: url]
print $fileHandle pack('CnA*nA*', ::MENU_LINK(), length($entry->Title()), $entry->Title(),
length($entry->Target()), $entry->Target());
}
elsif ($entry->Type() == ::MENU_TEXT())
{
# [UInt8: MENU_TEXT] [AString16: hext]
print $fileHandle pack('CnA*', ::MENU_TEXT(), length($entry->Title()), $entry->Title());
};
};
};
#
# Function: CheckForTrashedMenu
#
# Checks the menu to see if a significant number of file entries didn't resolve to actual files, and if so, saves a backup of the
# menu and issues a warning.
#
# Parameters:
#
# numberOriginallyInMenu - A count of how many file entries were in the menu orignally.
# numberRemoved - A count of how many file entries were removed from the menu.
#
sub CheckForTrashedMenu #(numberOriginallyInMenu, numberRemoved)
{
my ($self, $numberOriginallyInMenu, $numberRemoved) = @_;
no integer;
if ( ($numberOriginallyInMenu >= 6 && $numberRemoved == $numberOriginallyInMenu) ||
($numberOriginallyInMenu >= 12 && ($numberRemoved / $numberOriginallyInMenu) >= 0.4) ||
($numberRemoved >= 15) )
{
my $backupFile = NaturalDocs::Project->UserConfigFile('Menu_Backup.txt');
my $backupFileNumber = 1;
while (-e $backupFile)
{
$backupFileNumber++;
$backupFile = NaturalDocs::Project->UserConfigFile('Menu_Backup_' . $backupFileNumber . '.txt');
};
NaturalDocs::File->Copy( NaturalDocs::Project->UserConfigFile('Menu.txt'), $backupFile );
print STDERR
"\n"
# GNU format. See http://www.gnu.org/prep/standards_15.html
. "NaturalDocs: warning: possible trashed menu\n"
. "\n"
. " Natural Docs has detected that a significant number file entries in the\n"
. " menu did not resolve to actual files. A backup of your original menu file\n"
. " has been saved as\n"
. "\n"
. " " . $backupFile . "\n"
. "\n"
. " - If you recently deleted a lot of files from your project, you can safely\n"
. " ignore this message. They have been deleted from the menu as well.\n"
. " - If you recently rearranged your source tree, you may want to restore your\n"
. " menu from the backup and do a search and replace to preserve your layout.\n"
. " Otherwise the position of any moved files will be reset.\n"
. " - If neither of these is the case, you may have gotten the -i parameter\n"
. " wrong in the command line. You should definitely restore the backup and\n"
. " try again, because otherwise every file in your menu will be reset.\n"
. "\n";
};
use integer;
};
#
# Function: GenerateTimestampText
#
# Generates <timestampText> from <timestampCode> with the current date.
#
sub GenerateTimestampText
{
my $self = shift;
my @longMonths = ( 'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December' );
my @shortMonths = ( 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec' );
my (undef, undef, undef, $day, $month, $year) = localtime();
$year += 1900;
my $longDay;
if ($day % 10 == 1 && $day != 11)
{ $longDay = $day . 'st'; }
elsif ($day % 10 == 2 && $day != 12)
{ $longDay = $day . 'nd'; }
elsif ($day % 10 == 3 && $day != 13)
{ $longDay = $day . 'rd'; }
else
{ $longDay = $day . 'th'; };
$timestampText = $timestampCode;
$timestampText =~ s/(?<![a-z])month(?![a-z])/$longMonths[$month]/i;
$timestampText =~ s/(?<![a-z])mon(?![a-z])/$shortMonths[$month]/i;
$timestampText =~ s/(?<![a-z])mm(?![a-z])/sprintf('%02d', $month + 1)/ie;
$timestampText =~ s/(?<![a-z])m(?![a-z])/$month + 1/ie;
$timestampText =~ s/(?<![a-z])day(?![a-z])/$longDay/i;
$timestampText =~ s/(?<![a-z])dd(?![a-z])/sprintf('%02d', $day)/ie;
$timestampText =~ s/(?<![a-z])d(?![a-z])/$day/i;
$timestampText =~ s/(?<![a-z])(?:year|yyyy)(?![a-z])/$year/i;
$timestampText =~ s/(?<![a-z])(?:year|yyyy)(?![a-z])/$year/i; #XXX
$timestampText =~ s/(?<![a-z])yy(?![a-z])/sprintf('%02d', $year % 100)/ie;
};
use constant CONVERT_PARENTHESIS => 0x01;
use constant CONVERT_COMMAS => 0x02;
use constant CONVERT_COLONS => 0x04;
#
# Function: ConvertAmpChars
# Replaces certain characters in the string with their entities and returns it.
#
# Parameters:
#
# text - The text to convert.
# flags - The flags of any additional characters to convert.
#
# Flags:
#
# - CONVERT_PARENTHESIS
# - CONVERT_COMMAS
# - CONVERT_COLONS
#
# Returns:
#
# The string with the amp chars converted.
#
sub ConvertAmpChars #(string text, int flags) => string
{
my ($self, $text, $flags) = @_;
$text =~ s/&/&amp;/g;
$text =~ s/\{/&lbrace;/g;
$text =~ s/\}/&rbrace;/g;
if ($flags & CONVERT_PARENTHESIS())
{
$text =~ s/\(/&lparen;/g;
$text =~ s/\)/&rparen;/g;
};
if ($flags & CONVERT_COMMAS())
{
$text =~ s/\,/&comma;/g;
};
if ($flags & CONVERT_COLONS())
{
$text =~ s/\:/&colon;/g;
};
return $text;
};
#
# Function: RestoreAmpChars
# Replaces entity characters in the string with their original characters and returns it. This will restore all amp chars regardless
# of the flags passed to <ConvertAmpChars()>.
#
sub RestoreAmpChars #(string text) => string
{
my ($self, $text) = @_;
$text =~ s/&lparen;/(/gi;
$text =~ s/&rparen;/)/gi;
$text =~ s/&lbrace;/{/gi;
$text =~ s/&rbrace;/}/gi;
$text =~ s/&comma;/,/gi;
$text =~ s/&amp;/&/gi;
$text =~ s/&colon;/:/gi;
return $text;
};
###############################################################################
# Group: Auto-Adjustment Functions
#
# Function: ResolveInputDirectories
#
# Detects if the input directories in the menu file match those in the command line, and if not, tries to resolve them. This allows
# menu files to work across machines, since the absolute paths won't be the same but the relative ones should be.
#
# Parameters:
#
# inputDirectoryNames - A hashref of the input directories appearing in the menu file, or undef if none. The keys are the
# directories, and the values are their names. May be undef.
#
sub ResolveInputDirectories #(inputDirectoryNames)
{
my ($self, $menuDirectoryNames) = @_;
# Determine which directories don't match the command line, if any.
my $inputDirectories = NaturalDocs::Settings->InputDirectories();
my @unresolvedMenuDirectories;
foreach my $menuDirectory (keys %$menuDirectoryNames)
{
my $found;
foreach my $inputDirectory (@$inputDirectories)
{
if ($menuDirectory eq $inputDirectory)
{
$found = 1;
last;
};
};
if (!$found)
{ push @unresolvedMenuDirectories, $menuDirectory; };
};
# Quit if everything matches up, which should be the most common case.
if (!scalar @unresolvedMenuDirectories)
{ return; };
# Poop. See which input directories are still available.
my @unresolvedInputDirectories;
foreach my $inputDirectory (@$inputDirectories)
{
if (!exists $menuDirectoryNames->{$inputDirectory})
{ push @unresolvedInputDirectories, $inputDirectory; };
};
# Quit if there are none. This means an input directory is in the menu that isn't in the command line. Natural Docs should
# proceed normally and let the files be deleted.
if (!scalar @unresolvedInputDirectories)
{
$hasChanged = 1;
return;
};
# The index into menuDirectoryScores is the same as in unresolvedMenuDirectories. The index into each arrayref within it is
# the same as in unresolvedInputDirectories.
my @menuDirectoryScores;
for (my $i = 0; $i < scalar @unresolvedMenuDirectories; $i++)
{ push @menuDirectoryScores, [ ]; };
# Now plow through the menu, looking for files that have an unresolved base.
my @menuGroups = ( $menu );
while (scalar @menuGroups)
{
my $currentGroup = pop @menuGroups;
my $currentGroupContent = $currentGroup->GroupContent();
foreach my $entry (@$currentGroupContent)
{
if ($entry->Type() == ::MENU_GROUP())
{
push @menuGroups, $entry;
}
elsif ($entry->Type() == ::MENU_FILE())
{
# Check if it uses an unresolved base.
for (my $i = 0; $i < scalar @unresolvedMenuDirectories; $i++)
{
if (NaturalDocs::File->IsSubPathOf($unresolvedMenuDirectories[$i], $entry->Target()))
{
my $relativePath = NaturalDocs::File->MakeRelativePath($unresolvedMenuDirectories[$i], $entry->Target());
$self->ResolveFile($relativePath, \@unresolvedInputDirectories, $menuDirectoryScores[$i]);
last;
};
};
};
};
};
# Now, create an array of score objects. Each score object is the three value arrayref [ from, to, score ]. From and To are the
# conversion options and are the indexes into unresolvedInput/MenuDirectories. We'll sort this array by score to get the best
# possible conversions. Yes, really.
my @scores;
for (my $menuIndex = 0; $menuIndex < scalar @unresolvedMenuDirectories; $menuIndex++)
{
for (my $inputIndex = 0; $inputIndex < scalar @unresolvedInputDirectories; $inputIndex++)
{
if ($menuDirectoryScores[$menuIndex]->[$inputIndex])
{
push @scores, [ $menuIndex, $inputIndex, $menuDirectoryScores[$menuIndex]->[$inputIndex] ];
};
};
};
@scores = sort { $b->[2] <=> $a->[2] } @scores;
# Now we determine what goes where.
my @menuDirectoryConversions;
foreach my $scoreObject (@scores)
{
if (!defined $menuDirectoryConversions[ $scoreObject->[0] ])
{
$menuDirectoryConversions[ $scoreObject->[0] ] = $unresolvedInputDirectories[ $scoreObject->[1] ];
};
};
# Now, FINALLY, we do the conversion. Note that not every menu directory may have a conversion defined.
@menuGroups = ( $menu );
while (scalar @menuGroups)
{
my $currentGroup = pop @menuGroups;
my $currentGroupContent = $currentGroup->GroupContent();
foreach my $entry (@$currentGroupContent)
{
if ($entry->Type() == ::MENU_GROUP())
{
push @menuGroups, $entry;
}
elsif ($entry->Type() == ::MENU_FILE())
{
# Check if it uses an unresolved base.
for (my $i = 0; $i < scalar @unresolvedMenuDirectories; $i++)
{
if (NaturalDocs::File->IsSubPathOf($unresolvedMenuDirectories[$i], $entry->Target()) &&
defined $menuDirectoryConversions[$i])
{
my $relativePath = NaturalDocs::File->MakeRelativePath($unresolvedMenuDirectories[$i], $entry->Target());
$entry->SetTarget( NaturalDocs::File->JoinPaths($menuDirectoryConversions[$i], $relativePath) );
last;
};
};
};
};
};
# Whew.
$hasChanged = 1;
};
#
# Function: ResolveRelativeInputDirectories
#
# Resolves relative input directories to the input directories available.
#
sub ResolveRelativeInputDirectories
{
my ($self) = @_;
my $inputDirectories = NaturalDocs::Settings->InputDirectories();
my $resolvedInputDirectory;
if (scalar @$inputDirectories == 1)
{ $resolvedInputDirectory = $inputDirectories->[0]; }
else
{
my @score;
# Plow through the menu, looking for files and scoring them.
my @menuGroups = ( $menu );
while (scalar @menuGroups)
{
my $currentGroup = pop @menuGroups;
my $currentGroupContent = $currentGroup->GroupContent();
foreach my $entry (@$currentGroupContent)
{
if ($entry->Type() == ::MENU_GROUP())
{
push @menuGroups, $entry;
}
elsif ($entry->Type() == ::MENU_FILE())
{
$self->ResolveFile($entry->Target(), $inputDirectories, \@score);
};
};
};
# Determine the best match.
my $bestScore = 0;
my $bestIndex = 0;
for (my $i = 0; $i < scalar @$inputDirectories; $i++)
{
if ($score[$i] > $bestScore)
{
$bestScore = $score[$i];
$bestIndex = $i;
};
};
$resolvedInputDirectory = $inputDirectories->[$bestIndex];
};
# Okay, now that we have our resolved directory, update everything.
my @menuGroups = ( $menu );
while (scalar @menuGroups)
{
my $currentGroup = pop @menuGroups;
my $currentGroupContent = $currentGroup->GroupContent();
foreach my $entry (@$currentGroupContent)
{
if ($entry->Type() == ::MENU_GROUP())
{ push @menuGroups, $entry; }
elsif ($entry->Type() == ::MENU_FILE())
{
$entry->SetTarget( NaturalDocs::File->JoinPaths($resolvedInputDirectory, $entry->Target()) );
};
};
};
if (scalar @$inputDirectories > 1)
{ $hasChanged = 1; };
return $resolvedInputDirectory;
};
#
# Function: ResolveFile
#
# Tests a relative path against a list of directories. Adds one to the score of each base where there is a match.
#
# Parameters:
#
# relativePath - The relative file name to test.
# possibleBases - An arrayref of bases to test it against.
# possibleBaseScores - An arrayref of scores to adjust. The score indexes should correspond to the base indexes.
#
sub ResolveFile #(relativePath, possibleBases, possibleBaseScores)
{
my ($self, $relativePath, $possibleBases, $possibleBaseScores) = @_;
for (my $i = 0; $i < scalar @$possibleBases; $i++)
{
if (-e NaturalDocs::File->JoinPaths($possibleBases->[$i], $relativePath))
{ $possibleBaseScores->[$i]++; };
};
};
#
# Function: LockUserTitleChanges
#
# Detects if the user manually changed any file titles, and if so, automatically locks them with <MENU_FILE_NOAUTOTITLE>.
#
# Parameters:
#
# previousMenuFiles - A hashref of the files from the previous menu state. The keys are the <FileNames>, and the values are
# references to their <NaturalDocs::Menu::Entry> objects.
#
sub LockUserTitleChanges #(previousMenuFiles)
{
my ($self, $previousMenuFiles) = @_;
my @groupStack = ( $menu );
my $groupEntry;
while (scalar @groupStack)
{
$groupEntry = pop @groupStack;
foreach my $entry (@{$groupEntry->GroupContent()})
{
# If it's an unlocked file entry
if ($entry->Type() == ::MENU_FILE() && ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0)
{
my $previousEntry = $previousMenuFiles->{$entry->Target()};
# If the previous entry was also unlocked and the titles are different, the user changed the title. Automatically lock it.
if (defined $previousEntry && ($previousEntry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0 &&
$entry->Title() ne $previousEntry->Title())
{
$entry->SetFlags($entry->Flags() | ::MENU_FILE_NOAUTOTITLE());
$hasChanged = 1;
};
}
elsif ($entry->Type() == ::MENU_GROUP())
{
push @groupStack, $entry;
};
};
};
};
#
# Function: FlagAutoTitleChanges
#
# Finds which files have auto-titles that changed and flags their groups for updating with <MENU_GROUP_UPDATETITLES> and
# <MENU_GROUP_UPDATEORDER>.
#
sub FlagAutoTitleChanges
{
my ($self) = @_;
my @groupStack = ( $menu );
my $groupEntry;
while (scalar @groupStack)
{
$groupEntry = pop @groupStack;
foreach my $entry (@{$groupEntry->GroupContent()})
{
if ($entry->Type() == ::MENU_FILE() && ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0 &&
exists $defaultTitlesChanged{$entry->Target()})
{
$groupEntry->SetFlags($groupEntry->Flags() | ::MENU_GROUP_UPDATETITLES() | ::MENU_GROUP_UPDATEORDER());
$hasChanged = 1;
}
elsif ($entry->Type() == ::MENU_GROUP())
{
push @groupStack, $entry;
};
};
};
};
#
# Function: AutoPlaceNewFiles
#
# Adds files to the menu that aren't already on it, attempting to guess where they belong.
#
# New files are placed after a dummy <MENU_ENDOFORIGINAL> entry so that they don't affect the detected order. Also, the
# groups they're placed in get <MENU_GROUP_UPDATETITLES>, <MENU_GROUP_UPDATESTRUCTURE>, and
# <MENU_GROUP_UPDATEORDER> flags.
#
# Parameters:
#
# filesInMenu - An existence hash of all the <FileNames> present in the menu.
#
sub AutoPlaceNewFiles #(fileInMenu)
{
my ($self, $filesInMenu) = @_;
my $files = NaturalDocs::Project->FilesWithContent();
my $directories;
foreach my $file (keys %$files)
{
if (!exists $filesInMenu->{$file})
{
# This is done on demand because new files shouldn't be added very often, so this will save time.
if (!defined $directories)
{ $directories = $self->MatchDirectoriesAndGroups(); };
my $targetGroup;
my $fileDirectoryString = (NaturalDocs::File->SplitPath($file))[1];
$targetGroup = $directories->{$fileDirectoryString};
if (!defined $targetGroup)
{
# Okay, if there's no exact match, work our way down.
my @fileDirectories = NaturalDocs::File->SplitDirectories($fileDirectoryString);
do
{
pop @fileDirectories;
$targetGroup = $directories->{ NaturalDocs::File->JoinDirectories(@fileDirectories) };
}
while (!defined $targetGroup && scalar @fileDirectories);
if (!defined $targetGroup)
{ $targetGroup = $menu; };
};
$targetGroup->MarkEndOfOriginal();
$targetGroup->PushToGroup( NaturalDocs::Menu::Entry->New(::MENU_FILE(), undef, $file, undef) );
$targetGroup->SetFlags( $targetGroup->Flags() | ::MENU_GROUP_UPDATETITLES() |
::MENU_GROUP_UPDATESTRUCTURE() | ::MENU_GROUP_UPDATEORDER() );
$hasChanged = 1;
};
};
};
#
# Function: MatchDirectoriesAndGroups
#
# Determines which groups files in certain directories should be placed in.
#
# Returns:
#
# A hashref. The keys are the directory names, and the values are references to the group objects they should be placed in.
#
# This only repreesents directories that currently have files on the menu, so it shouldn't be assumed that every possible
# directory will exist. To match, you should first try to match the directory, and then strip the deepest directories one by
# one until there's a match or there's none left. If there's none left, use the root group <menu>.
#
sub MatchDirectoriesAndGroups
{
my ($self) = @_;
# The keys are the directory names, and the values are hashrefs. For the hashrefs, the keys are the group objects, and the
# values are the number of files in them from that directory. In other words,
# $directories{$directory}->{$groupEntry} = $count;
my %directories;
# Note that we need to use Tie::RefHash to use references as keys. Won't work otherwise. Also, not every Perl distro comes
# with Tie::RefHash::Nestable, so we can't rely on that.
# We're using an index instead of pushing and popping because we want to save a list of the groups in the order they appear
# to break ties.
my @groups = ( $menu );
my $groupIndex = 0;
# Count the number of files in each group that appear in each directory.
while ($groupIndex < scalar @groups)
{
my $groupEntry = $groups[$groupIndex];
foreach my $entry (@{$groupEntry->GroupContent()})
{
if ($entry->Type() == ::MENU_GROUP())
{
push @groups, $entry;
}
elsif ($entry->Type() == ::MENU_FILE())
{
my $directory = (NaturalDocs::File->SplitPath($entry->Target()))[1];
if (!exists $directories{$directory})
{
my $subHash = { };
tie %$subHash, 'Tie::RefHash';
$directories{$directory} = $subHash;
};
if (!exists $directories{$directory}->{$groupEntry})
{ $directories{$directory}->{$groupEntry} = 1; }
else
{ $directories{$directory}->{$groupEntry}++; };
};
};
$groupIndex++;
};
# Determine which group goes with which directory, breaking ties by using whichever group appears first.
my $finalDirectories = { };
while (my ($directory, $directoryGroups) = each %directories)
{
my $bestGroup;
my $bestCount = 0;
my %tiedGroups; # Existence hash
while (my ($group, $count) = each %$directoryGroups)
{
if ($count > $bestCount)
{
$bestGroup = $group;
$bestCount = $count;
%tiedGroups = ( );
}
elsif ($count == $bestCount)
{
$tiedGroups{$group} = 1;
};
};
# Break ties.
if (scalar keys %tiedGroups)
{
$tiedGroups{$bestGroup} = 1;
foreach my $group (@groups)
{
if (exists $tiedGroups{$group})
{
$bestGroup = $group;
last;
};
};
};
$finalDirectories->{$directory} = $bestGroup;
};
return $finalDirectories;
};
#
# Function: RemoveDeadFiles
#
# Removes files from the menu that no longer exist or no longer have Natural Docs content.
#
# Returns:
#
# The number of file entries removed.
#
sub RemoveDeadFiles
{
my ($self) = @_;
my @groupStack = ( $menu );
my $numberRemoved = 0;
my $filesWithContent = NaturalDocs::Project->FilesWithContent();
while (scalar @groupStack)
{
my $groupEntry = pop @groupStack;
my $groupContent = $groupEntry->GroupContent();
my $index = 0;
while ($index < scalar @$groupContent)
{
if ($groupContent->[$index]->Type() == ::MENU_FILE() &&
!exists $filesWithContent->{ $groupContent->[$index]->Target() } )
{
$groupEntry->DeleteFromGroup($index);
$groupEntry->SetFlags( $groupEntry->Flags() | ::MENU_GROUP_UPDATETITLES() |
::MENU_GROUP_UPDATESTRUCTURE() );
$numberRemoved++;
$hasChanged = 1;
}
elsif ($groupContent->[$index]->Type() == ::MENU_GROUP())
{
push @groupStack, $groupContent->[$index];
$index++;
}
else
{ $index++; };
};
};
return $numberRemoved;
};
#
# Function: BanAndUnbanIndexes
#
# Adjusts the indexes that are banned depending on if the user added or deleted any.
#
sub BanAndUnbanIndexes
{
my ($self) = @_;
# Unban any indexes that are present, meaning the user added them back manually without deleting the ban.
foreach my $index (keys %indexes)
{ delete $bannedIndexes{$index}; };
# Ban any indexes that were in the previous menu but not the current, meaning the user manually deleted them. However,
# don't do this if the topic isn't indexable, meaning they changed the topic type rather than the menu.
foreach my $index (keys %previousIndexes)
{
if (!exists $indexes{$index} && NaturalDocs::Topics->TypeInfo($index)->Index())
{ $bannedIndexes{$index} = 1; };
};
};
#
# Function: AddAndRemoveIndexes
#
# Automatically adds and removes index entries on the menu as necessary. <DetectIndexGroups()> should be called
# beforehand.
#
sub AddAndRemoveIndexes
{
my ($self) = @_;
my %validIndexes;
my @allIndexes = NaturalDocs::Topics->AllIndexableTypes();
foreach my $index (@allIndexes)
{
# Strip the banned indexes first so it's potentially less work for SymbolTable.
if (!exists $bannedIndexes{$index})
{ $validIndexes{$index} = 1; };
};
%validIndexes = %{NaturalDocs::SymbolTable->HasIndexes(\%validIndexes)};
# Delete dead indexes and find the best index group.
my @groupStack = ( $menu );
my $bestIndexGroup;
my $bestIndexCount = 0;
while (scalar @groupStack)
{
my $currentGroup = pop @groupStack;
my $index = 0;
my $currentIndexCount = 0;
while ($index < scalar @{$currentGroup->GroupContent()})
{
my $entry = $currentGroup->GroupContent()->[$index];
if ($entry->Type() == ::MENU_INDEX())
{
$currentIndexCount++;
if ($currentIndexCount > $bestIndexCount)
{
$bestIndexCount = $currentIndexCount;
$bestIndexGroup = $currentGroup;
};
# Remove it if it's dead.
if (!exists $validIndexes{ $entry->Target() })
{
$currentGroup->DeleteFromGroup($index);
delete $indexes{ $entry->Target() };
$hasChanged = 1;
}
else
{ $index++; };
}
else
{
if ($entry->Type() == ::MENU_GROUP())
{ push @groupStack, $entry; };
$index++;
};
};
};
# Now add the new indexes.
foreach my $index (keys %indexes)
{ delete $validIndexes{$index}; };
if (scalar keys %validIndexes)
{
# Add a group if there are no indexes at all.
if ($bestIndexCount == 0)
{
$menu->MarkEndOfOriginal();
my $newIndexGroup = NaturalDocs::Menu::Entry->New(::MENU_GROUP(), 'Index', undef,
::MENU_GROUP_ISINDEXGROUP());
$menu->PushToGroup($newIndexGroup);
$bestIndexGroup = $newIndexGroup;
$menu->SetFlags( $menu->Flags() | ::MENU_GROUP_UPDATEORDER() | ::MENU_GROUP_UPDATESTRUCTURE() );
};
# Add the new indexes.
$bestIndexGroup->MarkEndOfOriginal();
my $isIndexGroup = $bestIndexGroup->Flags() & ::MENU_GROUP_ISINDEXGROUP();
foreach my $index (keys %validIndexes)
{
my $title;
if ($isIndexGroup)
{
if ($index eq ::TOPIC_GENERAL())
{ $title = 'Everything'; }
else
{ $title = NaturalDocs::Topics->NameOfType($index, 1); };
}
else
{
$title = NaturalDocs::Topics->NameOfType($index) . ' Index';
};
my $newEntry = NaturalDocs::Menu::Entry->New(::MENU_INDEX(), $title, $index, undef);
$bestIndexGroup->PushToGroup($newEntry);
$indexes{$index} = 1;
};
$bestIndexGroup->SetFlags( $bestIndexGroup->Flags() |
::MENU_GROUP_UPDATEORDER() | ::MENU_GROUP_UPDATESTRUCTURE() );
$hasChanged = 1;
};
};
#
# Function: RemoveDeadGroups
#
# Removes groups with less than two entries. It will always remove empty groups, and it will remove groups with one entry if it
# has the <MENU_GROUP_UPDATESTRUCTURE> flag.
#
sub RemoveDeadGroups
{
my ($self) = @_;
my $index = 0;
while ($index < scalar @{$menu->GroupContent()})
{
my $entry = $menu->GroupContent()->[$index];
if ($entry->Type() == ::MENU_GROUP())
{
my $removed = $self->RemoveIfDead($entry, $menu, $index);
if (!$removed)
{ $index++; };
}
else
{ $index++; };
};
};
#
# Function: RemoveIfDead
#
# Checks a group and all its sub-groups for life and remove any that are dead. Empty groups are removed, and groups with one
# entry and the <MENU_GROUP_UPDATESTRUCTURE> flag have their entry moved to the parent group.
#
# Parameters:
#
# groupEntry - The group to check for possible deletion.
# parentGroupEntry - The parent group to move the single entry to if necessary.
# parentGroupIndex - The index of the group in its parent.
#
# Returns:
#
# Whether the group was removed or not.
#
sub RemoveIfDead #(groupEntry, parentGroupEntry, parentGroupIndex)
{
my ($self, $groupEntry, $parentGroupEntry, $parentGroupIndex) = @_;
# Do all sub-groups first, since their deletions will affect our UPDATESTRUCTURE flag and content count.
my $index = 0;
while ($index < scalar @{$groupEntry->GroupContent()})
{
my $entry = $groupEntry->GroupContent()->[$index];
if ($entry->Type() == ::MENU_GROUP())
{
my $removed = $self->RemoveIfDead($entry, $groupEntry, $index);
if (!$removed)
{ $index++; };
}
else
{ $index++; };
};
# Now check ourself.
my $count = scalar @{$groupEntry->GroupContent()};
if ($groupEntry->Flags() & ::MENU_GROUP_HASENDOFORIGINAL())
{ $count--; };
if ($count == 0)
{
$parentGroupEntry->DeleteFromGroup($parentGroupIndex);
$parentGroupEntry->SetFlags( $parentGroupEntry->Flags() | ::MENU_GROUP_UPDATESTRUCTURE() );
$hasChanged = 1;
return 1;
}
elsif ($count == 1 && ($groupEntry->Flags() & ::MENU_GROUP_UPDATESTRUCTURE()) )
{
my $onlyEntry = $groupEntry->GroupContent()->[0];
if ($onlyEntry->Type() == ::MENU_ENDOFORIGINAL())
{ $onlyEntry = $groupEntry->GroupContent()->[1]; };
$parentGroupEntry->DeleteFromGroup($parentGroupIndex);
$parentGroupEntry->MarkEndOfOriginal();
$parentGroupEntry->PushToGroup($onlyEntry);
$parentGroupEntry->SetFlags( $parentGroupEntry->Flags() | ::MENU_GROUP_UPDATETITLES() |
::MENU_GROUP_UPDATEORDER() | ::MENU_GROUP_UPDATESTRUCTURE() );
$hasChanged = 1;
return 1;
}
else
{ return undef; };
};
#
# Function: DetectIndexGroups
#
# Finds groups that are primarily used for indexes and gives them the <MENU_GROUP_ISINDEXGROUP> flag.
#
sub DetectIndexGroups
{
my ($self) = @_;
my @groupStack = ( $menu );
while (scalar @groupStack)
{
my $groupEntry = pop @groupStack;
my $isIndexGroup = -1; # -1: Can't tell yet. 0: Can't be an index group. 1: Is an index group so far.
foreach my $entry (@{$groupEntry->GroupContent()})
{
if ($entry->Type() == ::MENU_INDEX())
{
if ($isIndexGroup == -1)
{ $isIndexGroup = 1; };
}
# Text is tolerated, but it still needs at least one index entry.
elsif ($entry->Type() != ::MENU_TEXT())
{
$isIndexGroup = 0;
if ($entry->Type() == ::MENU_GROUP())
{ push @groupStack, $entry; };
};
};
if ($isIndexGroup == 1)
{
$groupEntry->SetFlags( $groupEntry->Flags() | ::MENU_GROUP_ISINDEXGROUP() );
};
};
};
#
# Function: CreateDirectorySubGroups
#
# Where possible, creates sub-groups based on directories for any long groups that have <MENU_GROUP_UPDATESTRUCTURE>
# set. Clears the flag afterwards on groups that are short enough to not need any more sub-groups, but leaves it for the rest.
#
sub CreateDirectorySubGroups
{
my ($self) = @_;
my @groupStack = ( $menu );
foreach my $groupEntry (@groupStack)
{
if ($groupEntry->Flags() & ::MENU_GROUP_UPDATESTRUCTURE())
{
# Count the number of files.
my $fileCount = 0;
foreach my $entry (@{$groupEntry->GroupContent()})
{
if ($entry->Type() == ::MENU_FILE())
{ $fileCount++; };
};
if ($fileCount > MAXFILESINGROUP)
{
my @sharedDirectories = $self->SharedDirectoriesOf($groupEntry);
my $unsharedIndex = scalar @sharedDirectories;
# The keys are the first directory entries after the shared ones, and the values are the number of files that are in
# that directory. Files that don't have subdirectories after the shared directories aren't included because they shouldn't
# be put in a subgroup.
my %directoryCounts;
foreach my $entry (@{$groupEntry->GroupContent()})
{
if ($entry->Type() == ::MENU_FILE())
{
my @entryDirectories = NaturalDocs::File->SplitDirectories( (NaturalDocs::File->SplitPath($entry->Target()))[1] );
if (scalar @entryDirectories > $unsharedIndex)
{
my $unsharedDirectory = $entryDirectories[$unsharedIndex];
if (!exists $directoryCounts{$unsharedDirectory})
{ $directoryCounts{$unsharedDirectory} = 1; }
else
{ $directoryCounts{$unsharedDirectory}++; };
};
};
};
# Now create the subgroups.
# The keys are the first directory entries after the shared ones, and the values are the groups for those files to be
# put in. There will only be entries for the groups with at least MINFILESINNEWGROUP files.
my %directoryGroups;
while (my ($directory, $count) = each %directoryCounts)
{
if ($count >= MINFILESINNEWGROUP)
{
my $newGroup = NaturalDocs::Menu::Entry->New( ::MENU_GROUP(), ucfirst($directory), undef,
::MENU_GROUP_UPDATETITLES() |
::MENU_GROUP_UPDATEORDER() );
if ($count > MAXFILESINGROUP)
{ $newGroup->SetFlags( $newGroup->Flags() | ::MENU_GROUP_UPDATESTRUCTURE()); };
$groupEntry->MarkEndOfOriginal();
push @{$groupEntry->GroupContent()}, $newGroup;
$directoryGroups{$directory} = $newGroup;
$fileCount -= $count;
};
};
# Now fill the subgroups.
if (scalar keys %directoryGroups)
{
my $afterOriginal;
my $index = 0;
while ($index < scalar @{$groupEntry->GroupContent()})
{
my $entry = $groupEntry->GroupContent()->[$index];
if ($entry->Type() == ::MENU_FILE())
{
my @entryDirectories =
NaturalDocs::File->SplitDirectories( (NaturalDocs::File->SplitPath($entry->Target()))[1] );
my $unsharedDirectory = $entryDirectories[$unsharedIndex];
if (exists $directoryGroups{$unsharedDirectory})
{
my $targetGroup = $directoryGroups{$unsharedDirectory};
if ($afterOriginal)
{ $targetGroup->MarkEndOfOriginal(); };
$targetGroup->PushToGroup($entry);
$groupEntry->DeleteFromGroup($index);
}
else
{ $index++; };
}
elsif ($entry->Type() == ::MENU_ENDOFORIGINAL())
{
$afterOriginal = 1;
$index++;
}
elsif ($entry->Type() == ::MENU_GROUP())
{
# See if we need to relocate this group.
my @groupDirectories = $self->SharedDirectoriesOf($entry);
# The group's shared directories must be at least two levels deeper than the current. If the first level deeper
# is a new group, move it there because it's a subdirectory of that one.
if (scalar @groupDirectories - scalar @sharedDirectories >= 2)
{
my $unsharedDirectory = $groupDirectories[$unsharedIndex];
if (exists $directoryGroups{$unsharedDirectory} &&
$directoryGroups{$unsharedDirectory} != $entry)
{
my $targetGroup = $directoryGroups{$unsharedDirectory};
if ($afterOriginal)
{ $targetGroup->MarkEndOfOriginal(); };
$targetGroup->PushToGroup($entry);
$groupEntry->DeleteFromGroup($index);
# We need to retitle the group if it has the name of the unshared directory.
my $oldTitle = $entry->Title();
$oldTitle =~ s/ +//g;
$unsharedDirectory =~ s/ +//g;
if (lc($oldTitle) eq lc($unsharedDirectory))
{
$entry->SetTitle($groupDirectories[$unsharedIndex + 1]);
};
}
else
{ $index++; };
}
else
{ $index++; };
}
else
{ $index++; };
};
$hasChanged = 1;
if ($fileCount <= MAXFILESINGROUP)
{ $groupEntry->SetFlags( $groupEntry->Flags() & ~::MENU_GROUP_UPDATESTRUCTURE() ); };
$groupEntry->SetFlags( $groupEntry->Flags() | ::MENU_GROUP_UPDATETITLES() |
::MENU_GROUP_UPDATEORDER() );
};
}; # If group has >MAXFILESINGROUP files
}; # If group has UPDATESTRUCTURE
# Okay, now go through all the subgroups. We do this after the above so that newly created groups can get subgrouped
# further.
foreach my $entry (@{$groupEntry->GroupContent()})
{
if ($entry->Type() == ::MENU_GROUP())
{ push @groupStack, $entry; };
};
}; # For each group entry
};
#
# Function: DetectOrder
#
# Detects the order of the entries in all groups that have the <MENU_GROUP_UPDATEORDER> flag set. Will set one of the
# <MENU_GROUP_FILESSORTED>, <MENU_GROUP_FILESANDGROUPSSORTED>, <MENU_GROUP_EVERYTHINGSORTED>, or
# <MENU_GROUP_UNSORTED> flags. It will always go for the most comprehensive sort possible, so if a group only has one
# entry, it will be flagged as <MENU_GROUP_EVERYTHINGSORTED>.
#
# <DetectIndexGroups()> should be called beforehand, as the <MENU_GROUP_ISINDEXGROUP> flag affects how the order is
# detected.
#
# The sort detection stops if it reaches a <MENU_ENDOFORIGINAL> entry, so new entries can be added to the end while still
# allowing the original sort to be detected.
#
# Parameters:
#
# forceAll - If set, the order will be detected for all groups regardless of whether <MENU_GROUP_UPDATEORDER> is set.
#
sub DetectOrder #(forceAll)
{
my ($self, $forceAll) = @_;
my @groupStack = ( $menu );
while (scalar @groupStack)
{
my $groupEntry = pop @groupStack;
my $index = 0;
# First detect the sort.
if ($forceAll || ($groupEntry->Flags() & ::MENU_GROUP_UPDATEORDER()) )
{
my $order = ::MENU_GROUP_EVERYTHINGSORTED();
my $lastFile;
my $lastFileOrGroup;
while ($index < scalar @{$groupEntry->GroupContent()} &&
$groupEntry->GroupContent()->[$index]->Type() != ::MENU_ENDOFORIGINAL() &&
$order != ::MENU_GROUP_UNSORTED())
{
my $entry = $groupEntry->GroupContent()->[$index];
# Ignore the last entry if it's an index group. We don't want it to affect the sort.
if ($index + 1 == scalar @{$groupEntry->GroupContent()} &&
$entry->Type() == ::MENU_GROUP() && ($entry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) )
{
# Ignore.
# This is an awkward code construct, basically working towards an else instead of using an if, but the code just gets
# too hard to read otherwise. The compiled code should work out to roughly the same thing anyway.
}
# Ignore the first entry if it's the general index in an index group. We don't want it to affect the sort.
elsif ($index == 0 && ($groupEntry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) &&
$entry->Type() == ::MENU_INDEX() && $entry->Target() eq ::TOPIC_GENERAL() )
{
# Ignore.
}
# Degenerate the sort.
else
{
if ($order == ::MENU_GROUP_EVERYTHINGSORTED() && $index > 0 &&
::StringCompare($entry->Title(), $groupEntry->GroupContent()->[$index - 1]->Title()) < 0)
{ $order = ::MENU_GROUP_FILESANDGROUPSSORTED(); };
if ($order == ::MENU_GROUP_FILESANDGROUPSSORTED() &&
($entry->Type() == ::MENU_FILE() || $entry->Type() == ::MENU_GROUP()) &&
defined $lastFileOrGroup && ::StringCompare($entry->Title(), $lastFileOrGroup->Title()) < 0)
{ $order = ::MENU_GROUP_FILESSORTED(); };
if ($order == ::MENU_GROUP_FILESSORTED() &&
$entry->Type() == ::MENU_FILE() && defined $lastFile &&
::StringCompare($entry->Title(), $lastFile->Title()) < 0)
{ $order = ::MENU_GROUP_UNSORTED(); };
};
# Set the lastX parameters for comparison and add sub-groups to the stack.
if ($entry->Type() == ::MENU_FILE())
{
$lastFile = $entry;
$lastFileOrGroup = $entry;
}
elsif ($entry->Type() == ::MENU_GROUP())
{
$lastFileOrGroup = $entry;
push @groupStack, $entry;
};
$index++;
};
$groupEntry->SetFlags($groupEntry->Flags() | $order);
};
# Find any subgroups in the remaining entries.
while ($index < scalar @{$groupEntry->GroupContent()})
{
my $entry = $groupEntry->GroupContent()->[$index];
if ($entry->Type() == ::MENU_GROUP())
{ push @groupStack, $entry; };
$index++;
};
};
};
#
# Function: GenerateAutoFileTitles
#
# Creates titles for the unlocked file entries in all groups that have the <MENU_GROUP_UPDATETITLES> flag set. It clears the
# flag afterwards so it can be used efficiently for multiple sweeps.
#
# Parameters:
#
# forceAll - If set, forces all the unlocked file titles to update regardless of whether the group has the
# <MENU_GROUP_UPDATETITLES> flag set.
#
sub GenerateAutoFileTitles #(forceAll)
{
my ($self, $forceAll) = @_;
my @groupStack = ( $menu );
while (scalar @groupStack)
{
my $groupEntry = pop @groupStack;
if ($forceAll || ($groupEntry->Flags() & ::MENU_GROUP_UPDATETITLES()) )
{
# Find common prefixes and paths to strip from the default menu titles.
my @sharedDirectories = $self->SharedDirectoriesOf($groupEntry);
my $noSharedDirectories = (scalar @sharedDirectories == 0);
my @sharedPrefixes;
my $noSharedPrefixes;
foreach my $entry (@{$groupEntry->GroupContent()})
{
if ($entry->Type() == ::MENU_FILE())
{
# Find the common prefixes among all file entries that are unlocked and don't use the file name as their default title.
my $defaultTitle = NaturalDocs::Project->DefaultMenuTitleOf($entry->Target());
if (!$noSharedPrefixes && ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0 &&
$defaultTitle ne $entry->Target())
{
# If the filename is part of the title, separate it off so no part of it gets included as a common prefix. This would
# happen if there's a group with only one file in it (Project.h => h) or only files that differ by extension
# (Project.h, Project.cpp => h, cpp) and people labeled them manually (// File: Project.h).
my $filename = (NaturalDocs::File->SplitPath($entry->Target()))[2];
my $filenamePart;
if ( length $defaultTitle >= length $filename &&
lc(substr($defaultTitle, 0 - length($filename))) eq lc($filename) )
{
$filenamePart = substr($defaultTitle, 0 - length($filename));
$defaultTitle = substr($defaultTitle, 0, 0 - length($filename));
};
my @entryPrefixes = split(/(\.|::|->)/, $defaultTitle);
# Remove potential leading undef/empty string.
if (!length $entryPrefixes[0])
{ shift @entryPrefixes; };
# Remove last entry. Something has to exist for the title. If we already separated off the filename, that will be
# it instead.
if (!$filenamePart)
{ pop @entryPrefixes; };
if (!scalar @entryPrefixes)
{ $noSharedPrefixes = 1; }
elsif (!scalar @sharedPrefixes)
{ @sharedPrefixes = @entryPrefixes; }
elsif ($entryPrefixes[0] ne $sharedPrefixes[0])
{ $noSharedPrefixes = 1; }
# If both arrays have entries, and the first is shared...
else
{
my $index = 1;
while ($index < scalar @sharedPrefixes && $entryPrefixes[$index] eq $sharedPrefixes[$index])
{ $index++; };
if ($index < scalar @sharedPrefixes)
{ splice(@sharedPrefixes, $index); };
};
};
}; # if entry is MENU_FILE
}; # foreach entry in group content.
if (!scalar @sharedPrefixes)
{ $noSharedPrefixes = 1; };
# Update all the menu titles of unlocked file entries.
foreach my $entry (@{$groupEntry->GroupContent()})
{
if ($entry->Type() == ::MENU_FILE() && ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0)
{
my $title = NaturalDocs::Project->DefaultMenuTitleOf($entry->Target());
if ($title eq $entry->Target())
{
my ($volume, $directoryString, $file) = NaturalDocs::File->SplitPath($entry->Target());
my @directories = NaturalDocs::File->SplitDirectories($directoryString);
if (!$noSharedDirectories)
{ splice(@directories, 0, scalar @sharedDirectories); };
# directory\...\directory\file.ext
if (scalar @directories > 2)
{ @directories = ( $directories[0], '...', $directories[-1] ); };
$directoryString = NaturalDocs::File->JoinDirectories(@directories);
$title = NaturalDocs::File->JoinPaths($directoryString, $file);
}
else
{
my $filename = (NaturalDocs::File->SplitPath($entry->Target()))[2];
my $filenamePart;
if ( length $title >= length $filename &&
lc(substr($title, 0 - length($filename))) eq lc($filename) )
{
$filenamePart = substr($title, 0 - length($filename));
$title = substr($title, 0, 0 - length($filename));
};
my @segments = split(/(::|\.|->)/, $title);
if (!length $segments[0])
{ shift @segments; };
if ($filenamePart)
{ push @segments, $filenamePart; };
if (!$noSharedPrefixes)
{ splice(@segments, 0, scalar @sharedPrefixes); };
# package...package::target
if (scalar @segments > 5)
{ splice(@segments, 1, scalar @segments - 4, '...'); };
$title = join('', @segments);
};
$entry->SetTitle($title);
}; # If entry is an unlocked file
}; # Foreach entry
$groupEntry->SetFlags( $groupEntry->Flags() & ~::MENU_GROUP_UPDATETITLES() );
}; # If updating group titles
# Now find any subgroups.
foreach my $entry (@{$groupEntry->GroupContent()})
{
if ($entry->Type() == ::MENU_GROUP())
{ push @groupStack, $entry; };
};
};
};
#
# Function: ResortGroups
#
# Resorts all groups that have <MENU_GROUP_UPDATEORDER> set. Assumes <DetectOrder()> and <GenerateAutoFileTitles()>
# have already been called. Will clear the flag and any <MENU_ENDOFORIGINAL> entries on reordered groups.
#
# Parameters:
#
# forceAll - If set, resorts all groups regardless of whether <MENU_GROUP_UPDATEORDER> is set.
#
sub ResortGroups #(forceAll)
{
my ($self, $forceAll) = @_;
my @groupStack = ( $menu );
while (scalar @groupStack)
{
my $groupEntry = pop @groupStack;
if ($forceAll || ($groupEntry->Flags() & ::MENU_GROUP_UPDATEORDER()) )
{
my $newEntriesIndex;
# Strip the ENDOFORIGINAL.
if ($groupEntry->Flags() & ::MENU_GROUP_HASENDOFORIGINAL())
{
$newEntriesIndex = 0;
while ($newEntriesIndex < scalar @{$groupEntry->GroupContent()} &&
$groupEntry->GroupContent()->[$newEntriesIndex]->Type() != ::MENU_ENDOFORIGINAL() )
{ $newEntriesIndex++; };
$groupEntry->DeleteFromGroup($newEntriesIndex);
$groupEntry->SetFlags( $groupEntry->Flags() & ~::MENU_GROUP_HASENDOFORIGINAL() );
}
else
{ $newEntriesIndex = -1; };
# Strip the exceptions.
my $trailingIndexGroup;
my $leadingGeneralIndex;
if ( ($groupEntry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) &&
$groupEntry->GroupContent()->[0]->Type() == ::MENU_INDEX() &&
$groupEntry->GroupContent()->[0]->Target() eq ::TOPIC_GENERAL() )
{
$leadingGeneralIndex = shift @{$groupEntry->GroupContent()};
if ($newEntriesIndex != -1)
{ $newEntriesIndex--; };
}
elsif (scalar @{$groupEntry->GroupContent()} && $newEntriesIndex != 0)
{
my $lastIndex;
if ($newEntriesIndex != -1)
{ $lastIndex = $newEntriesIndex - 1; }
else
{ $lastIndex = scalar @{$groupEntry->GroupContent()} - 1; };
if ($groupEntry->GroupContent()->[$lastIndex]->Type() == ::MENU_GROUP() &&
( $groupEntry->GroupContent()->[$lastIndex]->Flags() & ::MENU_GROUP_ISINDEXGROUP() ) )
{
$trailingIndexGroup = $groupEntry->GroupContent()->[$lastIndex];
$groupEntry->DeleteFromGroup($lastIndex);
if ($newEntriesIndex != -1)
{ $newEntriesIndex++; };
};
};
# If there weren't already exceptions, strip them from the new entries.
if ( (!defined $trailingIndexGroup || !defined $leadingGeneralIndex) && $newEntriesIndex != -1)
{
my $index = $newEntriesIndex;
while ($index < scalar @{$groupEntry->GroupContent()})
{
my $entry = $groupEntry->GroupContent()->[$index];
if (!defined $trailingIndexGroup &&
$entry->Type() == ::MENU_GROUP() && ($entry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) )
{
$trailingIndexGroup = $entry;
$groupEntry->DeleteFromGroup($index);
}
elsif (!defined $leadingGeneralIndex && ($groupEntry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) &&
$entry->Type() == ::MENU_INDEX() && !defined $entry->Target())
{
$leadingGeneralIndex = $entry;
$groupEntry->DeleteFromGroup($index);
}
else
{ $index++; };
};
};
# If there's no order, we still want to sort the new additions.
if ($groupEntry->Flags() & ::MENU_GROUP_UNSORTED())
{
if ($newEntriesIndex != -1)
{
my @newEntries =
@{$groupEntry->GroupContent()}[$newEntriesIndex..scalar @{$groupEntry->GroupContent()} - 1];
@newEntries = sort { $self->CompareEntries($a, $b) } @newEntries;
foreach my $newEntry (@newEntries)
{
$groupEntry->GroupContent()->[$newEntriesIndex] = $newEntry;
$newEntriesIndex++;
};
};
}
elsif ($groupEntry->Flags() & ::MENU_GROUP_EVERYTHINGSORTED())
{
@{$groupEntry->GroupContent()} = sort { $self->CompareEntries($a, $b) } @{$groupEntry->GroupContent()};
}
elsif ( ($groupEntry->Flags() & ::MENU_GROUP_FILESSORTED()) ||
($groupEntry->Flags() & ::MENU_GROUP_FILESANDGROUPSSORTED()) )
{
my $groupContent = $groupEntry->GroupContent();
my @newEntries;
if ($newEntriesIndex != -1)
{ @newEntries = splice( @$groupContent, $newEntriesIndex ); };
# First resort the existing entries.
# A couple of support functions. They're defined here instead of spun off into their own functions because they're only
# used here and to make them general we would need to add support for the other sort options.
sub IsIncludedInSort #(groupEntry, entry)
{
my ($self, $groupEntry, $entry) = @_;
return ($entry->Type() == ::MENU_FILE() ||
( $entry->Type() == ::MENU_GROUP() &&
($groupEntry->Flags() & ::MENU_GROUP_FILESANDGROUPSSORTED()) ) );
};
sub IsSorted #(groupEntry)
{
my ($self, $groupEntry) = @_;
my $lastApplicable;
foreach my $entry (@{$groupEntry->GroupContent()})
{
# If the entry is applicable to the sort order...
if ($self->IsIncludedInSort($groupEntry, $entry))
{
if (defined $lastApplicable)
{
if ($self->CompareEntries($entry, $lastApplicable) < 0)
{ return undef; };
};
$lastApplicable = $entry;
};
};
return 1;
};
# There's a good chance it's still sorted. They should only become unsorted if an auto-title changes.
if (!$self->IsSorted($groupEntry))
{
# Crap. Okay, method one is to sort each group of continuous sortable elements. There's a possibility that doing
# this will cause the whole to become sorted again. We try this first, even though it isn't guaranteed to succeed,
# because it will restore the sort without moving any unsortable entries.
# Copy it because we'll need the original if this fails.
my @originalGroupContent = @$groupContent;
my $index = 0;
my $startSortable = 0;
while (1)
{
# If index is on an unsortable entry or the end of the array...
if ($index == scalar @$groupContent || !$self->IsIncludedInSort($groupEntry, $groupContent->[$index]))
{
# If we have at least two sortable entries...
if ($index - $startSortable >= 2)
{
# Sort them.
my @sortableEntries = @{$groupContent}[$startSortable .. $index - 1];
@sortableEntries = sort { $self->CompareEntries($a, $b) } @sortableEntries;
foreach my $sortableEntry (@sortableEntries)
{
$groupContent->[$startSortable] = $sortableEntry;
$startSortable++;
};
};
if ($index == scalar @$groupContent)
{ last; };
$startSortable = $index + 1;
};
$index++;
};
if (!$self->IsSorted($groupEntry))
{
# Crap crap. Okay, now we do a full sort but with potential damage to the original structure. Each unsortable
# element is locked to the next sortable element. We sort the sortable elements, bringing all the unsortable
# pieces with them.
my @pieces = ( [ ] );
my $currentPiece = $pieces[0];
foreach my $entry (@originalGroupContent)
{
push @$currentPiece, $entry;
# If the entry is sortable...
if ($self->IsIncludedInSort($groupEntry, $entry))
{
$currentPiece = [ ];
push @pieces, $currentPiece;
};
};
my $lastUnsortablePiece;
# If the last entry was sortable, we'll have an empty piece at the end. Drop it.
if (scalar @{$pieces[-1]} == 0)
{ pop @pieces; }
# If the last entry wasn't sortable, the last piece won't end with a sortable element. Save it, but remove it
# from the list.
else
{ $lastUnsortablePiece = pop @pieces; };
# Sort the list.
@pieces = sort { $self->CompareEntries( $a->[-1], $b->[-1] ) } @pieces;
# Copy it back to the original.
if (defined $lastUnsortablePiece)
{ push @pieces, $lastUnsortablePiece; };
my $index = 0;
foreach my $piece (@pieces)
{
foreach my $entry (@{$piece})
{
$groupEntry->GroupContent()->[$index] = $entry;
$index++;
};
};
};
};
# Okay, the orginal entries are sorted now. Sort the new entries and apply.
if (scalar @newEntries)
{
@newEntries = sort { $self->CompareEntries($a, $b) } @newEntries;
my @originalEntries = @$groupContent;
@$groupContent = ( );
while (1)
{
while (scalar @originalEntries && !$self->IsIncludedInSort($groupEntry, $originalEntries[0]))
{ push @$groupContent, (shift @originalEntries); };
if (!scalar @originalEntries || !scalar @newEntries)
{ last; };
while (scalar @newEntries && $self->CompareEntries($newEntries[0], $originalEntries[0]) < 0)
{ push @$groupContent, (shift @newEntries); };
push @$groupContent, (shift @originalEntries);
if (!scalar @originalEntries || !scalar @newEntries)
{ last; };
};
if (scalar @originalEntries)
{ push @$groupContent, @originalEntries; }
elsif (scalar @newEntries)
{ push @$groupContent, @newEntries; };
};
};
# Now re-add the exceptions.
if (defined $leadingGeneralIndex)
{
unshift @{$groupEntry->GroupContent()}, $leadingGeneralIndex;
};
if (defined $trailingIndexGroup)
{
$groupEntry->PushToGroup($trailingIndexGroup);
};
};
foreach my $entry (@{$groupEntry->GroupContent()})
{
if ($entry->Type() == ::MENU_GROUP())
{ push @groupStack, $entry; };
};
};
};
#
# Function: CompareEntries
#
# A comparison function for use in sorting. Compares the two entries by their titles with <StringCompare()>, but in the case
# of a tie, puts <MENU_FILE> entries above <MENU_GROUP> entries.
#
sub CompareEntries #(a, b)
{
my ($self, $a, $b) = @_;
my $result = ::StringCompare($a->Title(), $b->Title());
if ($result == 0)
{
if ($a->Type() == ::MENU_FILE() && $b->Type() == ::MENU_GROUP())
{ $result = -1; }
elsif ($a->Type() == ::MENU_GROUP() && $b->Type() == ::MENU_FILE())
{ $result = 1; };
};
return $result;
};
#
# Function: SharedDirectoriesOf
#
# Returns an array of all the directories shared by the files in the group. If none, returns an empty array.
#
sub SharedDirectoriesOf #(group)
{
my ($self, $groupEntry) = @_;
my @sharedDirectories;
foreach my $entry (@{$groupEntry->GroupContent()})
{
if ($entry->Type() == ::MENU_FILE())
{
my @entryDirectories = NaturalDocs::File->SplitDirectories( (NaturalDocs::File->SplitPath($entry->Target()))[1] );
if (!scalar @sharedDirectories)
{ @sharedDirectories = @entryDirectories; }
else
{ ::ShortenToMatchStrings(\@sharedDirectories, \@entryDirectories); };
if (!scalar @sharedDirectories)
{ last; };
};
};
return @sharedDirectories;
};
1;