1###############################################################################
2#
3#   Package: NaturalDocs::Project
4#
5###############################################################################
6#
7#   A package that manages information about the files in the source tree, as well as the list of files that have to be parsed
8#   and built.
9#
10#   Usage and Dependencies:
11#
12#       - All the <Config and Data File Functions> are available immediately, except for the status functions.
13#
14#       - <ReparseEverything()> and <RebuildEverything()> are available immediately, because they may need to be called
15#         after <LoadConfigFileInfo()> but before <LoadSourceFileInfo()>.
16#
17#       - Prior to <LoadConfigFileInfo()>, <NaturalDocs::Settings> must be initialized.
18#
19#       - After <LoadConfigFileInfo()>, the status <Config and Data File Functions> are available as well.
20#
21#       - Prior to <LoadSourceFileInfo()>, <NaturalDocs::Settings> and <NaturalDocs::Languages> must be initialized.
22#
23#       - After <LoadSourceFileInfo()>, the rest of the <Source File Functions> are available.
24#
25###############################################################################
26
27# This file is part of Natural Docs, which is Copyright � 2003-2010 Greg Valure
28# Natural Docs is licensed under version 3 of the GNU Affero General Public License (AGPL)
29# Refer to License.txt for the complete details
30
31use NaturalDocs::Project::SourceFile;
32use NaturalDocs::Project::ImageFile;
33
34use strict;
35use integer;
36
37package NaturalDocs::Project;
38
39
40###############################################################################
41# Group: File Handles
42
43#
44#   handle: FH_FILEINFO
45#
46#   The file handle for the file information file, <FileInfo.nd>.
47#
48
49#
50#   handle: FH_CONFIGFILEINFO
51#
52#   The file handle for the config file information file, <ConfigFileInfo.nd>.
53#
54
55#
56#   handle: FH_IMAGEFILE
57#
58#   The file handle for determining the dimensions of image files.
59#
60
61
62
63###############################################################################
64# Group: Source File Variables
65
66
67#
68#   hash: supportedFiles
69#
70#   A hash of all the supported files in the input directory.  The keys are the <FileNames>, and the values are
71#   <NaturalDocs::Project::SourceFile> objects.
72#
73my %supportedFiles;
74
75#
76#   hash: filesToParse
77#
78#   An existence hash of all the <FileNames> that need to be parsed.
79#
80my %filesToParse;
81
82#
83#   hash: filesToBuild
84#
85#   An existence hash of all the <FileNames> that need to be built.
86#
87my %filesToBuild;
88
89#
90#   hash: filesToPurge
91#
92#   An existence hash of the <FileNames> that had Natural Docs content last time, but now either don't exist or no longer have
93#   content.
94#
95my %filesToPurge;
96
97#
98#   hash: unbuiltFilesWithContent
99#
100#   An existence hash of all the <FileNames> that have Natural Docs content but are not part of <filesToBuild>.
101#
102my %unbuiltFilesWithContent;
103
104
105# bool: reparseEverything
106# Whether all the source files need to be reparsed.
107my $reparseEverything;
108
109# bool: rebuildEverything
110# Whether all the source files need to be rebuilt.
111my $rebuildEverything;
112
113# hash: mostUsedLanguage
114# The name of the most used language.  Doesn't include text files.
115my $mostUsedLanguage;
116
117
118
119###############################################################################
120# Group: Configuration File Variables
121
122
123#
124#   hash: mainConfigFile
125#
126#   A hash mapping all the main configuration file names without paths to their <FileStatus>.  Prior to <LoadConfigFileInfo()>,
127#   it serves as an existence hashref of the file names.
128#
129my %mainConfigFiles = ( 'Topics.txt' => 1, 'Languages.txt' => 1 );
130
131#
132#   hash: userConfigFiles
133#
134#   A hash mapping all the user configuration file names without paths to their <FileStatus>.  Prior to <LoadConfigFileInfo()>,
135#   it serves as an existence hashref of the file names.
136#
137my %userConfigFiles = ( 'Topics.txt' => 1, 'Languages.txt' => 1, 'Menu.txt' => 1 );
138
139
140
141
142###############################################################################
143# Group: Image File Variables
144
145
146#
147#   hash: imageFileExtensions
148#
149#   An existence hash of all the file extensions for images.  Extensions are in all lowercase.
150#
151my %imageFileExtensions = ( 'jpg' => 1, 'jpeg' => 1, 'gif' => 1, 'png' => 1, 'bmp' => 1 );
152
153
154#
155#   hash: imageFiles
156#
157#   A hash of all the image files in the project.  The keys are the <FileNames> and the values are
158#   <NaturalDocs::Project::ImageFiles>.
159#
160my %imageFiles;
161
162
163#
164#   hash: imageFilesToUpdate
165#
166#   An existence hash of all the image <FileNames> that need to be updated, either because they changed or they're new to the
167#   project.
168#
169my %imageFilesToUpdate;
170
171
172#
173#   hash: imageFilesToPurge
174#
175#   An existence hash of all the image <FileNames> that need to be purged, either because the files no longer exist or because
176#   they are no longer used.
177#
178my %imageFilesToPurge;
179
180
181#
182#   hash: insensitiveImageFiles
183#
184#   A hash that maps all lowercase image <FileNames> to their proper case as it would appear in <imageFiles>.  Used for
185#   case insensitivity, obviously.
186#
187#   You can't just use all lowercase in <imageFiles> because both Linux and HTTP are case sensitive, so the original case must
188#   be preserved.  We also want to allow separate entries for files that differ based only on case, so it goes to <imageFiles> first
189#   where they can be distinguished and here only if there's no match.  Ties are broken by whichever is lower with cmp, because
190#   it has to resolve consistently on all runs of the program.
191#
192my %insensitiveImageFiles;
193
194
195
196###############################################################################
197# Group: Files
198
199
200#
201#   File: FileInfo.nd
202#
203#   An index of the state of the files as of the last parse.  Used to determine if files were added, deleted, or changed.
204#
205#   Format:
206#
207#       The format is a text file.
208#
209#       > [VersionInt: app version]
210#
211#       The beginning of the file is the <VersionInt> it was generated with.
212#
213#       > [most used language name]
214#
215#       Next is the name of the most used language in the source tree.  Does not include text files.
216#
217#       Each following line is
218#
219#       > [file name] tab [last modification time] tab [has ND content (0 or 1)] tab [default menu title] \n
220#
221#   Revisions:
222#
223#       1.3:
224#
225#           - The line following the <VersionInt>, which was previously the last modification time of <Menu.txt>, was changed to
226#             the name of the most used language.
227#
228#       1.16:
229#
230#           - File names are now absolute.  Prior to 1.16, they were relative to the input directory since only one was allowed.
231#
232#       1.14:
233#
234#           - The file was renamed from NaturalDocs.files to FileInfo.nd and moved into the Data subdirectory.
235#
236#       0.95:
237#
238#           - The file version was changed to match the program version.  Prior to 0.95, the version line was 1.  Test for "1" instead
239#             of "1.0" to distinguish.
240#
241
242
243#
244#   File: ConfigFileInfo.nd
245#
246#   An index of the state of the config files as of the last parse.
247#
248#   Format:
249#
250#       > [BINARY_FORMAT]
251#       > [VersionInt: app version]
252#
253#       First is the standard <BINARY_FORMAT> <VersionInt> header.
254#
255#       > [UInt32: last modification time of menu]
256#       > [UInt32: last modification of main topics file]
257#       > [UInt32: last modification of user topics file]
258#       > [UInt32: last modification of main languages file]
259#       > [UInt32: last modification of user languages file]
260#
261#       Next are the last modification times of various configuration files as UInt32s in the standard Unix format.
262#
263#
264#   Revisions:
265#
266#       1.3:
267#
268#           - The file was added to Natural Docs.  Previously the last modification of <Menu.txt> was stored in <FileInfo.nd>, and
269#             <Topics.txt> and <Languages.txt> didn't exist.
270#
271
272
273#
274#   File: ImageFileInfo.nd
275#
276#   An index of the state of the image files as of the last parse.
277#
278#   Format:
279#
280#       > [Standard Binary Header]
281#
282#       First is the standard binary file header as defined by <NaturalDocs::BinaryFile>.
283#
284#       > [UString16: file name or undef]
285#       > [UInt32: last modification time]
286#       > [UInt8: was used]
287#
288#       This section is repeated until the file name is null.  The last modification times are UInt32s in the standard Unix format.
289#
290#
291#   Revisions:
292#
293#		1.52:
294#
295#			- AString16s were changed to UString16s.
296#
297#       1.4:
298#
299#           - The file was added to Natural Docs.
300#
301
302
303
304###############################################################################
305# Group: File Functions
306
307#
308#   Function: LoadSourceFileInfo
309#
310#   Loads the project file from disk and compares it against the files in the input directory.  Project is loaded from
311#   <FileInfo.nd>.  New and changed files will be added to <FilesToParse()>, and if they have content,
312#   <FilesToBuild()>.
313#
314#   Will call <NaturalDocs::Languages->OnMostUsedLanguageKnown()> if <MostUsedLanguage()> changes.
315#
316#   Returns:
317#
318#       Returns whether the project was changed in any way.
319#
320sub LoadSourceFileInfo
321    {
322    my ($self) = @_;
323
324    $self->GetAllSupportedFiles();
325    NaturalDocs::Languages->OnMostUsedLanguageKnown();
326
327    my $fileIsOkay;
328    my $version;
329    my $hasChanged;
330    my $lineReader;
331
332    if (open(FH_FILEINFO, '<' . $self->DataFile('FileInfo.nd')))
333        {
334        $lineReader = NaturalDocs::LineReader->New(\*FH_FILEINFO);
335
336        # Check if the file is in the right format.
337        $version = NaturalDocs::Version->FromString($lineReader->Get());
338
339        # The project file need to be rebuilt for 1.16.  The source files need to be reparsed and the output files rebuilt for 1.51.
340        # We'll tolerate the difference between 1.16 and 1.3 in the loader.
341
342        if (NaturalDocs::Version->CheckFileFormat( $version, NaturalDocs::Version->FromString('1.16') ))
343            {
344            $fileIsOkay = 1;
345
346            if (!NaturalDocs::Version->CheckFileFormat( $version, NaturalDocs::Version->FromString('1.51') ))
347                {
348                $reparseEverything = 1;
349                $rebuildEverything = 1;
350                $hasChanged = 1;
351                };
352            }
353        else
354            {
355            close(FH_FILEINFO);
356            $hasChanged = 1;
357            };
358        };
359
360
361    if ($fileIsOkay)
362        {
363        my %indexedFiles;
364
365
366        my $line = $lineReader->Get();
367
368        # Prior to 1.3 it was the last modification time of Menu.txt, which we ignore and treat as though the most used language
369        # changed.  Prior to 1.32 the settings didn't transfer over correctly to Menu.txt so we need to behave that way again.
370        if ($version < NaturalDocs::Version->FromString('1.32') || lc($mostUsedLanguage) ne lc($line))
371            {
372            $reparseEverything = 1;
373            NaturalDocs::SymbolTable->RebuildAllIndexes();
374            };
375
376
377        # Parse the rest of the file.
378
379        while ($line = $lineReader->Get())
380            {
381            my ($file, $modification, $hasContent, $menuTitle) = split(/\t/, $line, 4);
382
383            # If the file no longer exists...
384            if (!exists $supportedFiles{$file})
385                {
386                if ($hasContent)
387                    {  $filesToPurge{$file} = 1;  };
388
389                $hasChanged = 1;
390                }
391
392            # If the file still exists...
393            else
394                {
395                $indexedFiles{$file} = 1;
396
397                # If the file changed...
398                if ($supportedFiles{$file}->LastModified() != $modification)
399                    {
400                    $supportedFiles{$file}->SetStatus(::FILE_CHANGED());
401                    $filesToParse{$file} = 1;
402
403                    # If the file loses its content, this will be removed by SetHasContent().
404                    if ($hasContent)
405                        {  $filesToBuild{$file} = 1;  };
406
407                    $hasChanged = 1;
408                    }
409
410                # If the file has not changed...
411                else
412                    {
413                    my $status;
414
415                    if ($rebuildEverything && $hasContent)
416                        {
417                        $status = ::FILE_CHANGED();
418
419                        # If the file loses its content, this will be removed by SetHasContent().
420                        $filesToBuild{$file} = 1;
421                        $hasChanged = 1;
422                        }
423                    else
424                        {
425                        $status = ::FILE_SAME();
426
427                        if ($hasContent)
428                            {  $unbuiltFilesWithContent{$file} = 1;  };
429                        };
430
431                    if ($reparseEverything)
432                        {
433                        $status = ::FILE_CHANGED();
434
435                        $filesToParse{$file} = 1;
436                        $hasChanged = 1;
437                        };
438
439                    $supportedFiles{$file}->SetStatus($status);
440                    };
441
442                $supportedFiles{$file}->SetHasContent($hasContent);
443                $supportedFiles{$file}->SetDefaultMenuTitle($menuTitle);
444                };
445            };
446
447        close(FH_FILEINFO);
448
449
450        # Check for added files.
451
452        if (scalar keys %supportedFiles > scalar keys %indexedFiles)
453            {
454            foreach my $file (keys %supportedFiles)
455                {
456                if (!exists $indexedFiles{$file})
457                    {
458                    $supportedFiles{$file}->SetStatus(::FILE_NEW());
459                    $supportedFiles{$file}->SetDefaultMenuTitle($file);
460                    $supportedFiles{$file}->SetHasContent(undef);
461                    $filesToParse{$file} = 1;
462                    # It will be added to filesToBuild if HasContent gets set to true when it's parsed.
463                    $hasChanged = 1;
464                    };
465                };
466            };
467        }
468
469    # If something's wrong with FileInfo.nd, everything is new.
470    else
471        {
472        foreach my $file (keys %supportedFiles)
473            {
474            $supportedFiles{$file}->SetStatus(::FILE_NEW());
475            $supportedFiles{$file}->SetDefaultMenuTitle($file);
476            $supportedFiles{$file}->SetHasContent(undef);
477            $filesToParse{$file} = 1;
478            # It will be added to filesToBuild if HasContent gets set to true when it's parsed.
479            };
480
481        $hasChanged = 1;
482        };
483
484
485    # There are other side effects, so we need to call this.
486    if ($rebuildEverything)
487        {  $self->RebuildEverything();  };
488
489
490    return $hasChanged;
491    };
492
493
494#
495#   Function: SaveSourceFileInfo
496#
497#   Saves the source file info to disk.  Everything is saved in <FileInfo.nd>.
498#
499sub SaveSourceFileInfo
500    {
501    my ($self) = @_;
502
503    open(FH_FILEINFO, '>' . $self->DataFile('FileInfo.nd'))
504        or die "Couldn't save project file " . $self->DataFile('FileInfo.nd') . "\n";
505
506    NaturalDocs::Version->ToTextFile(\*FH_FILEINFO, NaturalDocs::Settings->AppVersion());
507
508    print FH_FILEINFO $mostUsedLanguage . "\n";
509
510    while (my ($fileName, $file) = each %supportedFiles)
511        {
512        print FH_FILEINFO $fileName . "\t"
513                              . $file->LastModified() . "\t"
514                              . ($file->HasContent() || '0') . "\t"
515                              . $file->DefaultMenuTitle() . "\n";
516        };
517
518    close(FH_FILEINFO);
519    };
520
521
522#
523#   Function: LoadConfigFileInfo
524#
525#   Loads the config file info from disk.
526#
527sub LoadConfigFileInfo
528    {
529    my ($self) = @_;
530
531    my $fileIsOkay;
532    my $version;
533    my $fileName = NaturalDocs::Project->DataFile('ConfigFileInfo.nd');
534
535    if (open(FH_CONFIGFILEINFO, '<' . $fileName))
536        {
537        # See if it's binary.
538        binmode(FH_CONFIGFILEINFO);
539
540        my $firstChar;
541        read(FH_CONFIGFILEINFO, $firstChar, 1);
542
543        if ($firstChar == ::BINARY_FORMAT())
544            {
545            $version = NaturalDocs::Version->FromBinaryFile(\*FH_CONFIGFILEINFO);
546
547            # It hasn't changed since being introduced.
548
549            if (NaturalDocs::Version->CheckFileFormat($version))
550                {  $fileIsOkay = 1;  }
551            else
552                {  close(FH_CONFIGFILEINFO);  };
553            }
554
555        else # it's not in binary
556            {  close(FH_CONFIGFILEINFO);  };
557        };
558
559    my @configFiles = ( $self->UserConfigFile('Menu.txt'), \$userConfigFiles{'Menu.txt'},
560                                 $self->MainConfigFile('Topics.txt'), \$mainConfigFiles{'Topics.txt'},
561                                 $self->UserConfigFile('Topics.txt'), \$userConfigFiles{'Topics.txt'},
562                                 $self->MainConfigFile('Languages.txt'), \$mainConfigFiles{'Languages.txt'},
563                                 $self->UserConfigFile('Languages.txt'), \$userConfigFiles{'Languages.txt'} );
564
565    if ($fileIsOkay)
566        {
567        my $raw;
568
569        read(FH_CONFIGFILEINFO, $raw, 20);
570        my @configFileDates = unpack('NNNNN', $raw);
571
572        while (scalar @configFiles)
573            {
574            my $file = shift @configFiles;
575            my $fileStatus = shift @configFiles;
576            my $fileDate = shift @configFileDates;
577
578            if (-e $file)
579                {
580                if ($fileDate == (stat($file))[9])
581                    {  $$fileStatus = ::FILE_SAME();  }
582                else
583                    {  $$fileStatus = ::FILE_CHANGED();  };
584                }
585            else
586                {  $$fileStatus = ::FILE_DOESNTEXIST();  };
587            };
588
589        close(FH_CONFIGFILEINFO);
590        }
591    else # !$fileIsOkay
592        {
593        while (scalar @configFiles)
594            {
595            my $file = shift @configFiles;
596            my $fileStatus = shift @configFiles;
597
598            if (-e $file)
599                {  $$fileStatus = ::FILE_CHANGED();  }
600            else
601                {  $$fileStatus = ::FILE_DOESNTEXIST();  };
602            };
603        };
604
605    if ($userConfigFiles{'Menu.txt'} == ::FILE_SAME() && $rebuildEverything)
606        {  $userConfigFiles{'Menu.txt'} = ::FILE_CHANGED();  };
607    };
608
609
610#
611#   Function: SaveConfigFileInfo
612#
613#   Saves the config file info to disk.  You *must* save all other config files first, such as <Menu.txt> and <Topics.txt>.
614#
615sub SaveConfigFileInfo
616    {
617    my ($self) = @_;
618
619    open (FH_CONFIGFILEINFO, '>' . NaturalDocs::Project->DataFile('ConfigFileInfo.nd'))
620        or die "Couldn't save " . NaturalDocs::Project->DataFile('ConfigFileInfo.nd') . ".\n";
621
622    binmode(FH_CONFIGFILEINFO);
623
624    print FH_CONFIGFILEINFO '' . ::BINARY_FORMAT();
625
626    NaturalDocs::Version->ToBinaryFile(\*FH_CONFIGFILEINFO, NaturalDocs::Settings->AppVersion());
627
628    print FH_CONFIGFILEINFO pack('NNNNN', (stat($self->UserConfigFile('Menu.txt')))[9],
629                                                                (stat($self->MainConfigFile('Topics.txt')))[9],
630                                                                (stat($self->UserConfigFile('Topics.txt')))[9],
631                                                                (stat($self->MainConfigFile('Languages.txt')))[9],
632                                                                (stat($self->UserConfigFile('Languages.txt')))[9] );
633
634    close(FH_CONFIGFILEINFO);
635    };
636
637
638#
639#   Function: LoadImageFileInfo
640#
641#   Loads the image file info from disk.
642#
643sub LoadImageFileInfo
644    {
645    my ($self) = @_;
646
647    my $version = NaturalDocs::BinaryFile->OpenForReading( NaturalDocs::Project->DataFile('ImageFileInfo.nd') );
648    my $fileIsOkay;
649
650    if (defined $version)
651        {
652        if (NaturalDocs::Version->CheckFileFormat($version, NaturalDocs::Version->FromString('1.52')))
653            {  $fileIsOkay = 1;  }
654        else
655            {  NaturalDocs::BinaryFile->Close();  };
656        };
657
658    if ($fileIsOkay)
659        {
660        # [UString16: file name or undef]
661
662        while (my $imageFile = NaturalDocs::BinaryFile->GetUString16())
663            {
664            # [UInt32: last modified]
665            # [UInt8: was used]
666
667            my $lastModified = NaturalDocs::BinaryFile->GetUInt32();
668            my $wasUsed = NaturalDocs::BinaryFile->GetUInt8();
669
670            my $imageFileObject = $imageFiles{$imageFile};
671
672            # If there's an image file in ImageFileInfo.nd that no longer exists...
673            if (!$imageFileObject)
674                {
675                $imageFileObject = NaturalDocs::Project::ImageFile->New($lastModified, ::FILE_DOESNTEXIST(), $wasUsed);
676                $imageFiles{$imageFile} = $imageFileObject;
677
678                if ($wasUsed)
679                    {  $imageFilesToPurge{$imageFile} = 1;  };
680                }
681            else
682                {
683                $imageFileObject->SetWasUsed($wasUsed);
684
685                # This will be removed if it gets any references.
686                if ($wasUsed)
687                    {  $imageFilesToPurge{$imageFile} = 1;  };
688
689                if ($imageFileObject->LastModified() == $lastModified && !$rebuildEverything)
690                    {  $imageFileObject->SetStatus(::FILE_SAME());  }
691                else
692                    {  $imageFileObject->SetStatus(::FILE_CHANGED());  };
693                };
694            };
695
696        NaturalDocs::BinaryFile->Close();
697        }
698
699    else # !$fileIsOkay
700        {
701        $self->RebuildEverything();
702        };
703    };
704
705
706#
707#   Function: SaveImageFileInfo
708#
709#   Saves the image file info to disk.
710#
711sub SaveImageFileInfo
712    {
713    my $self = shift;
714
715    NaturalDocs::BinaryFile->OpenForWriting( NaturalDocs::Project->DataFile('ImageFileInfo.nd') );
716
717    while (my ($imageFile, $imageFileInfo) = each %imageFiles)
718        {
719        if ($imageFileInfo->Status() != ::FILE_DOESNTEXIST())
720            {
721            # [UString16: file name or undef]
722            # [UInt32: last modification time]
723            # [UInt8: was used]
724
725            NaturalDocs::BinaryFile->WriteUString16($imageFile);
726            NaturalDocs::BinaryFile->WriteUInt32($imageFileInfo->LastModified());
727            NaturalDocs::BinaryFile->WriteUInt8( ($imageFileInfo->ReferenceCount() > 0 ? 1 : 0) );
728            };
729        };
730
731    NaturalDocs::BinaryFile->WriteUString16(undef);
732    NaturalDocs::BinaryFile->Close();
733    };
734
735
736#
737#   Function: MigrateOldFiles
738#
739#   If the project uses the old file names used prior to 1.14, it converts them to the new file names.
740#
741sub MigrateOldFiles
742    {
743    my ($self) = @_;
744
745    my $projectDirectory = NaturalDocs::Settings->ProjectDirectory();
746
747    # We use the menu file as a test to see if we're using the new format.
748    if (-e NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs_Menu.txt'))
749        {
750        # The Data subdirectory would have been created by NaturalDocs::Settings.
751
752        rename( NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs_Menu.txt'), $self->UserConfigFile('Menu.txt') );
753
754        if (-e NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.sym'))
755            {  rename( NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.sym'), $self->DataFile('SymbolTable.nd') );  };
756
757        if (-e NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.files'))
758            {  rename( NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.files'), $self->DataFile('FileInfo.nd') );  };
759
760        if (-e NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.m'))
761            {  rename( NaturalDocs::File->JoinPaths($projectDirectory, 'NaturalDocs.m'), $self->DataFile('PreviousMenuState.nd') );  };
762        };
763    };
764
765
766
767###############################################################################
768# Group: Config and Data File Functions
769
770
771#
772#   Function: MainConfigFile
773#
774#   Returns the full path to the passed main configuration file.  Pass the file name only.
775#
776sub MainConfigFile #(string file)
777    {
778    my ($self, $file) = @_;
779    return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ConfigDirectory(), $file );
780    };
781
782#
783#   Function: MainConfigFileStatus
784#
785#   Returns the <FileStatus> of the passed main configuration file.  Pass the file name only.
786#
787sub MainConfigFileStatus #(string file)
788    {
789    my ($self, $file) = @_;
790    return $mainConfigFiles{$file};
791    };
792
793#
794#   Function: UserConfigFile
795#
796#   Returns the full path to the passed user configuration file.  Pass the file name only.
797#
798sub UserConfigFile #(string file)
799    {
800    my ($self, $file) = @_;
801    return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDirectory(), $file );
802    };
803
804#
805#   Function: UserConfigFileStatus
806#
807#   Returns the <FileStatus> of the passed user configuration file.  Pass the file name only.
808#
809sub UserConfigFileStatus #(string file)
810    {
811    my ($self, $file) = @_;
812    return $userConfigFiles{$file};
813    };
814
815#
816#   Function: DataFile
817#
818#   Returns the full path to the passed data file.  Pass the file name only.
819#
820sub DataFile #(string file)
821    {
822    my ($self, $file) = @_;
823    return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDataDirectory(), $file );
824    };
825
826
827
828
829###############################################################################
830# Group: Source File Functions
831
832
833# Function: FilesToParse
834# Returns an existence hashref of the <FileNames> to parse.  This is not a copy of the data, so don't change it.
835sub FilesToParse
836    {  return \%filesToParse;  };
837
838# Function: FilesToBuild
839# Returns an existence hashref of the <FileNames> to build.  This is not a copy of the data, so don't change it.
840sub FilesToBuild
841    {  return \%filesToBuild;  };
842
843# Function: FilesToPurge
844# Returns an existence hashref of the <FileNames> that had content last time, but now either don't anymore or were deleted.
845# This is not a copy of the data, so don't change it.
846sub FilesToPurge
847    {  return \%filesToPurge;  };
848
849#
850#   Function: RebuildFile
851#
852#   Adds the file to the list of files to build.  This function will automatically filter out files that don't have Natural Docs content and
853#   files that are part of <FilesToPurge()>.  If this gets called on a file and that file later gets Natural Docs content, it will be added.
854#
855#   Parameters:
856#
857#       file - The <FileName> to build or rebuild.
858#
859sub RebuildFile #(file)
860    {
861    my ($self, $file) = @_;
862
863    # We don't want to add it to the build list if it doesn't exist, doesn't have Natural Docs content, or it's going to be purged.
864    # If it wasn't parsed yet and will later be found to have ND content, it will be added by SetHasContent().
865    if (exists $supportedFiles{$file} && !exists $filesToPurge{$file} && $supportedFiles{$file}->HasContent())
866        {
867        $filesToBuild{$file} = 1;
868
869        if (exists $unbuiltFilesWithContent{$file})
870            {  delete $unbuiltFilesWithContent{$file};  };
871        };
872    };
873
874
875#
876#   Function: ReparseEverything
877#
878#   Adds all supported files to the list of files to parse.  This does not necessarily mean these files are going to be rebuilt.
879#
880sub ReparseEverything
881    {
882    my ($self) = @_;
883
884    if (!$reparseEverything)
885        {
886        foreach my $file (keys %supportedFiles)
887            {
888            $filesToParse{$file} = 1;
889            };
890
891        $reparseEverything = 1;
892        };
893    };
894
895
896#
897#   Function: RebuildEverything
898#
899#   Adds all supported files to the list of files to build.  This does not necessarily mean these files are going to be reparsed.
900#
901sub RebuildEverything
902    {
903    my ($self) = @_;
904
905    foreach my $file (keys %unbuiltFilesWithContent)
906        {
907        $filesToBuild{$file} = 1;
908        };
909
910    %unbuiltFilesWithContent = ( );
911    $rebuildEverything = 1;
912
913    NaturalDocs::SymbolTable->RebuildAllIndexes();
914
915    if ($userConfigFiles{'Menu.txt'} == ::FILE_SAME())
916        {  $userConfigFiles{'Menu.txt'} = ::FILE_CHANGED();  };
917
918    while (my ($imageFile, $imageObject) = each %imageFiles)
919        {
920        if ($imageObject->ReferenceCount())
921            {  $imageFilesToUpdate{$imageFile} = 1;  };
922        };
923    };
924
925
926# Function: UnbuiltFilesWithContent
927# Returns an existence hashref of the <FileNames> that have Natural Docs content but are not part of <FilesToBuild()>.  This is
928# not a copy of the data so don't change it.
929sub UnbuiltFilesWithContent
930    {  return \%unbuiltFilesWithContent;  };
931
932# Function: FilesWithContent
933# Returns and existence hashref of the <FileNames> that have Natural Docs content.
934sub FilesWithContent
935    {
936    # Don't keep this one internally, but there's an easy way to make it.
937    return { %filesToBuild, %unbuiltFilesWithContent };
938    };
939
940
941#
942#   Function: HasContent
943#
944#   Returns whether the <FileName> contains Natural Docs content.
945#
946sub HasContent #(file)
947    {
948    my ($self, $file) = @_;
949
950    if (exists $supportedFiles{$file})
951        {  return $supportedFiles{$file}->HasContent();  }
952    else
953        {  return undef;  };
954    };
955
956
957#
958#   Function: SetHasContent
959#
960#   Sets whether the <FileName> has Natural Docs content or not.
961#
962sub SetHasContent #(file, hasContent)
963    {
964    my ($self, $file, $hasContent) = @_;
965
966    if (exists $supportedFiles{$file} && $supportedFiles{$file}->HasContent() != $hasContent)
967        {
968        # If the file now has content...
969        if ($hasContent)
970            {
971            $filesToBuild{$file} = 1;
972            }
973
974        # If the file's content has been removed...
975        else
976            {
977            delete $filesToBuild{$file};  # may not be there
978            $filesToPurge{$file} = 1;
979            };
980
981        $supportedFiles{$file}->SetHasContent($hasContent);
982        };
983    };
984
985
986#
987#   Function: StatusOf
988#
989#   Returns the <FileStatus> of the passed <FileName>.
990#
991sub StatusOf #(file)
992    {
993    my ($self, $file) = @_;
994
995    if (exists $supportedFiles{$file})
996        {  return $supportedFiles{$file}->Status();  }
997    else
998        {  return ::FILE_DOESNTEXIST();  };
999    };
1000
1001
1002#
1003#   Function: DefaultMenuTitleOf
1004#
1005#   Returns the default menu title of the <FileName>.  If one isn't specified, it returns the <FileName>.
1006#
1007sub DefaultMenuTitleOf #(file)
1008    {
1009    my ($self, $file) = @_;
1010
1011    if (exists $supportedFiles{$file})
1012        {  return $supportedFiles{$file}->DefaultMenuTitle();  }
1013    else
1014        {  return $file;  };
1015    };
1016
1017
1018#
1019#   Function: SetDefaultMenuTitle
1020#
1021#   Sets the <FileName's> default menu title.
1022#
1023sub SetDefaultMenuTitle #(file, menuTitle)
1024    {
1025    my ($self, $file, $menuTitle) = @_;
1026
1027    if (exists $supportedFiles{$file} && $supportedFiles{$file}->DefaultMenuTitle() ne $menuTitle)
1028        {
1029        $supportedFiles{$file}->SetDefaultMenuTitle($menuTitle);
1030        NaturalDocs::Menu->OnDefaultTitleChange($file);
1031        };
1032    };
1033
1034
1035#
1036#   Function: MostUsedLanguage
1037#
1038#   Returns the name of the most used language in the source trees.  Does not include text files.
1039#
1040sub MostUsedLanguage
1041    {  return $mostUsedLanguage;  };
1042
1043
1044
1045
1046###############################################################################
1047# Group: Image File Functions
1048
1049
1050#
1051#   Function: ImageFileExists
1052#   Returns whether the passed image file exists.
1053#
1054sub ImageFileExists #(FileName file) => bool
1055    {
1056    my ($self, $file) = @_;
1057
1058    if (!exists $imageFiles{$file})
1059        {  $file = $insensitiveImageFiles{lc($file)};  };
1060
1061    return (exists $imageFiles{$file} && $imageFiles{$file}->Status() != ::FILE_DOESNTEXIST());
1062    };
1063
1064
1065#
1066#   Function: ImageFileDimensions
1067#   Returns the dimensions of the passed image file as the array ( width, height ).  Returns them both as undef if it cannot be
1068#   determined.
1069#
1070sub ImageFileDimensions #(FileName file) => (int, int)
1071    {
1072    my ($self, $file) = @_;
1073
1074    if (!exists $imageFiles{$file})
1075        {  $file = $insensitiveImageFiles{lc($file)};  };
1076
1077    my $object = $imageFiles{$file};
1078    if (!$object)
1079        {  die "Tried to get the dimensions of an image that doesn't exist.";  };
1080
1081    if ($object->Width() == -1)
1082        {  $self->DetermineImageDimensions($file);  };
1083
1084    return ($object->Width(), $object->Height());
1085    };
1086
1087
1088#
1089#   Function: ImageFileCapitalization
1090#   Returns the properly capitalized version of the passed image <FileName>.  Image file paths are treated as case insensitive
1091#   regardless of whether the underlying operating system is or not, so we have to make sure the final version matches the
1092#   capitalization of the actual file.
1093#
1094sub ImageFileCapitalization #(FileName file) => FileName
1095    {
1096    my ($self, $file) = @_;
1097
1098    if (exists $imageFiles{$file})
1099        {  return $file;  }
1100    elsif (exists $insensitiveImageFiles{lc($file)})
1101        {  return $insensitiveImageFiles{lc($file)};  }
1102    else
1103        {  die "Tried to get the capitalization of an image file that doesn't exist.";  };
1104    };
1105
1106
1107#
1108#   Function: AddImageFileReference
1109#   Adds a reference to the passed image <FileName>.
1110#
1111sub AddImageFileReference #(FileName imageFile)
1112    {
1113    my ($self, $imageFile) = @_;
1114
1115    if (!exists $imageFiles{$imageFile})
1116        {  $imageFile = $insensitiveImageFiles{lc($imageFile)};  };
1117
1118    my $imageFileInfo = $imageFiles{$imageFile};
1119
1120    if ($imageFileInfo == undef || $imageFileInfo->Status() == ::FILE_DOESNTEXIST())
1121        {  die "Tried to add a reference to a non-existant image file.";  };
1122
1123    if ($imageFileInfo->AddReference() == 1)
1124        {
1125        delete $imageFilesToPurge{$imageFile};
1126
1127        if (!$imageFileInfo->WasUsed() ||
1128            $imageFileInfo->Status() == ::FILE_NEW() ||
1129            $imageFileInfo->Status() == ::FILE_CHANGED())
1130            {  $imageFilesToUpdate{$imageFile} = 1;  };
1131        };
1132    };
1133
1134
1135#
1136#   Function: DeleteImageFileReference
1137#   Deletes a reference from the passed image <FileName>.
1138#
1139sub DeleteImageFileReference #(FileName imageFile)
1140    {
1141    my ($self, $imageFile) = @_;
1142
1143    if (!exists $imageFiles{$imageFile})
1144        {  $imageFile = $insensitiveImageFiles{lc($imageFile)};  };
1145
1146    if (!exists $imageFiles{$imageFile})
1147        {  die "Tried to delete a reference to a non-existant image file.";  };
1148
1149    if ($imageFiles{$imageFile}->DeleteReference() == 0)
1150        {
1151        delete $imageFilesToUpdate{$imageFile};
1152
1153        if ($imageFiles{$imageFile}->WasUsed())
1154            {  $imageFilesToPurge{$imageFile} = 1;  };
1155        };
1156    };
1157
1158
1159#
1160#   Function: ImageFilesToUpdate
1161#   Returns an existence hashref of image <FileNames> that need to be updated.  *Do not change.*
1162#
1163sub ImageFilesToUpdate
1164    {  return \%imageFilesToUpdate;  };
1165
1166
1167#
1168#   Function: ImageFilesToPurge
1169#   Returns an existence hashref of image <FileNames> that need to be updated.  *Do not change.*
1170#
1171sub ImageFilesToPurge
1172    {  return \%imageFilesToPurge;  };
1173
1174
1175
1176###############################################################################
1177# Group: Support Functions
1178
1179#
1180#   Function: GetAllSupportedFiles
1181#
1182#   Gets all the supported files in the passed directory and its subdirectories and puts them into <supportedFiles>.  The only
1183#   attribute that will be set is <NaturalDocs::Project::SourceFile->LastModified()>.  Also sets <mostUsedLanguage>.
1184#
1185sub GetAllSupportedFiles
1186    {
1187    my ($self) = @_;
1188
1189    my @directories = @{NaturalDocs::Settings->InputDirectories()};
1190    my $isCaseSensitive = NaturalDocs::File->IsCaseSensitive();
1191
1192    # Keys are language names, values are counts.
1193    my %languageCounts;
1194
1195
1196    # Make an existence hash of excluded directories.
1197
1198    my %excludedDirectories;
1199    my $excludedDirectoryArrayRef = NaturalDocs::Settings->ExcludedInputDirectories();
1200
1201    foreach my $excludedDirectory (@$excludedDirectoryArrayRef)
1202        {
1203        if ($isCaseSensitive)
1204            {  $excludedDirectories{$excludedDirectory} = 1;  }
1205        else
1206            {  $excludedDirectories{lc($excludedDirectory)} = 1;  };
1207        };
1208
1209
1210    my $imagesOnly;
1211    my $language;
1212
1213    while (scalar @directories)
1214        {
1215        my $directory = pop @directories;
1216
1217        opendir DIRECTORYHANDLE, $directory;
1218        my @entries = readdir DIRECTORYHANDLE;
1219        closedir DIRECTORYHANDLE;
1220
1221        @entries = NaturalDocs::File->NoUpwards(@entries);
1222
1223        foreach my $entry (@entries)
1224            {
1225            my $fullEntry = NaturalDocs::File->JoinPaths($directory, $entry);
1226
1227            # If an entry is a directory, recurse.
1228            if (-d $fullEntry)
1229                {
1230                # Join again with the noFile flag set in case the platform handles them differently.
1231                $fullEntry = NaturalDocs::File->JoinPaths($directory, $entry, 1);
1232
1233                if ($isCaseSensitive)
1234                    {
1235                    if (!exists $excludedDirectories{$fullEntry})
1236                        {  push @directories, $fullEntry;  };
1237                    }
1238                else
1239                    {
1240                    if (!exists $excludedDirectories{lc($fullEntry)})
1241                        {  push @directories, $fullEntry;  };
1242                    };
1243                }
1244
1245            # Otherwise add it if it's a supported extension.
1246            else
1247                {
1248                my $extension = NaturalDocs::File->ExtensionOf($entry);
1249
1250                if (exists $imageFileExtensions{lc($extension)})
1251                    {
1252                    my $fileObject = NaturalDocs::Project::ImageFile->New( (stat($fullEntry))[9], ::FILE_NEW(), 0 );
1253                    $imageFiles{$fullEntry} = $fileObject;
1254
1255                    my $lcFullEntry = lc($fullEntry);
1256
1257                    if (!exists $insensitiveImageFiles{$lcFullEntry} ||
1258                        ($fullEntry cmp $insensitiveImageFiles{$lcFullEntry}) < 0)
1259                        {
1260                        $insensitiveImageFiles{$lcFullEntry} = $fullEntry;
1261                        };
1262                    }
1263                elsif (!$imagesOnly && ($language = NaturalDocs::Languages->LanguageOf($fullEntry)) )
1264                    {
1265                    my $fileObject = NaturalDocs::Project::SourceFile->New();
1266                    $fileObject->SetLastModified(( stat($fullEntry))[9] );
1267                    $supportedFiles{$fullEntry} = $fileObject;
1268
1269                    $languageCounts{$language->Name()}++;
1270                    };
1271                };
1272            };
1273
1274
1275        # After we run out of source directories, add the image directories.
1276
1277        if (scalar @directories == 0 && !$imagesOnly)
1278            {
1279            $imagesOnly = 1;
1280            @directories = @{NaturalDocs::Settings->ImageDirectories()};
1281            };
1282        };
1283
1284
1285    my $topCount = 0;
1286
1287    while (my ($language, $count) = each %languageCounts)
1288        {
1289        if ($count > $topCount && $language ne 'Text File')
1290            {
1291            $topCount = $count;
1292            $mostUsedLanguage = $language;
1293            };
1294        };
1295    };
1296
1297
1298#
1299#   Function: DetermineImageDimensions
1300#
1301#   Attempts to determine the dimensions of the passed image and apply them to their object in <imageFiles>.  Will set them to
1302#   undef if they can't be determined.
1303#
1304sub DetermineImageDimensions #(FileName imageFile)
1305    {
1306    my ($self, $imageFile) = @_;
1307
1308    my $imageFileObject = $imageFiles{$imageFile};
1309    if (!defined $imageFileObject)
1310        {  die "Tried to determine image dimensions of a file with no object.";  };
1311
1312    my $extension = lc( NaturalDocs::File->ExtensionOf($imageFile) );
1313    my ($width, $height);
1314
1315    if ($imageFileExtensions{$extension})
1316        {
1317        open(FH_IMAGEFILE, '<' . $imageFile)
1318            or die 'Could not open ' . $imageFile . "\n";
1319        binmode(FH_IMAGEFILE);
1320
1321        my $raw;
1322
1323        if ($extension eq 'gif')
1324            {
1325            read(FH_IMAGEFILE, $raw, 6);
1326
1327            if ($raw eq 'GIF87a' || $raw eq 'GIF89a')
1328                {
1329                read(FH_IMAGEFILE, $raw, 4);
1330                ($width, $height) = unpack('vv', $raw);
1331                };
1332            }
1333
1334        elsif ($extension eq 'png')
1335            {
1336            read(FH_IMAGEFILE, $raw, 8);
1337
1338            if ($raw eq "\x89PNG\x0D\x0A\x1A\x0A")
1339                {
1340                seek(FH_IMAGEFILE, 4, 1);
1341                read(FH_IMAGEFILE, $raw, 4);
1342
1343                if ($raw eq 'IHDR')
1344                    {
1345                    read(FH_IMAGEFILE, $raw, 8);
1346                    ($width, $height) = unpack('NN', $raw);
1347                    };
1348                };
1349            }
1350
1351        elsif ($extension eq 'bmp')
1352            {
1353            read(FH_IMAGEFILE, $raw, 2);
1354
1355            if ($raw eq 'BM')
1356                {
1357                seek(FH_IMAGEFILE, 16, 1);
1358                read(FH_IMAGEFILE, $raw, 8);
1359
1360                ($width, $height) = unpack('VV', $raw);
1361                };
1362            }
1363
1364        elsif ($extension eq 'jpg' || $extension eq 'jpeg')
1365            {
1366            read(FH_IMAGEFILE, $raw, 2);
1367            my $isOkay = ($raw eq "\xFF\xD8");
1368
1369            while ($isOkay)
1370                {
1371                read(FH_IMAGEFILE, $raw, 4);
1372                my ($marker, $code, $length) = unpack('CCn', $raw);
1373
1374                $isOkay = ($marker eq 0xFF);
1375
1376                if ($isOkay)
1377                    {
1378                    if ($code >= 0xC0 && $code <= 0xC3)
1379                        {
1380                        read(FH_IMAGEFILE, $raw, 5);
1381                        ($height, $width) = unpack('xnn', $raw);
1382                        last;
1383                        }
1384
1385                    else
1386                        {
1387                        $isOkay = seek(FH_IMAGEFILE, $length - 2, 1);
1388                        };
1389                    };
1390                };
1391            };
1392
1393        close(FH_IMAGEFILE);
1394        };
1395
1396
1397    # Sanity check the values.  Although images can theoretically be bigger than 5000, most won't.  The worst that happens in this
1398    # case is just that they don't get length and width values in the output anyway.
1399    if ($width > 0 && $width < 5000 && $height > 0 && $height < 5000)
1400        {  $imageFileObject->SetDimensions($width, $height);  }
1401    else
1402        {  $imageFileObject->SetDimensions(undef, undef);  };
1403    };
1404
1405
14061;
1407