1###############################################################################
2#
3#   Package: NaturalDocs::Settings
4#
5###############################################################################
6#
7#   A package to handle the command line and various other program settings.
8#
9###############################################################################
10
11# This file is part of Natural Docs, which is Copyright � 2003-2010 Greg Valure
12# Natural Docs is licensed under version 3 of the GNU Affero General Public License (AGPL)
13# Refer to License.txt for the complete details
14
15use Cwd ();
16
17use NaturalDocs::Settings::BuildTarget;
18
19use strict;
20use integer;
21
22package NaturalDocs::Settings;
23
24
25###############################################################################
26# Group: Information
27
28=pod begin nd
29
30    Topic: Usage and Dependencies
31
32        - The <Constant Functions> can be called immediately.
33
34        - Prior to initialization, <NaturalDocs::Builder> must have all its output packages registered.
35
36        - To initialize, call <Load()>.  All functions except <InputDirectoryNameOf()> will then be available.
37
38        - <GenerateDirectoryNames()> must be called before <InputDirectoryNameOf()> will work.  Currently it is called by
39          <NaturalDocs::Menu->LoadAndUpdate()>.
40
41
42    Architecture: Internal Overview
43
44        - <Load()> first parses the command line, gathering all the settings and checking for errors.  All <NaturalDocs::Builder>
45          packages must be registered before this is called because it needs their command line options.
46          <NaturalDocs::Project->ReparseEverything()> and <NaturalDocs::Project->RebuildEverything()> are called right away if -r
47          or -ro are used.
48
49        - Output directories are *not* named at this point.  See <Named Directories>.
50
51        - The previous settings from the last time Natural Docs was run are loaded and compared to the current settings.
52          <NaturalDocs::Project->ReparseEverything()> and <NaturalDocs::Project->RebuildEverything()> are called if there are
53          any differences that warrant it.
54
55        - It then waits for <GenerateDirectoryNames()> to be called by <NaturalDocs::Menu>.  The reason for this is that the
56          previous directory names are stored as hints in the menu file, for reasons explained in <Named Directories>.  Once that
57          happens all the unnamed directories have names generated for them so everything is named.  The package is completely
58          set up.
59
60        - The input directories are stored in an array instead of a hash because the order they were declared in matters.  If two
61          people use multiple input directories on separate computers without sharing a menu file, they should at least get consistent
62          directory names by declaring them in the same order.
63
64
65    Architecture: Named Directories
66
67        Ever since Natural Docs introduced multiple input directories in 1.16, they've had to be named.  Since they don't necessarily
68        extend from the same root anymore, they can't share an output directory without the risk of file name conflicts.  There was
69        an early attempt at giving them actual names, but now they're just numbered from 1.
70
71        Directory names aren't generated right away.  It waits for <Menu.txt> to load because that holds the obfuscated names from
72        the last run.  <NaturalDocs::Menu> then calls <GenerateDirectoryNames()> and passes those along as hints.
73        <GenerateDirectoryNames()> then applies them to any matches and generates new ones for any remaining.  This is done so
74        that output page locations can remain consistent when built on multiple computers, so long as the menu file is shared.  I tend
75        to think the menu file is the most likely configuration file to be shared.
76
77
78    Architecture: Removed Directories
79
80        Directories that were part of the previous run but aren't anymore are still stored in the package.  The primary reason, though
81        there may be others, is file purging.  If an input directory is removed, all the output files that were generated from anything
82        in it need to be removed.  To find out what the output file name was for a removed source file, it needs to be able to split it
83        from it's original input directory and know what that directory was named.  If this didn't happen those output files would be
84        orphaned, as was the case prior to 1.32.
85
86=cut
87
88
89
90###############################################################################
91# Group: Variables
92
93
94# handle: PREVIOUS_SETTINGS_FILEHANDLE
95# The file handle used with <PreviousSettings.nd>.
96
97# array: inputDirectories
98# An array of input directories.
99my @inputDirectories;
100
101# array: inputDirectoryNames
102# An array of the input directory names.  Each name corresponds to the directory of the same index in <inputDirectories>.
103my @inputDirectoryNames;
104
105# array: imageDirectories
106# An array of image directories.
107my @imageDirectories;
108
109# array: imageDirectoryNames
110# An array of the image directory names.  Each name corresponds to the directory of the same index in <imageDirectories>.
111my @imageDirectoryNames;
112
113# array: relativeImageDirectories
114# An array of the relative paths for images.  The asterisks found in the command line are not present.
115my @relativeImageDirectories;
116
117# array: excludedInputDirectories
118# An array of input directories to exclude.
119my @excludedInputDirectories;
120
121# array: removedInputDirectories
122# An array of input directories that were once in the command line but are no longer.
123my @removedInputDirectories;
124
125# array: removedInputDirectoryNames
126# An array of the removed input directories' names.  Each name corresponds to the directory of the same index in
127# <removedInputDirectories>.
128my @removedInputDirectoryNames;
129
130# array: removedImageDirectories
131# An array of image directories that were once in the command line but are no longer.
132my @removedImageDirectories;
133
134# array: removedImageDirectoryNames
135# An array of the removed image directories' names.  Each name corresponds to the directory of the same index in
136# <removedImageDirectories>.
137my @removedImageDirectoryNames;
138
139# var: projectDirectory
140# The project directory.
141my $projectDirectory;
142
143# array: buildTargets
144# An array of <NaturalDocs::Settings::BuildTarget>s.
145my @buildTargets;
146
147# var: documentedOnly
148# Whether undocumented code aspects should be included in the output.
149my $documentedOnly;
150
151# int: tabLength
152# The number of spaces in tabs.
153my $tabLength;
154
155# bool: noAutoGroup
156# Whether auto-grouping is turned off.
157my $noAutoGroup;
158
159# bool: onlyFileTitles
160# Whether source files should always use the file name as the title.
161my $onlyFileTitles;
162
163# bool: isQuiet
164# Whether the script should be run in quiet mode or not.
165my $isQuiet;
166
167# bool: rebuildData
168# WHether most data files should be ignored and rebuilt.
169my $rebuildData;
170
171# array: styles
172# An array of style names to use, most important first.
173my @styles;
174
175# var: highlightCode
176# Whether syntax highlighting should be applied to code tags.
177my $highlightCode;
178
179# var: highlightAnonymous
180# Whether syntax highlighting should be applied to anonymous code tags.
181my $highlightAnonymous;
182
183
184###############################################################################
185# Group: Files
186
187
188#
189#   File: PreviousSettings.nd
190#
191#   Stores the previous command line settings.
192#
193#   Format:
194#
195#       > [BINARY_FORMAT]
196#       > [VersionInt: app version]
197#
198#       The file starts with the standard <BINARY_FORMAT> <VersionInt> header.
199#
200#       > [UInt8: tab length]
201#       > [UInt8: documented only (0 or 1)]
202#       > [UInt8: no auto-group (0 or 1)]
203#       > [UInt8: only file titles (0 or 1)]
204#		> [UInt8: highlight code (0 or 1)]
205#		> [UInt8: highlight anonymous (0 or 1)]
206#       >
207#       > [UInt8: number of input directories]
208#       > [UString16: input directory] [UString16: input directory name] ...
209#
210#       A count of input directories, then that number of directory/name pairs.
211#
212#       > [UInt8: number of output targets]
213#       > [UString16: output directory] [UString16: output format command line option] ...
214#
215#       A count of output targets, then that number of directory/format pairs.
216#
217#
218#   Revisions:
219#
220#		1.52:
221#
222#			- Changed AString16s to UString16s.
223#
224#		1.51:
225#
226#			- Removed charset.
227#
228#		1.5:
229#
230#			- Added highlight code and highlight anonymous.
231#
232#       1.4:
233#
234#           - Added only file titles.
235#
236#       1.33:
237#
238#           - Added charset.
239#
240#       1.3:
241#
242#           - Removed headers-only, which was a 0/1 UInt8 after tab length.
243#           - Change auto-group level (1 = no, 2 = yes, 3 = full only) to no auto-group (0 or 1).
244#
245#       1.22:
246#
247#           - Added auto-group level.
248#
249#       1.2:
250#
251#           - File was added to the project.  Prior to 1.2, it didn't exist.
252#
253
254
255###############################################################################
256# Group: Action Functions
257
258#
259#   Function: Load
260#
261#   Loads and parses all settings from the command line and configuration files.  Will exit if the options are invalid or the syntax
262#   reference was requested.
263#
264sub Load
265    {
266    my ($self) = @_;
267
268    $self->ParseCommandLine();
269    $self->LoadAndComparePreviousSettings();
270    };
271
272
273#
274#   Function: Save
275#
276#   Saves all settings in configuration files to disk.
277#
278sub Save
279    {
280    my ($self) = @_;
281
282    $self->SavePreviousSettings();
283    };
284
285
286#
287#   Function: GenerateDirectoryNames
288#
289#   Generates names for each of the input and image directories, which can later be retrieved with <InputDirectoryNameOf()>
290#   and <ImageDirectoryNameOf()>.
291#
292#   Parameters:
293#
294#       inputHints - A hashref of suggested input directory names, where the keys are the directories and the values are the names.
295#                        These take precedence over anything generated.  You should include names for directories that are no longer in
296#                        the command line.  This parameter may be undef.
297#       imageHints - Same as inputHints, only for the image directories.
298#
299sub GenerateDirectoryNames #(hashref inputHints, hashref imageHints)
300    {
301    my ($self, $inputHints, $imageHints) = @_;
302
303    my %usedInputNames;
304    my %usedImageNames;
305
306
307    if (defined $inputHints)
308        {
309        # First, we have to convert all non-numeric names to numbers, since they may come from a pre-1.32 menu file.  We do it
310        # here instead of in NaturalDocs::Menu to keep the naming scheme centralized.
311
312        my @names = values %$inputHints;
313        my $hasNonNumeric;
314
315        foreach my $name (@names)
316            {
317            if ($name !~ /^[0-9]+$/)
318                {
319                $hasNonNumeric = 1;
320                last;
321                };
322            };
323
324
325        if ($hasNonNumeric)
326            {
327            # Hash mapping old names to new names.
328            my %conversion;
329
330            # The sequential number to use.  Starts at two because we want 'default' to be one.
331            my $currentNumber = 2;
332
333            # If there's only one name, we set it to one no matter what it was set to before.
334            if (scalar @names == 1)
335                {  $conversion{$names[0]} = 1;  }
336            else
337                {
338                # We sort the list first because we want the end result to be predictable.  This conversion could be happening on many
339                # machines, and they may not all specify the input directories in the same order.  They need to all come up with the
340                # same result.
341                @names = sort @names;
342
343                foreach my $name (@names)
344                    {
345                    if ($name eq 'default')
346                        {  $conversion{$name} = 1;  }
347                    else
348                        {
349                        $conversion{$name} = $currentNumber;
350                        $currentNumber++;
351                        };
352                    };
353                };
354
355            # Convert them to the new names.
356            foreach my $directory (keys %$inputHints)
357                {
358                $inputHints->{$directory} = $conversion{ $inputHints->{$directory} };
359                };
360            };
361
362
363        # Now we apply all the names from the hints, and save any unused ones as removed directories.
364
365        for (my $i = 0; $i < scalar @inputDirectories; $i++)
366            {
367            if (exists $inputHints->{$inputDirectories[$i]})
368                {
369                $inputDirectoryNames[$i] = $inputHints->{$inputDirectories[$i]};
370                $usedInputNames{ $inputDirectoryNames[$i] } = 1;
371                delete $inputHints->{$inputDirectories[$i]};
372                };
373            };
374
375
376        # Any remaining hints are saved as removed directories.
377
378        while (my ($directory, $name) = each %$inputHints)
379            {
380            push @removedInputDirectories, $directory;
381            push @removedInputDirectoryNames, $name;
382            };
383        };
384
385
386    if (defined $imageHints)
387        {
388        # Image directory names were never non-numeric, so there is no conversion.  Apply all the names from the hints.
389
390        for (my $i = 0; $i < scalar @imageDirectories; $i++)
391            {
392            if (exists $imageHints->{$imageDirectories[$i]})
393                {
394                $imageDirectoryNames[$i] = $imageHints->{$imageDirectories[$i]};
395                $usedImageNames{ $imageDirectoryNames[$i] } = 1;
396                delete $imageHints->{$imageDirectories[$i]};
397                };
398            };
399
400
401        # Any remaining hints are saved as removed directories.
402
403        while (my ($directory, $name) = each %$imageHints)
404            {
405            push @removedImageDirectories, $directory;
406            push @removedImageDirectoryNames, $name;
407            };
408        };
409
410
411    # Now we generate names for anything remaining.
412
413    my $inputCounter = 1;
414
415    for (my $i = 0; $i < scalar @inputDirectories; $i++)
416        {
417        if (!defined $inputDirectoryNames[$i])
418            {
419            while (exists $usedInputNames{$inputCounter})
420                {  $inputCounter++;  };
421
422            $inputDirectoryNames[$i] = $inputCounter;
423            $usedInputNames{$inputCounter} = 1;
424
425            $inputCounter++;
426            };
427        };
428
429
430    my $imageCounter = 1;
431
432    for (my $i = 0; $i < scalar @imageDirectories; $i++)
433        {
434        if (!defined $imageDirectoryNames[$i])
435            {
436            while (exists $usedImageNames{$imageCounter})
437                {  $imageCounter++;  };
438
439            $imageDirectoryNames[$i] = $imageCounter;
440            $usedImageNames{$imageCounter} = 1;
441
442            $imageCounter++;
443            };
444        };
445    };
446
447
448
449###############################################################################
450# Group: Information Functions
451
452
453#
454#   Function: InputDirectories
455#
456#   Returns an arrayref of input directories.  Do not change.
457#
458#   This will not return any removed input directories.
459#
460sub InputDirectories
461    {  return \@inputDirectories;  };
462
463#
464#   Function: InputDirectoryNameOf
465#
466#   Returns the generated name of the passed input directory.  <GenerateDirectoryNames()> must be called once before this
467#   function is available.
468#
469#   If a name for a removed input directory is available, it will be returned as well.
470#
471sub InputDirectoryNameOf #(directory)
472    {
473    my ($self, $directory) = @_;
474
475    for (my $i = 0; $i < scalar @inputDirectories; $i++)
476        {
477        if ($directory eq $inputDirectories[$i])
478            {  return $inputDirectoryNames[$i];  };
479        };
480
481    for (my $i = 0; $i < scalar @removedInputDirectories; $i++)
482        {
483        if ($directory eq $removedInputDirectories[$i])
484            {  return $removedInputDirectoryNames[$i];  };
485        };
486
487    return undef;
488    };
489
490
491#
492#   Function: SplitFromInputDirectory
493#
494#   Takes an input file name and returns the array ( inputDirectory, relativePath ).
495#
496#   If the file cannot be split from an input directory, it will try to do it with the removed input directories.
497#
498sub SplitFromInputDirectory #(file)
499    {
500    my ($self, $file) = @_;
501
502    foreach my $directory (@inputDirectories, @removedInputDirectories)
503        {
504        if (NaturalDocs::File->IsSubPathOf($directory, $file))
505            {  return ( $directory, NaturalDocs::File->MakeRelativePath($directory, $file) );  };
506        };
507
508    return ( );
509    };
510
511
512#
513#   Function: ImageDirectories
514#
515#   Returns an arrayref of image directories.  Do not change.
516#
517#   This will not return any removed image directories.
518#
519sub ImageDirectories
520    {  return \@imageDirectories;  };
521
522
523#
524#   Function: ImageDirectoryNameOf
525#
526#   Returns the generated name of the passed image or input directory.  <GenerateDirectoryNames()> must be called once before
527#   this function is available.
528#
529#   If a name for a removed input or image directory is available, it will be returned as well.
530#
531sub ImageDirectoryNameOf #(directory)
532    {
533    my ($self, $directory) = @_;
534
535    for (my $i = 0; $i < scalar @imageDirectories; $i++)
536        {
537        if ($directory eq $imageDirectories[$i])
538            {  return $imageDirectoryNames[$i];  };
539        };
540
541    for (my $i = 0; $i < scalar @removedImageDirectories; $i++)
542        {
543        if ($directory eq $removedImageDirectories[$i])
544            {  return $removedImageDirectoryNames[$i];  };
545        };
546
547    return undef;
548    };
549
550
551#
552#   Function: SplitFromImageDirectory
553#
554#   Takes an input image file name and returns the array ( imageDirectory, relativePath ).
555#
556#   If the file cannot be split from an image directory, it will try to do it with the removed image directories.
557#
558sub SplitFromImageDirectory #(file)
559    {
560    my ($self, $file) = @_;
561
562    foreach my $directory (@imageDirectories, @removedImageDirectories)
563        {
564        if (NaturalDocs::File->IsSubPathOf($directory, $file))
565            {  return ( $directory, NaturalDocs::File->MakeRelativePath($directory, $file) );  };
566        };
567
568    return ( );
569    };
570
571
572#
573#   Function: RelativeImageDirectories
574#
575#   Returns an arrayref of relative image directories.  Do not change.
576#
577sub RelativeImageDirectories
578    {  return \@relativeImageDirectories;  };
579
580
581# Function: ExcludedInputDirectories
582# Returns an arrayref of input directories to exclude.  Do not change.
583sub ExcludedInputDirectories
584    {  return \@excludedInputDirectories;  };
585
586
587# Function: BuildTargets
588# Returns an arrayref of <NaturalDocs::Settings::BuildTarget>s.  Do not change.
589sub BuildTargets
590    {  return \@buildTargets;  };
591
592
593#
594#   Function: OutputDirectoryOf
595#
596#   Returns the output directory of a builder object.
597#
598#   Parameters:
599#
600#       object - The builder object, whose class is derived from <NaturalDocs::Builder::Base>.
601#
602#   Returns:
603#
604#       The builder directory, or undef if the object wasn't found..
605#
606sub OutputDirectoryOf #(object)
607    {
608    my ($self, $object) = @_;
609
610    foreach my $buildTarget (@buildTargets)
611        {
612        if ($buildTarget->Builder() == $object)
613            {  return $buildTarget->Directory();  };
614        };
615
616    return undef;
617    };
618
619
620# Function: Styles
621# Returns an arrayref of the styles associated with the output.
622sub Styles
623    {  return \@styles;  };
624
625# Function: ProjectDirectory
626# Returns the project directory.
627sub ProjectDirectory
628    {  return $projectDirectory;  };
629
630# Function: ProjectDataDirectory
631# Returns the project data directory.
632sub ProjectDataDirectory
633    {  return NaturalDocs::File->JoinPaths($projectDirectory, 'Data', 1);  };
634
635# Function: StyleDirectory
636# Returns the main style directory.
637sub StyleDirectory
638    {  return NaturalDocs::File->JoinPaths($FindBin::RealBin, 'Styles', 1);  };
639
640# Function: JavaScriptDirectory
641# Returns the main JavaScript directory.
642sub JavaScriptDirectory
643    {  return NaturalDocs::File->JoinPaths($FindBin::RealBin, 'JavaScript', 1);  };
644
645# Function: ConfigDirectory
646# Returns the main configuration directory.
647sub ConfigDirectory
648    {  return NaturalDocs::File->JoinPaths($FindBin::RealBin, 'Config', 1);  };
649
650# Function: DocumentedOnly
651# Returns whether undocumented code aspects should be included in the output.
652sub DocumentedOnly
653    {  return $documentedOnly;  };
654
655# Function: TabLength
656# Returns the number of spaces tabs should be expanded to.
657sub TabLength
658    {  return $tabLength;  };
659
660# Function: NoAutoGroup
661# Returns whether auto-grouping is turned off.
662sub NoAutoGroup
663    {  return $noAutoGroup;  };
664
665# Function: OnlyFileTitles
666# Returns whether source files should always use the file name as the title.
667sub OnlyFileTitles
668    {  return $onlyFileTitles;  };
669
670# Function: IsQuiet
671# Returns whether the script should be run in quiet mode or not.
672sub IsQuiet
673    {  return $isQuiet;  };
674
675# Function: RebuildData
676# Returns whether all data files should be ignored and rebuilt.
677sub RebuildData
678    {  return $rebuildData;  };
679
680# Function: HighlightCode
681# Returns whether to apply syntax highlighting (start code) sections.
682sub HighlightCode
683	{  return $highlightCode;  }
684
685# Function: HighlightAnonymous
686# Returns whether to apply syntax highlighting to anonymous code sections designated with :, >, or |.
687sub HighlightAnonymous
688	{  return $highlightAnonymous;  }
689
690
691###############################################################################
692# Group: Constant Functions
693
694#
695#   Function: AppVersion
696#
697#   Returns Natural Docs' version number as an integer.  Use <TextAppVersion()> to get a printable version.
698#
699sub AppVersion
700    {
701    my ($self) = @_;
702    return NaturalDocs::Version->FromString($self->TextAppVersion());
703    };
704
705#
706#   Function: TextAppVersion
707#
708#   Returns Natural Docs' version number as plain text.
709#
710sub TextAppVersion
711    {
712    return '1.52';
713    };
714
715#
716#   Function: AppURL
717#
718#   Returns a string of the project's current web address.
719#
720sub AppURL
721    {  return 'http://www.naturaldocs.org';  };
722
723
724
725###############################################################################
726# Group: Support Functions
727
728
729#
730#   Function: ParseCommandLine
731#
732#   Parses and validates the command line.  Will cause the script to exit if the options ask for the syntax reference or
733#   are invalid.
734#
735sub ParseCommandLine
736    {
737    my ($self) = @_;
738
739    my %synonyms = ( 'input'    => '-i',
740                                  'source' => '-i',
741                                  'excludeinput' => '-xi',
742                                  'excludesource' => '-xi',
743                                  'images' => '-img',
744                                  'output'  => '-o',
745                                  'project' => '-p',
746                                  'documentedonly' => '-do',
747                                  'style'    => '-s',
748                                  'rebuild' => '-r',
749                                  'rebuildoutput' => '-ro',
750                                  'tablength' => '-t',
751                                  'quiet'    => '-q',
752                                  'headersonly' => '-ho',
753                                  'help'     => '-h',
754                                  'autogroup' => '-ag',
755                                  'noautogroup' => '-nag',
756                                  'onlyfiletitles' => '-oft',
757                                  'onlyfiletitle' => '-oft',
758                                  'highlight' => '-hl',
759                                  'highlighting' => '-hl' );
760
761
762    my @errorMessages;
763
764    my $valueRef;
765    my $option;
766
767    my @outputStrings;
768    my @imageStrings;
769    my $highlightString;
770
771
772    # Sometimes $valueRef is set to $ignored instead of undef because we don't want certain errors to cause other,
773    # unnecessary errors.  For example, if they set the input directory twice, we want to show that error and swallow the
774    # specified directory without complaint.  Otherwise it would complain about the directory too as if it were random crap
775    # inserted into the command line.
776    my $ignored;
777
778    my $index = 0;
779
780    while ($index < scalar @ARGV)
781        {
782        my $arg = $ARGV[$index];
783
784        if (substr($arg, 0, 1) eq '-')
785            {
786            $option = lc($arg);
787
788            # Support options like -t2 as well as -t 2.
789            if ($option =~ /^([^0-9]+)([0-9]+)$/)
790                {
791                $option = $1;
792                splice(@ARGV, $index + 1, 0, $2);
793                };
794
795            # Convert long forms to short.
796            if (substr($option, 1, 1) eq '-')
797                {
798                # Strip all dashes.
799                my $newOption = $option;
800                $newOption =~ tr/-//d;
801
802                if (exists $synonyms{$newOption})
803                    {  $option = $synonyms{$newOption};  }
804                }
805
806            if ($option eq '-i')
807                {
808                push @inputDirectories, undef;
809                $valueRef = \$inputDirectories[-1];
810                }
811            elsif ($option eq '-xi')
812                {
813                push @excludedInputDirectories, undef;
814                $valueRef = \$excludedInputDirectories[-1];
815                }
816            elsif ($option eq '-img')
817                {
818                push @imageStrings, undef;
819                $valueRef = \$imageStrings[-1];
820                }
821            elsif ($option eq '-p')
822                {
823                if (defined $projectDirectory)
824                    {
825                    push @errorMessages, 'You cannot have more than one project directory.';
826                    $valueRef = \$ignored;
827                    }
828                else
829                    {  $valueRef = \$projectDirectory;  };
830                }
831            elsif ($option eq '-o')
832                {
833                push @outputStrings, undef;
834                $valueRef = \$outputStrings[-1];
835                }
836            elsif ($option eq '-s')
837                {
838                $valueRef = \$styles[0];
839                }
840            elsif ($option eq '-t')
841                {
842                $valueRef = \$tabLength;
843                }
844            elsif ($option eq '-hl')
845            	{
846            	$valueRef = \$highlightString;
847            	}
848            elsif ($option eq '-ag')
849                {
850                push @errorMessages, 'The -ag setting is no longer supported.  You can use -nag (--no-auto-group) to turn off '
851                                               . "auto-grouping, but there aren't multiple levels anymore.";
852                $valueRef = \$ignored;
853                }
854
855            # Options that aren't followed by content.
856            else
857                {
858                $valueRef = undef;
859
860                if ($option eq '-r')
861                    {
862                    NaturalDocs::Project->ReparseEverything();
863                    NaturalDocs::Project->RebuildEverything();
864                    $rebuildData = 1;
865                    }
866                elsif ($option eq '-ro')
867                    {
868                    NaturalDocs::Project->RebuildEverything();
869                    }
870                elsif ($option eq '-do')
871                    {  $documentedOnly = 1;  }
872                elsif ($option eq '-oft')
873                    {  $onlyFileTitles = 1;  }
874                elsif ($option eq '-q')
875                    {  $isQuiet = 1;  }
876                elsif ($option eq '-ho')
877                    {
878                    push @errorMessages, 'The -ho setting is no longer supported.  You can have Natural Docs skip over the source file '
879                                                   . 'extensions by editing Languages.txt in your project directory.';
880                    }
881                elsif ($option eq '-nag')
882                    {  $noAutoGroup = 1;  }
883                elsif ($option eq '-?' || $option eq '-h')
884                    {
885                    $self->PrintSyntax();
886                    exit;
887                    }
888                else
889                    {  push @errorMessages, 'Unrecognized option ' . $option;  };
890
891                };
892
893            }
894
895        # Is a segment of text, not an option...
896        else
897            {
898            if (defined $valueRef)
899                {
900                # We want to preserve spaces in paths.
901                if (defined $$valueRef)
902                    {  $$valueRef .= ' ';  };
903
904                $$valueRef .= $arg;
905                }
906
907            else
908                {
909                push @errorMessages, 'Unrecognized element ' . $arg;
910                };
911            };
912
913        $index++;
914        };
915
916
917    # Validate the style, if specified.
918
919    if ($styles[0])
920        {
921        my @stylePieces = split(/ +/, $styles[0]);
922        @styles = ( );
923
924        while (scalar @stylePieces)
925            {
926            if (lc($stylePieces[0]) eq 'custom')
927                {
928                push @errorMessages, 'The "Custom" style setting is no longer supported.  Copy your custom style sheet to your '
929                                               . 'project directory and you can refer to it with -s.';
930                shift @stylePieces;
931                }
932            else
933                {
934                # People may use styles with spaces in them.  If a style doesn't exist, we need to join the pieces until we find one that
935                # does or we run out of pieces.
936
937                my $extras = 0;
938                my $success;
939
940                while ($extras < scalar @stylePieces)
941                    {
942                    my $style;
943
944                    if (!$extras)
945                        {  $style = $stylePieces[0];  }
946                    else
947                        {  $style = join(' ', @stylePieces[0..$extras]);  };
948
949                    my $cssFile = NaturalDocs::File->JoinPaths( $self->StyleDirectory(), $style . '.css' );
950                    if (-e $cssFile)
951                        {
952                        push @styles, $style;
953                        splice(@stylePieces, 0, 1 + $extras);
954                        $success = 1;
955                        last;
956                        }
957                    else
958                        {
959                        $cssFile = NaturalDocs::File->JoinPaths( $self->ProjectDirectory(), $style . '.css' );
960
961                        if (-e $cssFile)
962                            {
963                            push @styles, $style;
964                            splice(@stylePieces, 0, 1 + $extras);
965                            $success = 1;
966                            last;
967                            }
968                        else
969                            {  $extras++;  };
970                        };
971                    };
972
973                if (!$success)
974                    {
975                    push @errorMessages, 'The style "' . $stylePieces[0] . '" does not exist.';
976                    shift @stylePieces;
977                    };
978                };
979            };
980        }
981    else
982        {  @styles = ( 'Default' );  };
983
984
985    # Decode and validate the output strings.
986
987    my %outputDirectories;
988
989    foreach my $outputString (@outputStrings)
990        {
991        my ($format, $directory) = split(/ /, $outputString, 2);
992
993        if (!defined $directory)
994            {  push @errorMessages, 'The -o option needs two parameters: -o [format] [directory]';  }
995        else
996            {
997            if (!NaturalDocs::File->PathIsAbsolute($directory))
998                {  $directory = NaturalDocs::File->JoinPaths(Cwd::cwd(), $directory, 1);  };
999
1000            $directory = NaturalDocs::File->CanonizePath($directory);
1001
1002            if (! -e $directory || ! -d $directory)
1003                {
1004                # They may have forgotten the format portion and the directory name had a space in it.
1005                if (-e ($format . ' ' . $directory) && -d ($format . ' ' . $directory))
1006                    {
1007                    push @errorMessages, 'The -o option needs two parameters: -o [format] [directory]';
1008                    $format = undef;
1009                    }
1010                else
1011                    {  push @errorMessages, 'The output directory ' . $directory . ' does not exist.';  }
1012                }
1013            elsif (exists $outputDirectories{$directory})
1014                {  push @errorMessages, 'You cannot specify the output directory ' . $directory . ' more than once.';  }
1015            else
1016                {  $outputDirectories{$directory} = 1;  };
1017
1018            if (defined $format)
1019                {
1020                my $builderPackage = NaturalDocs::Builder->OutputPackageOf($format);
1021
1022                if (defined $builderPackage)
1023                    {
1024                    push @buildTargets,
1025                            NaturalDocs::Settings::BuildTarget->New($builderPackage->New(), $directory);
1026                    }
1027                else
1028                    {
1029                    push @errorMessages, 'The output format ' . $format . ' doesn\'t exist or is not installed.';
1030                    $valueRef = \$ignored;
1031                    };
1032                };
1033            };
1034        };
1035
1036    if (!scalar @buildTargets)
1037        {  push @errorMessages, 'You did not specify an output directory.';  };
1038
1039
1040    # Decode and validate the image strings.
1041
1042    foreach my $imageString (@imageStrings)
1043        {
1044        if ($imageString =~ /^ *\*/)
1045            {
1046            # The below NaturalDocs::File functions assume everything is canonized.
1047            $imageString = NaturalDocs::File->CanonizePath($imageString);
1048
1049            my ($volume, $directoryString) = NaturalDocs::File->SplitPath($imageString, 1);
1050            my @directories = NaturalDocs::File->SplitDirectories($directoryString);
1051
1052            shift @directories;
1053
1054            $directoryString = NaturalDocs::File->JoinDirectories(@directories);
1055            push @relativeImageDirectories, NaturalDocs::File->JoinPath($volume, $directoryString);
1056            }
1057        else
1058            {
1059            if (!NaturalDocs::File->PathIsAbsolute($imageString))
1060                {  $imageString = NaturalDocs::File->JoinPaths(Cwd::cwd(), $imageString, 1);  };
1061
1062            $imageString = NaturalDocs::File->CanonizePath($imageString);
1063
1064            if (! -e $imageString || ! -d $imageString)
1065                {  push @errorMessages, 'The image directory ' . $imageString . ' does not exist.';  };
1066
1067            push @imageDirectories, $imageString;
1068            };
1069        };
1070
1071
1072    # Make sure the input and project directories are specified, canonized, and exist.
1073
1074    if (scalar @inputDirectories)
1075        {
1076        for (my $i = 0; $i < scalar @inputDirectories; $i++)
1077            {
1078            if (!NaturalDocs::File->PathIsAbsolute($inputDirectories[$i]))
1079                {  $inputDirectories[$i] = NaturalDocs::File->JoinPaths(Cwd::cwd(), $inputDirectories[$i], 1);  };
1080
1081            $inputDirectories[$i] = NaturalDocs::File->CanonizePath($inputDirectories[$i]);
1082
1083            if (! -e $inputDirectories[$i] || ! -d $inputDirectories[$i])
1084                {  push @errorMessages, 'The input directory ' . $inputDirectories[$i] . ' does not exist.';  };
1085            };
1086        }
1087    else
1088        {  push @errorMessages, 'You did not specify an input (source) directory.';  };
1089
1090    if (defined $projectDirectory)
1091        {
1092        if (!NaturalDocs::File->PathIsAbsolute($projectDirectory))
1093            {  $projectDirectory = NaturalDocs::File->JoinPaths(Cwd::cwd(), $projectDirectory, 1);  };
1094
1095        $projectDirectory = NaturalDocs::File->CanonizePath($projectDirectory);
1096
1097        if (! -e $projectDirectory || ! -d $projectDirectory)
1098            {  push @errorMessages, 'The project directory ' . $projectDirectory . ' does not exist.';  };
1099
1100        # Create the Data subdirectory if it doesn't exist.
1101        NaturalDocs::File->CreatePath( NaturalDocs::File->JoinPaths($projectDirectory, 'Data', 1) );
1102        }
1103    else
1104        {  push @errorMessages, 'You did not specify a project directory.';  };
1105
1106
1107    # Make sure the excluded input directories are canonized, and add the project and output directories to the list.
1108
1109    for (my $i = 0; $i < scalar @excludedInputDirectories; $i++)
1110        {
1111        if (!NaturalDocs::File->PathIsAbsolute($excludedInputDirectories[$i]))
1112            {  $excludedInputDirectories[$i] = NaturalDocs::File->JoinPaths(Cwd::cwd(), $excludedInputDirectories[$i], 1);  };
1113
1114        $excludedInputDirectories[$i] = NaturalDocs::File->CanonizePath($excludedInputDirectories[$i]);
1115        };
1116
1117    push @excludedInputDirectories, $projectDirectory;
1118
1119    foreach my $buildTarget (@buildTargets)
1120        {
1121        push @excludedInputDirectories, $buildTarget->Directory();
1122        };
1123
1124
1125    # Determine the tab length, and default to four if not specified.
1126
1127    if (defined $tabLength)
1128        {
1129        if ($tabLength !~ /^[0-9]+$/)
1130            {  push @errorMessages, 'The tab length must be a number.';  };
1131        }
1132    else
1133        {  $tabLength = 4;  };
1134
1135
1136    # Decode and validate the highlight setting.
1137
1138    if (defined $highlightString)
1139    	{
1140    	$highlightString = lc($highlightString);
1141
1142    	if ($highlightString eq 'off')
1143    		{
1144    		$highlightCode = undef;
1145    		$highlightAnonymous = undef;
1146    		}
1147    	elsif ($highlightString eq 'code')
1148    		{
1149    		$highlightCode = 1;
1150    		$highlightAnonymous = undef;
1151    		}
1152    	elsif ($highlightString eq 'all')
1153    		{
1154    		$highlightCode = 1;
1155    		$highlightAnonymous = 1;
1156    		}
1157    	else
1158    		{  push @errorMessages, $highlightString . ' is not a valid value for --highlight.';  }
1159    	}
1160    else
1161    	{
1162    	$highlightCode = 1;
1163    	$highlightAnonymous = undef;
1164    	}
1165
1166
1167    # Exit with the error message if there was one.
1168
1169    if (scalar @errorMessages)
1170        {
1171        print join("\n", @errorMessages) . "\nType NaturalDocs -h to see the syntax reference.\n";
1172        exit;
1173        };
1174    };
1175
1176#
1177#   Function: PrintSyntax
1178#
1179#   Prints the syntax reference.
1180#
1181sub PrintSyntax
1182    {
1183    my ($self) = @_;
1184
1185    # Make sure all line lengths are under 80 characters.
1186
1187    print
1188
1189    "Natural Docs, version " . $self->TextAppVersion() . "\n"
1190    . $self->AppURL() . "\n"
1191    . "This program is licensed under version 3 of the AGPL\n"
1192    . "Refer to License.txt for the complete details\n"
1193    . "--------------------------------------\n"
1194    . "\n"
1195    . "Syntax:\n"
1196    . "\n"
1197    . "    NaturalDocs -i [input (source) directory]\n"
1198    . "               (-i [input (source) directory] ...)\n"
1199    . "                -o [output format] [output directory]\n"
1200    . "               (-o [output format] [output directory] ...)\n"
1201    . "                -p [project directory]\n"
1202    . "                [options]\n"
1203    . "\n"
1204    . "Examples:\n"
1205    . "\n"
1206    . "    NaturalDocs -i C:\\My Project\\Source -o HTML C:\\My Project\\Docs\n"
1207    . "                -p C:\\My Project\\Natural Docs\n"
1208    . "    NaturalDocs -i /src/project -o HTML /doc/project\n"
1209    . "                -p /etc/naturaldocs/project -s Small -q\n"
1210    . "\n"
1211    . "Required Parameters:\n"
1212    . "\n"
1213    . " -i [dir]\n--input [dir]\n--source [dir]\n"
1214    . "     Specifies an input (source) directory.  Required.\n"
1215    . "     Can be specified multiple times.\n"
1216    . "\n"
1217    . " -o [fmt] [dir]\n--output [fmt] [dir]\n"
1218    . "    Specifies an output format and directory.  Required.\n"
1219    . "    Can be specified multiple times, but only once per directory.\n"
1220    . "    Possible output formats:\n";
1221
1222    $self->PrintOutputFormats('    - ');
1223
1224    print
1225    "\n"
1226    . " -p [dir]\n--project [dir]\n"
1227    . "    Specifies the project directory.  Required.\n"
1228    . "    There needs to be a unique project directory for every source directory.\n"
1229    . "\n"
1230    . "Optional Parameters:\n"
1231    . "\n"
1232    . " -s [style] ([style] [style] ...)\n--style [style] ([style] [style] ...)\n"
1233    . "    Specifies the CSS style when building HTML output.  If multiple styles are\n"
1234    . "    specified, they will all be included in the order given.\n"
1235    . "\n"
1236    . " -img [image directory]\n--image [image directory]\n"
1237    . "    Specifies an image directory.  Can be specified multiple times.\n"
1238    . "    Start with * to specify a relative directory, as in -img */images.\n"
1239    . "\n"
1240    . " -do\n--documented-only\n"
1241    . "    Specifies only documented code aspects should be included in the output.\n"
1242    . "\n"
1243    . " -t [len]\n--tab-length [len]\n"
1244    . "    Specifies the number of spaces tabs should be expanded to.  This only needs\n"
1245    . "    to be set if you use tabs in example code and text diagrams.  Defaults to 4.\n"
1246    . "\n"
1247    . " -xi [dir]\n--exclude-input [dir]\n--exclude-source [dir]\n"
1248    . "    Excludes an input (source) directory from the documentation.\n"
1249    . "    Automatically done for the project and output directories.  Can\n"
1250    . "    be specified multiple times.\n"
1251    . "\n"
1252    . " -nag\n--no-auto-group\n"
1253    . "    Turns off auto-grouping completely.\n"
1254    . "\n"
1255    . " -oft\n--only-file-titles\n"
1256    . "    Source files will only use the file name as the title.\n"
1257    . "\n"
1258    . " -hl [option]\n--highlight [option]\n"
1259    . "    Specifies when syntax highlighting should be applied.  Defaults to code.\n"
1260    . "    off  - No syntax highlighting is applied.\n"
1261    . "    code - Syntax highlighting is only applied to prototypes and (start code)\n"
1262    . "           segments.\n"
1263    . "    all  - Systax highlighting is applied to prototypes, (start code) segments,\n"
1264    . "           and lines starting with >, :, or |."
1265    . "\n"
1266    . " -r\n--rebuild\n"
1267    . "    Rebuilds all output and data files from scratch.\n"
1268    . "    Does not affect the menu file.\n"
1269    . "\n"
1270    . " -ro\n--rebuild-output\n"
1271    . "    Rebuilds all output files from scratch.\n"
1272    . "\n"
1273    . " -q\n--quiet\n"
1274    . "    Suppresses all non-error output.\n"
1275    . "\n"
1276    . " -?\n -h\n--help\n"
1277    . "    Displays this syntax reference.\n";
1278    };
1279
1280
1281#
1282#   Function: PrintOutputFormats
1283#
1284#   Prints all the possible output formats that can be specified with -o.  Each one will be placed on its own line.
1285#
1286#   Parameters:
1287#
1288#       prefix - Characters to prefix each one with, such as for indentation.
1289#
1290sub PrintOutputFormats #(prefix)
1291    {
1292    my ($self, $prefix) = @_;
1293
1294    my $outputPackages = NaturalDocs::Builder::OutputPackages();
1295
1296    foreach my $outputPackage (@$outputPackages)
1297        {
1298        print $prefix . $outputPackage->CommandLineOption() . "\n";
1299        };
1300    };
1301
1302
1303#
1304#   Function: LoadAndComparePreviousSettings
1305#
1306#   Loads <PreviousSettings.nd> and compares the values there with those in the command line.  If differences require it,
1307#   sets <rebuildData> and/or <rebuildOutput>.
1308#
1309sub LoadAndComparePreviousSettings
1310    {
1311    my ($self) = @_;
1312
1313    my $fileIsOkay;
1314
1315    if (!NaturalDocs::Settings->RebuildData())
1316        {
1317        my $version;
1318
1319        if (NaturalDocs::BinaryFile->OpenForReading( NaturalDocs::Project->DataFile('PreviousSettings.nd'),
1320                                                                           NaturalDocs::Version->FromString('1.52') ))
1321            {  $fileIsOkay = 1;  };
1322        };
1323
1324    if (!$fileIsOkay)
1325        {
1326        # We need to reparse everything because --documented-only may have changed.
1327        # We need to rebuild everything because --tab-length may have changed.
1328        NaturalDocs::Project->ReparseEverything();
1329        NaturalDocs::Project->RebuildEverything();
1330        }
1331    else
1332        {
1333        my $raw;
1334
1335        # [UInt8: tab expansion]
1336        # [UInt8: documented only (0 or 1)]
1337        # [UInt8: no auto-group (0 or 1)]
1338        # [UInt8: only file titles (0 or 1)]
1339        # [UInt8: highlight code (0 or 1)]
1340        # [UInt8: highlight anonymous (0 or 1)]
1341
1342        my $prevTabLength = NaturalDocs::BinaryFile->GetUInt8();
1343        my $prevDocumentedOnly = NaturalDocs::BinaryFile->GetUInt8();
1344        my $prevNoAutoGroup = NaturalDocs::BinaryFile->GetUInt8();
1345        my $prevOnlyFileTitles = NaturalDocs::BinaryFile->GetUInt8();
1346        my $prevHighlightCode = NaturalDocs::BinaryFile->GetUInt8();
1347        my $prevHighlightAnonymous = NaturalDocs::BinaryFile->GetUInt8();
1348
1349        if ($prevDocumentedOnly == 0)
1350            {  $prevDocumentedOnly = undef;  };
1351        if ($prevNoAutoGroup == 0)
1352            {  $prevNoAutoGroup = undef;  };
1353        if ($prevOnlyFileTitles == 0)
1354            {  $prevOnlyFileTitles = undef;  };
1355        if ($prevHighlightCode == 0)
1356            {  $prevHighlightCode = undef;  };
1357        if ($prevHighlightAnonymous == 0)
1358            {  $prevHighlightAnonymous = undef;  };
1359
1360        if ($prevTabLength != $self->TabLength() ||
1361        	$prevHighlightCode != $self->HighlightCode() ||
1362        	$prevHighlightAnonymous != $self->HighlightAnonymous())
1363            {
1364            NaturalDocs::Project->RebuildEverything();
1365            };
1366
1367        if ($prevDocumentedOnly != $self->DocumentedOnly() ||
1368            $prevNoAutoGroup != $self->NoAutoGroup() ||
1369            $prevOnlyFileTitles != $self->OnlyFileTitles())
1370            {
1371            NaturalDocs::Project->ReparseEverything();
1372            };
1373
1374
1375        # [UInt8: number of input directories]
1376
1377        my $inputDirectoryCount = NaturalDocs::BinaryFile->GetUInt8();
1378
1379        while ($inputDirectoryCount)
1380            {
1381            # [UString16: input directory] [UString16: input directory name] ...
1382
1383            my $inputDirectory = NaturalDocs::BinaryFile->GetUString16();
1384            my $inputDirectoryName = NaturalDocs::BinaryFile->GetUString16();
1385
1386            # Not doing anything with this for now.
1387
1388            $inputDirectoryCount--;
1389            };
1390
1391
1392        # [UInt8: number of output targets]
1393
1394        my $outputTargetCount = NaturalDocs::BinaryFile->GetUInt8();
1395
1396        # Keys are the directories, values are the command line options.
1397        my %previousOutputDirectories;
1398
1399        while ($outputTargetCount)
1400            {
1401            # [UString16: output directory] [UString16: output format command line option] ...
1402
1403            my $outputDirectory = NaturalDocs::BinaryFile->GetUString16();
1404            my $outputCommand = NaturalDocs::BinaryFile->GetUString16();
1405
1406            $previousOutputDirectories{$outputDirectory} = $outputCommand;
1407
1408            $outputTargetCount--;
1409            };
1410
1411        # Check if any targets were added to the command line, or if their formats changed.  We don't care if targets were
1412        # removed.
1413        my $buildTargets = $self->BuildTargets();
1414
1415        foreach my $buildTarget (@$buildTargets)
1416            {
1417            if (!exists $previousOutputDirectories{$buildTarget->Directory()} ||
1418                $buildTarget->Builder()->CommandLineOption() ne $previousOutputDirectories{$buildTarget->Directory()})
1419                {
1420                NaturalDocs::Project->RebuildEverything();
1421                last;
1422                };
1423            };
1424
1425        NaturalDocs::BinaryFile->Close();
1426        };
1427    };
1428
1429
1430#
1431#   Function: SavePreviousSettings
1432#
1433#   Saves the settings into <PreviousSettings.nd>.
1434#
1435sub SavePreviousSettings
1436    {
1437    my ($self) = @_;
1438
1439    NaturalDocs::BinaryFile->OpenForWriting(  NaturalDocs::Project->DataFile('PreviousSettings.nd') );
1440
1441    # [UInt8: tab length]
1442    # [UInt8: documented only (0 or 1)]
1443    # [UInt8: no auto-group (0 or 1)]
1444    # [UInt8: only file titles (0 or 1)]
1445    # [UInt8: highlight code (0 or 1)]
1446    # [UInt8: highlight anonymous (0 or 1)]
1447    # [UInt8: number of input directories]
1448
1449    my $inputDirectories = $self->InputDirectories();
1450
1451    NaturalDocs::BinaryFile->WriteUInt8($self->TabLength());
1452    NaturalDocs::BinaryFile->WriteUInt8($self->DocumentedOnly() ? 1 : 0);
1453    NaturalDocs::BinaryFile->WriteUInt8($self->NoAutoGroup() ? 1 : 0);
1454    NaturalDocs::BinaryFile->WriteUInt8($self->OnlyFileTitles() ? 1 : 0);
1455    NaturalDocs::BinaryFile->WriteUInt8($self->HighlightCode() ? 1 : 0);
1456    NaturalDocs::BinaryFile->WriteUInt8($self->HighlightAnonymous() ? 1 : 0);
1457    NaturalDocs::BinaryFile->WriteUInt8(scalar @$inputDirectories);
1458
1459    foreach my $inputDirectory (@$inputDirectories)
1460        {
1461        my $inputDirectoryName = $self->InputDirectoryNameOf($inputDirectory);
1462
1463        # [UString16: input directory] [UString16: input directory name] ...
1464        NaturalDocs::BinaryFile->WriteUString16($inputDirectory);
1465        NaturalDocs::BinaryFile->WriteUString16($inputDirectoryName);
1466        };
1467
1468    # [UInt8: number of output targets]
1469
1470    my $buildTargets = $self->BuildTargets();
1471    NaturalDocs::BinaryFile->WriteUInt8(scalar @$buildTargets);
1472
1473    foreach my $buildTarget (@$buildTargets)
1474        {
1475        # [UString16: output directory] [UString16: output format command line option] ...
1476        NaturalDocs::BinaryFile->WriteUString16( $buildTarget->Directory() );
1477        NaturalDocs::BinaryFile->WriteUString16( $buildTarget->Builder()->CommandLineOption() );
1478        };
1479
1480    NaturalDocs::BinaryFile->Close();
1481    };
1482
1483
14841;
1485