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