mirror of
https://github.com/ddnet/ddnet.git
synced 2024-11-14 12:08:20 +00:00
3407 lines
116 KiB
Perl
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:
|
|
#
|
|
# & - 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;
|