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