OSDN Git Service

Version 5.91
[vbslib/main.git] / GPL_bin_fullset / NaturalDocs / Modules / NaturalDocs / Topics.pm
1 ###############################################################################
2 #
3 #   Package: NaturalDocs::Topics
4 #
5 ###############################################################################
6 #
7 #   The topic constants and functions to convert them to and from strings used throughout the script.  All constants are exported
8 #   by default.
9 #
10 ###############################################################################
11
12 # This file is part of Natural Docs, which is Copyright © 2003-2010 Greg Valure
13 # Natural Docs is licensed under version 3 of the GNU Affero General Public License (AGPL)
14 # Refer to License.txt for the complete details
15
16 use Text::Wrap ( );
17 use Tie::RefHash ( );
18
19 use strict;
20 use integer;
21
22 use NaturalDocs::Topics::Type;
23
24 package NaturalDocs::Topics;
25
26 use base 'Exporter';
27 our @EXPORT = ( 'TOPIC_GENERAL', 'TOPIC_GENERIC', 'TOPIC_GROUP', 'TOPIC_CLASS', 'TOPIC_FILE', 'TOPIC_FUNCTION',
28                           'TOPIC_VARIABLE', 'TOPIC_PROPERTY', 'TOPIC_TYPE', 'TOPIC_ENUMERATION', 'TOPIC_CONSTANT',
29                           'TOPIC_INTERFACE', 'TOPIC_EVENT', 'TOPIC_DELEGATE', 'TOPIC_SECTION' );
30
31
32
33 ###############################################################################
34 # Group: Types
35
36
37 #
38 #   Type: TopicType
39 #
40 #   A string representing a topic type as defined in <Topics.txt>.  It's format should be treated as opaque; use <MakeTopicType()>
41 #   to get them from topic names.  However, they can be compared for equality with string functions.
42 #
43
44
45 #
46 #   Constants: Default TopicTypes
47 #
48 #   Exported constants of the default <TopicTypes>, so you don't have to go through <TypeFromName()> every time.
49 #
50 #   TOPIC_GENERAL - The general <TopicType>, which has the special meaning of none in particular.
51 #   TOPIC_GENERIC - Generic <TopicType>.
52 #   TOPIC_GROUP - Group <TopicType>.
53 #   TOPIC_CLASS - Class <TopicType>.
54 #   TOPIC_INTERFACE - Interface <TopicType>.
55 #   TOPIC_FILE - File <TopicType>.
56 #   TOPIC_SECTION - Section <TopicType>.
57 #   TOPIC_FUNCTION - Function <TopicType>.
58 #   TOPIC_VARIABLE - Variable <TopicType>.
59 #   TOPIC_PROPERTY - Property <TopicType>.
60 #   TOPIC_TYPE - Type <TopicType>.
61 #   TOPIC_CONSTANT - Constant <TopicType>.
62 #   TOPIC_ENUMERATION - Enum <TopicType>.
63 #   TOPIC_DELEGATE - Delegate <TopicType>.
64 #   TOPIC_EVENT - Event <TopicType>.
65 #
66 use constant TOPIC_GENERAL => 'general';
67 use constant TOPIC_GENERIC => 'generic';
68 use constant TOPIC_GROUP => 'group';
69 use constant TOPIC_CLASS => 'class';
70 use constant TOPIC_INTERFACE => 'interface';
71 use constant TOPIC_FILE => 'file';
72 use constant TOPIC_SECTION => 'section';
73 use constant TOPIC_FUNCTION => 'function';
74 use constant TOPIC_VARIABLE => 'variable';
75 use constant TOPIC_PROPERTY => 'property';
76 use constant TOPIC_TYPE => 'type';
77 use constant TOPIC_CONSTANT => 'constant';
78 use constant TOPIC_ENUMERATION => 'enumeration';
79 use constant TOPIC_DELEGATE => 'delegate';
80 use constant TOPIC_EVENT => 'event';
81 # Dependency: The values of these constants must match what is generated by MakeTopicType().
82 # Dependency: These types must be added to requiredTypeNames so that they always exist.
83
84
85
86
87 ###############################################################################
88 # Group: Variables
89
90
91 #
92 #   handle: FH_TOPICS
93 #
94 #   The file handle used when writing to <Topics.txt>.
95 #
96
97
98 #
99 #   hash: types
100 #
101 #   A hashref that maps <TopicTypes> to <NaturalDocs::Topics::Type>s.
102 #
103 my %types;
104
105
106 #
107 #   hash: names
108 #
109 #   A hashref that maps various forms of the all-lowercase type names to <TopicTypes>.  All are in the same hash because
110 #   two names that reduce to the same thing it would cause big problems, and we need to catch that.  Keys include
111 #
112 #   - Topic names
113 #   - Plural topic names
114 #   - Alphanumeric-only topic names
115 #   - Alphanumeric-only plural topic names
116 #
117 my %names;
118
119
120 #
121 #   hash: keywords
122 #
123 #   A hashref that maps all-lowercase keywords to their <TopicTypes>.  Must not have any of the same keys as
124 #   <pluralKeywords>.
125 #
126 my %keywords;
127
128
129 #
130 #   hash: pluralKeywords
131 #
132 #   A hashref that maps all-lowercase plural keywords to their <TopicTypes>.  Must not have any of the same keys as
133 #   <keywords>.
134 #
135 my %pluralKeywords;
136
137
138 #
139 #   hash: indexable
140 #
141 #   An existence hash of all the indexable <TopicTypes>.
142 #
143 my %indexable;
144
145
146 #
147 #   array: requiredTypeNames
148 #
149 #   An array of the <TopicType> names which are required to be defined in the main file.  Are in the order they should appear
150 #   when reformatting.
151 #
152 my @requiredTypeNames = ( 'Generic', 'Class', 'Interface', 'Section', 'File', 'Group', 'Function', 'Variable', 'Property', 'Type',
153                                            'Constant', 'Enumeration', 'Event', 'Delegate' );
154
155
156 #
157 #   array: legacyTypes
158 #
159 #   An array that converts the legacy topic types, which were numeric constants prior to 1.3, to the current <TopicTypes>.
160 #   The legacy types are used as an index into the array.  Note that this does not support list type values.
161 #
162 my @legacyTypes = ( TOPIC_GENERAL, TOPIC_CLASS, TOPIC_SECTION, TOPIC_FILE, TOPIC_GROUP, TOPIC_FUNCTION,
163                                 TOPIC_VARIABLE, TOPIC_GENERIC, TOPIC_TYPE, TOPIC_CONSTANT, TOPIC_PROPERTY );
164
165
166 #
167 #   array: mainTopicNames
168 #
169 #   An array of the <TopicType> names that are defined in the main <Topics.txt>.
170 #
171 my @mainTopicNames;
172
173
174
175 ###############################################################################
176 # Group: Files
177
178
179 #
180 #   File: Topics.txt
181 #
182 #   The configuration file that defines or overrides the topic definitions for Natural Docs.  One version sits in Natural Docs'
183 #   configuration directory, and another can be in a project directory to add to or override them.
184 #
185 #   > # [comments]
186 #
187 #   Everything after a # symbol is ignored.
188 #
189 #   Except when specifying topic names, everything below is case-insensitive.
190 #
191 #   > Format: [version]
192 #
193 #   Specifies the file format version of the file.
194 #
195 #
196 #   Sections:
197 #
198 #       > Ignore[d] Keyword[s]: [keyword], [keyword] ...
199 #       >    [keyword]
200 #       >    [keyword], [keyword]
201 #       >    ...
202 #
203 #       Ignores the keywords so that they're not recognized as Natural Docs topics anymore.  Can be specified as a list on the same
204 #       line and/or following like a normal Keywords section.
205 #
206 #       > Topic Type: [name]
207 #       > Alter Topic Type: [name]
208 #
209 #       Creates a new topic type or alters an existing one.  The name can only contain <CFChars> and isn't case sensitive, although
210 #       the original case is remembered for presentation.
211 #
212 #       The name General is reserved.  There are a number of default types that must be defined in the main file as well, but those
213 #       are governed by <NaturalDocs::Topics> and are not included here.  The default types can have their keywords or behaviors
214 #       changed, though, either by editing the default file or by overriding them in the user file.
215 #
216 #       Enumeration is a special type.  It is indexed with Types and its definition list members are listed with Constants according
217 #       to the rules in <Languages.txt>.
218 #
219 #
220 #   Topic Type Sections:
221 #
222 #       > Plural: [name]
223 #
224 #       Specifies the plural name of the topic type.  Defaults to the singular name.  Has the same restrictions as the topic type
225 #       name.
226 #
227 #       > Index: [yes|no]
228 #
229 #       Whether the topic type gets an index.  Defaults to yes.
230 #
231 #       > Scope: [normal|start|end|always global]
232 #
233 #       How the topic affects scope.  Defaults to normal.
234 #
235 #       normal - The topic stays within the current scope.
236 #       start - The topic starts a new scope for all the topics beneath it, like class topics.
237 #       end - The topic resets the scope back to global for all the topics beneath it, like section topics.
238 #       always global - The topic is defined as a global symbol, but does not change the scope for any other topics.
239 #
240 #       > Class Hierarchy: [yes|no]
241 #
242 #       Whether the topic is part of the class hierarchy.  Defaults to no.
243 #
244 #       > Page Title if First: [yes|no]
245 #
246 #       Whether the title of this topic becomes the page title if it is the first topic in a file.  Defaults to no.
247 #
248 #       > Break Lists: [yes|no]
249 #
250 #       Whether list topics should be broken into individual topics in the output.  Defaults to no.
251 #
252 #       > Can Group With: [topic type], [topic type], ...
253 #
254 #       The list of <TopicTypes> the topic can possibly be grouped with.
255 #
256 #       > [Add] Keyword[s]:
257 #       >    [keyword]
258 #       >    [keyword], [plural keyword]
259 #       >    ...
260 #
261 #       A list of the topic type's keywords.  Each line after the heading is the keyword and optionally its plural form.  This continues
262 #       until the next line in "keyword: value" format.  "Add" isn't required.
263 #
264 #       - Keywords can only have letters and numbers.  No punctuation or spaces are allowed.
265 #       - Keywords are not case sensitive.
266 #       - Subsequent keyword sections add to the list.  They don't replace it.
267 #       - Keywords can be redefined by other keyword sections.
268 #
269 #
270 #   Revisions:
271 #
272 #       1.3:
273 #
274 #           The initial version of this file.
275 #
276
277
278 ###############################################################################
279 # Group: File Functions
280
281
282 #
283 #   Function: Load
284 #
285 #   Loads both the master and the project version of <Topics.txt>.
286 #
287 sub Load
288     {
289     my $self = shift;
290
291     # Add the special General topic type.
292
293     $types{::TOPIC_GENERAL()} = NaturalDocs::Topics::Type->New('General', 'General', 1, ::SCOPE_NORMAL(), undef);
294     $names{'general'} = ::TOPIC_GENERAL();
295     $indexable{::TOPIC_GENERAL()} = 1;
296     # There are no keywords for the general topic.
297
298
299     $self->LoadFile(1);  # Main
300
301     # Dependency: All the default topic types must be checked for existence.
302
303     # Check to see if the required types are defined.
304     foreach my $name (@requiredTypeNames)
305         {
306         if (!exists $names{lc($name)})
307             {  NaturalDocs::ConfigFile->AddError('The ' . $name . ' topic type must be defined in the main topics file.');  };
308         };
309
310     my $errorCount = NaturalDocs::ConfigFile->ErrorCount();
311
312     if ($errorCount)
313         {
314         NaturalDocs::ConfigFile->PrintErrorsAndAnnotateFile();
315         NaturalDocs::Error->SoftDeath('There ' . ($errorCount == 1 ? 'is an error' : 'are ' . $errorCount . ' errors')
316                                                     . ' in ' . NaturalDocs::Project->MainConfigFile('Topics.txt'));
317         }
318
319
320     $self->LoadFile();  # User
321
322     $errorCount = NaturalDocs::ConfigFile->ErrorCount();
323
324     if ($errorCount)
325         {
326         NaturalDocs::ConfigFile->PrintErrorsAndAnnotateFile();
327         NaturalDocs::Error->SoftDeath('There ' . ($errorCount == 1 ? 'is an error' : 'are ' . $errorCount . ' errors')
328                                                     . ' in ' . NaturalDocs::Project->UserConfigFile('Topics.txt'));
329         }
330     };
331
332
333 #
334 #   Function: LoadFile
335 #
336 #   Loads a particular version of <Topics.txt>.
337 #
338 #   Parameters:
339 #
340 #       isMain - Whether the file is the main file or not.
341 #
342 sub LoadFile #(isMain)
343     {
344     my ($self, $isMain) = @_;
345
346     my ($file, $status);
347
348     if ($isMain)
349         {
350         $file = NaturalDocs::Project->MainConfigFile('Topics.txt');
351         $status = NaturalDocs::Project->MainConfigFileStatus('Topics.txt');
352         }
353     else
354         {
355         $file = NaturalDocs::Project->UserConfigFile('Topics.txt');
356         $status = NaturalDocs::Project->UserConfigFileStatus('Topics.txt');
357         };
358
359     my $version;
360
361     if ($version = NaturalDocs::ConfigFile->Open($file))
362         {
363         # The format hasn't changed since the file was introduced.
364
365         if ($status == ::FILE_CHANGED())
366             {  NaturalDocs::Project->ReparseEverything();  };
367
368         my ($topicTypeKeyword, $topicTypeName, $topicType, $topicTypeObject, $inKeywords, $inIgnoredKeywords);
369
370         # Keys are topic type objects, values are unparsed strings.
371         my %canGroupWith;
372         tie %canGroupWith, 'Tie::RefHash';
373
374         while (my ($keyword, $value) = NaturalDocs::ConfigFile->GetLine())
375             {
376             if ($keyword)
377                 {
378                 $inKeywords = 0;
379                 $inIgnoredKeywords = 0;
380                 };
381
382             if ($keyword eq 'topic type')
383                 {
384                 $topicTypeKeyword = $keyword;
385                 $topicTypeName = $value;
386
387                 # Resolve conflicts and create the type if necessary.
388
389                 $topicType = $self->MakeTopicType($topicTypeName);
390                 my $lcTopicTypeName = lc($topicTypeName);
391
392                 my $lcTopicTypeAName = $lcTopicTypeName;
393                 $lcTopicTypeAName =~ tr/a-z0-9//cd;
394
395                 if (!NaturalDocs::ConfigFile->HasOnlyCFChars($topicTypeName))
396                     {
397                     NaturalDocs::ConfigFile->AddError('Topic names can only have ' . NaturalDocs::ConfigFile->CFCharNames() . '.');
398                     }
399                 elsif ($topicType eq ::TOPIC_GENERAL())
400                     {
401                     NaturalDocs::ConfigFile->AddError('You cannot define a General topic type.');
402                     }
403                 elsif (defined $types{$topicType} || defined $names{$lcTopicTypeName} || defined $names{$lcTopicTypeAName})
404                     {
405                     NaturalDocs::ConfigFile->AddError('Topic type ' . $topicTypeName . ' is already defined or its name is too '
406                                                                      . 'similar to an existing name.  Use Alter Topic Type if you meant to override '
407                                                                      . 'its settings.');
408                     }
409                 else
410                     {
411                     $topicTypeObject = NaturalDocs::Topics::Type->New($topicTypeName, $topicTypeName, 1, ::SCOPE_NORMAL(),
412                                                                                                   0, 0);
413
414                     $types{$topicType} = $topicTypeObject;
415                     $names{$lcTopicTypeName} = $topicType;
416                     $names{$lcTopicTypeAName} = $topicType;
417
418                     $indexable{$topicType} = 1;
419
420                     if ($isMain)
421                         {  push @mainTopicNames, $topicTypeName;  };
422                     };
423                 }
424
425             elsif ($keyword eq 'alter topic type')
426                 {
427                 $topicTypeKeyword = $keyword;
428                 $topicTypeName = $value;
429
430                 # Resolve conflicts and create the type if necessary.
431
432                 $topicType = $names{lc($topicTypeName)};
433
434                 if (!defined $topicType)
435                     {  NaturalDocs::ConfigFile->AddError('Topic type ' . $topicTypeName . ' doesn\'t exist.');  }
436                 elsif ($topicType eq ::TOPIC_GENERAL())
437                     {  NaturalDocs::ConfigFile->AddError('You cannot alter the General topic type.');  }
438                 else
439                     {
440                     $topicTypeObject = $types{$topicType};
441                     };
442                 }
443
444             elsif ($keyword =~ /^ignored? keywords?$/)
445                 {
446                 $inIgnoredKeywords = 1;
447
448                 my @ignoredKeywords = split(/ ?, ?/, lc($value));
449
450                 foreach my $ignoredKeyword (@ignoredKeywords)
451                     {
452                     delete $keywords{$ignoredKeyword};
453                     delete $pluralKeywords{$ignoredKeyword};
454                     };
455                 }
456
457             # We continue even if there are errors in the topic type line so that we can find any other errors in the file as well.  We'd
458             # rather them all show up at once instead of them showing up one at a time between Natural Docs runs.  So we just ignore
459             # the settings if $topicTypeObject is undef.
460
461
462             elsif ($keyword eq 'plural')
463                 {
464                 my $pluralName = $value;
465                 my $lcPluralName = lc($pluralName);
466
467                 my $lcPluralAName = $lcPluralName;
468                 $lcPluralAName =~ tr/a-zA-Z0-9//cd;
469
470                 if (!NaturalDocs::ConfigFile->HasOnlyCFChars($pluralName))
471                     {
472                     NaturalDocs::ConfigFile->AddError('Plural names can only have '
473                                                                      . NaturalDocs::ConfigFile->CFCharNames() . '.');
474                     }
475                 elsif ($lcPluralAName eq 'general')
476                     {
477                     NaturalDocs::ConfigFile->AddError('You cannot use General as a plural name for ' . $topicTypeName . '.');
478                     }
479                 elsif ( (defined $names{$lcPluralName} && $names{$lcPluralName} ne $topicType) ||
480                          (defined $names{$lcPluralAName} && $names{$lcPluralAName} ne $topicType) )
481                     {
482                     NaturalDocs::ConfigFile->AddError($topicTypeName . "'s plural name, " . $pluralName
483                                                                      . ', is already defined or is too similar to an existing name.');
484                     }
485
486                 elsif (defined $topicTypeObject)
487                     {
488                     $topicTypeObject->SetPluralName($pluralName);
489
490                     $names{$lcPluralName} = $topicType;
491                     $names{$lcPluralAName} = $topicType;
492                     };
493                 }
494
495             elsif ($keyword eq 'index')
496                 {
497                 $value = lc($value);
498
499                 if ($value eq 'yes')
500                     {
501                     if (defined $topicTypeObject)
502                         {
503                         $topicTypeObject->SetIndex(1);
504                         $indexable{$topicType} = 1;
505                         };
506                     }
507                 elsif ($value eq 'no')
508                     {
509                     if (defined $topicTypeObject)
510                         {
511                         $topicTypeObject->SetIndex(0);
512                         delete $indexable{$topicType};
513                         };
514                     }
515                 else
516                     {
517                     NaturalDocs::ConfigFile->AddError('Index lines can only be "yes" or "no".');
518                     };
519                 }
520
521             elsif ($keyword eq 'class hierarchy')
522                 {
523                 $value = lc($value);
524
525                 if ($value eq 'yes')
526                     {
527                     if (defined $topicTypeObject)
528                         {  $topicTypeObject->SetClassHierarchy(1);  };
529                     }
530                 elsif ($value eq 'no')
531                     {
532                     if (defined $topicTypeObject)
533                         {  $topicTypeObject->SetClassHierarchy(0);  };
534                     }
535                 else
536                     {
537                     NaturalDocs::ConfigFile->AddError('Class Hierarchy lines can only be "yes" or "no".');
538                     };
539                 }
540
541             elsif ($keyword eq 'scope')
542                 {
543                 $value = lc($value);
544
545                 if ($value eq 'normal')
546                     {
547                     if (defined $topicTypeObject)
548                         {  $topicTypeObject->SetScope(::SCOPE_NORMAL());  };
549                     }
550                 elsif ($value eq 'start')
551                     {
552                     if (defined $topicTypeObject)
553                         {  $topicTypeObject->SetScope(::SCOPE_START());  };
554                     }
555                 elsif ($value eq 'end')
556                     {
557                     if (defined $topicTypeObject)
558                         {  $topicTypeObject->SetScope(::SCOPE_END());  };
559                     }
560                 elsif ($value eq 'always global')
561                     {
562                     if (defined $topicTypeObject)
563                         {  $topicTypeObject->SetScope(::SCOPE_ALWAYS_GLOBAL());  };
564                     }
565                 else
566                     {
567                     NaturalDocs::ConfigFile->AddError('Scope lines can only be "normal", "start", "end", or "always global".');
568                     };
569                 }
570
571             elsif ($keyword eq 'page title if first')
572                 {
573                 $value = lc($value);
574
575                 if ($value eq 'yes')
576                     {
577                     if (defined $topicTypeObject)
578                         {  $topicTypeObject->SetPageTitleIfFirst(1);  };
579                     }
580                 elsif ($value eq 'no')
581                     {
582                     if (defined $topicTypeObject)
583                         {  $topicTypeObject->SetPageTitleIfFirst(undef);  };
584                     }
585                 else
586                     {
587                     NaturalDocs::ConfigFile->AddError('Page Title if First lines can only be "yes" or "no".');
588                     };
589                 }
590
591             elsif ($keyword eq 'break lists')
592                 {
593                 $value = lc($value);
594
595                 if ($value eq 'yes')
596                     {
597                     if (defined $topicTypeObject)
598                         {  $topicTypeObject->SetBreakLists(1);  };
599                     }
600                 elsif ($value eq 'no')
601                     {
602                     if (defined $topicTypeObject)
603                         {  $topicTypeObject->SetBreakLists(undef);  };
604                     }
605                 else
606                     {
607                     NaturalDocs::ConfigFile->AddError('Break Lists lines can only be "yes" or "no".');
608                     };
609                 }
610
611             elsif ($keyword eq 'can group with')
612                 {
613                 if (defined $topicTypeObject)
614                     {  $canGroupWith{$topicTypeObject} = lc($value);  };
615                 }
616
617             elsif ($keyword =~ /^(?:add )?keywords?$/)
618                 {
619                 $inKeywords = 1;
620                 }
621
622             elsif (defined $keyword)
623                 {  NaturalDocs::ConfigFile->AddError($keyword . ' is not a valid keyword.');  }
624
625             elsif (!$inKeywords && !$inIgnoredKeywords)
626                 {
627                 NaturalDocs::ConfigFile->AddError('All lines in ' . $topicTypeKeyword . ' sections must begin with a keyword.');
628                 }
629
630             else # No keyword but in keyword section.
631                 {
632                 $value = lc($value);
633
634                 if ($value =~ /^([a-z0-9 ]*[a-z0-9]) ?, ?([a-z0-9 ]+)$/)
635                     {
636                     my ($singular, $plural) = ($1, $2);
637
638                     if ($inIgnoredKeywords)
639                         {
640                         delete $keywords{$singular};
641                         delete $keywords{$plural};
642                         delete $pluralKeywords{$singular};
643                         delete $pluralKeywords{$plural};
644                         }
645                     elsif (defined $topicTypeObject)
646                         {
647                         $keywords{$singular} = $topicType;
648                         delete $pluralKeywords{$singular};
649
650                         $pluralKeywords{$plural} = $topicType;
651                         delete $keywords{$plural};
652                         };
653                     }
654                 elsif ($value =~ /^[a-z0-9 ]+$/)
655                     {
656                     if ($inIgnoredKeywords)
657                         {
658                         delete $keywords{$value};
659                         delete $pluralKeywords{$value};
660                         }
661                     elsif (defined $topicType)
662                         {
663                         $keywords{$value} = $topicType;
664                         delete $pluralKeywords{$value};
665                         };
666                     }
667                 else
668                     {
669                     NaturalDocs::ConfigFile->AddError('Keywords can only have letters, numbers, and spaces.  '
670                                                                      . 'Plurals must be separated by a comma.');
671                     };
672                 };
673             };
674
675         NaturalDocs::ConfigFile->Close();
676
677
678         # Parse out the Can Group With lines now that everything's defined.
679
680         while (my ($typeObject, $value) = each %canGroupWith)
681             {
682             my @values = split(/ ?, ?/, $value);
683             my @types;
684
685             foreach my $value (@values)
686                 {
687                 # We're just going to ignore invalid items.
688                 if (exists $names{$value})
689                     {  push @types, $names{$value};  };
690                 };
691
692             if (scalar @types)
693                 {  $typeObject->SetCanGroupWith(\@types);  };
694             };
695         }
696
697     else # couldn't open file
698         {
699         if ($isMain)
700             {  die "Couldn't open topics file " . $file . "\n";  }
701         else
702             {  NaturalDocs::Project->ReparseEverything();  };
703         };
704     };
705
706
707 #
708 #   Function: Save
709 #
710 #   Saves the main and user versions of <Topics.txt>.
711 #
712 sub Save
713     {
714     my $self = shift;
715
716     $self->SaveFile(1); # Main
717     $self->SaveFile(0); # User
718     };
719
720
721 #
722 #   Function: SaveFile
723 #
724 #   Saves a particular version of <Topics.txt>.
725 #
726 #   Parameters:
727 #
728 #       isMain - Whether the file is the main file or not.
729 #
730 sub SaveFile #(isMain)
731     {
732     my ($self, $isMain) = @_;
733
734     my $file;
735
736     if ($isMain)
737         {
738         if (NaturalDocs::Project->MainConfigFileStatus('Topics.txt') == ::FILE_SAME())
739             {  return;  };
740         $file = NaturalDocs::Project->MainConfigFile('Topics.txt');
741         }
742     else
743         {
744         # We have to check the main one two because this lists the topics defined in it.
745         if (NaturalDocs::Project->UserConfigFileStatus('Topics.txt') == ::FILE_SAME() &&
746             NaturalDocs::Project->MainConfigFileStatus('Topics.txt') == ::FILE_SAME())
747             {  return;  };
748         $file = NaturalDocs::Project->UserConfigFile('Topics.txt');
749         };
750
751
752     # Array of topic type names in the order they appear in the file.  If Alter Topic Type is used, the name will end with an asterisk.
753     my @topicTypeOrder;
754
755     # Keys are topic type names, values are property hashrefs.  Hashref keys are the property names, values the value.
756     # For keywords, the key is Keywords and the values are arrayrefs of singular and plural pairs.  If no plural is defined, the entry
757     # will be undef.
758     my %properties;
759
760     # List of ignored keywords specified as Ignore Keywords: [keyword], [keyword], ...
761     my @inlineIgnoredKeywords;
762
763     # List of ignored keywords specified in [keyword], [plural keyword] lines.  Done in pairs, like for regular keywords.
764     my @separateIgnoredKeywords;
765
766     my $inIgnoredKeywords;
767
768     if (NaturalDocs::ConfigFile->Open($file))
769         {
770         # We can assume the file is valid.
771
772         my ($keyword, $value, $topicTypeName);
773
774         while (($keyword, $value) = NaturalDocs::ConfigFile->GetLine())
775             {
776             $keyword = lc($keyword);
777
778             if ($keyword eq 'topic type' || $keyword eq 'alter topic type')
779                 {
780                 $topicTypeName = $types{ $names{lc($value)} }->Name();
781
782                 if ($keyword eq 'alter topic type')
783                     {  $topicTypeName .= '*';  };
784
785                 push @topicTypeOrder, $topicTypeName;
786
787                 if (!exists $properties{$topicTypeName})
788                     {  $properties{$topicTypeName} = { 'keywords' => [ ] };  };
789                 }
790
791             elsif ($keyword eq 'plural')
792                 {
793                 $properties{$topicTypeName}->{$keyword} = $value;
794                 }
795
796             elsif ($keyword eq 'index' ||
797                     $keyword eq 'scope' ||
798                     $keyword eq 'page title if first' ||
799                     $keyword eq 'class hierarchy' ||
800                     $keyword eq 'break lists' ||
801                     $keyword eq 'can group with')
802                 {
803                 $properties{$topicTypeName}->{$keyword} = lc($value);
804                 }
805
806             elsif ($keyword =~ /^(?:add )?keywords?$/)
807                 {
808                 $inIgnoredKeywords = 0;
809                 }
810
811             elsif ($keyword =~ /^ignored? keywords?$/)
812                 {
813                 $inIgnoredKeywords = 1;
814                 if ($value)
815                     {  push @inlineIgnoredKeywords, split(/ ?, ?/, $value);  };
816                 }
817
818             elsif (!$keyword)
819                 {
820                 my ($singular, $plural) = split(/ ?, ?/, lc($value));
821
822                 if ($inIgnoredKeywords)
823                     {  push @separateIgnoredKeywords, $singular, $plural;  }
824                 else
825                     {  push @{$properties{$topicTypeName}->{'keywords'}}, $singular, $plural;  };
826                 };
827             };
828
829         NaturalDocs::ConfigFile->Close();
830         };
831
832
833     if (!open(FH_TOPICS, '>' . $file))
834         {
835         # The main file may be on a shared volume or some other place the user doesn't have write access to.  Since this is only to
836         # reformat the file, we can ignore the failure.
837         if ($isMain)
838             {  return;  }
839         else
840             {  die "Couldn't save " . $file;  };
841         };
842
843     binmode(FH_TOPICS, ':encoding(UTF-8)');
844     print FH_TOPICS 'Format: ' . NaturalDocs::Settings->TextAppVersion() . "\n\n";
845
846     # Remember the 80 character limit.
847
848     if ($isMain)
849         {
850         print FH_TOPICS
851         "# This is the main Natural Docs topics file.  If you change anything here, it\n"
852         . "# will apply to EVERY PROJECT you use Natural Docs on.  If you'd like to\n"
853         . "# change something for just one project, edit the Topics.txt in its project\n"
854         . "# directory instead.\n";
855         }
856     else
857         {
858         print FH_TOPICS
859         "# This is the Natural Docs topics file for this project.  If you change anything\n"
860         . "# here, it will apply to THIS PROJECT ONLY.  If you'd like to change something\n"
861         . "# for all your projects, edit the Topics.txt in Natural Docs' Config directory\n"
862         . "# instead.\n\n\n";
863
864         if (scalar @inlineIgnoredKeywords || scalar @separateIgnoredKeywords)
865             {
866             if (scalar @inlineIgnoredKeywords == 1 && !scalar @separateIgnoredKeywords)
867                 {
868                 print FH_TOPICS 'Ignore Keyword: ' . $inlineIgnoredKeywords[0] . "\n";
869                 }
870             else
871                 {
872                 print FH_TOPICS
873                 'Ignore Keywords: ' . join(', ', @inlineIgnoredKeywords) . "\n";
874
875                 for (my $i = 0; $i < scalar @separateIgnoredKeywords; $i += 2)
876                     {
877                     print FH_TOPICS '   ' . $separateIgnoredKeywords[$i];
878
879                     if (defined $separateIgnoredKeywords[$i + 1])
880                         {  print FH_TOPICS ', ' . $separateIgnoredKeywords[$i + 1];  };
881
882                     print FH_TOPICS "\n";
883                     };
884                 };
885             }
886         else
887             {
888             print FH_TOPICS
889             "# If you'd like to prevent keywords from being recognized by Natural Docs, you\n"
890             . "# can do it like this:\n"
891             . "# Ignore Keywords: [keyword], [keyword], ...\n"
892             . "#\n"
893             . "# Or you can use the list syntax like how they are defined:\n"
894             . "# Ignore Keywords:\n"
895             . "#    [keyword]\n"
896             . "#    [keyword], [plural keyword]\n"
897             . "#    ...\n";
898             };
899         };
900
901     print FH_TOPICS # [CFChars]
902     "\n\n"
903     . "#-------------------------------------------------------------------------------\n"
904     . "# SYNTAX:\n"
905     . "#\n";
906
907     if ($isMain)
908         {
909         print FH_TOPICS
910         "# Topic Type: [name]\n"
911         . "#    Creates a new topic type.  Each type gets its own index and behavior\n"
912         . "#    settings.  Its name can have letters, numbers, spaces, and these\n"
913         . "#    charaters: - / . '\n"
914         . "#\n"
915         . "#    The Enumeration type is special.  It's indexed with Types but its members\n"
916         . "#    are indexed with Constants according to the rules in Languages.txt.\n"
917         . "#\n"
918         }
919     else
920         {
921         print FH_TOPICS
922         "# Topic Type: [name]\n"
923         . "# Alter Topic Type: [name]\n"
924         . "#    Creates a new topic type or alters one from the main file.  Each type gets\n"
925         . "#    its own index and behavior settings.  Its name can have letters, numbers,\n"
926         . "#    spaces, and these charaters: - / . '\n"
927         . "#\n";
928         };
929
930     print FH_TOPICS
931     "# Plural: [name]\n"
932     . "#    Sets the plural name of the topic type, if different.\n"
933     . "#\n"
934     . "# Keywords:\n"
935     . "#    [keyword]\n"
936     . "#    [keyword], [plural keyword]\n"
937     . "#    ...\n";
938
939     if ($isMain)
940         {
941         print FH_TOPICS
942         "#    Defines a list of keywords for the topic type.  They may only contain\n"
943         . "#    letters, numbers, and spaces and are not case sensitive.  Plural keywords\n"
944         . "#    are used for list topics.\n";
945         }
946     else
947         {
948         print FH_TOPICS
949         "#    Defines or adds to the list of keywords for the topic type.  They may only\n"
950         . "#    contain letters, numbers, and spaces and are not case sensitive.  Plural\n"
951         . "#    keywords are used for list topics.  You can redefine keywords found in the\n"
952         . "#    main topics file.\n";
953         }
954
955     print FH_TOPICS
956     "#\n"
957     . "# Index: [yes|no]\n"
958     . "#    Whether the topics get their own index.  Defaults to yes.  Everything is\n"
959     . "#    included in the general index regardless of this setting.\n"
960     . "#\n"
961     . "# Scope: [normal|start|end|always global]\n"
962     . "#    How the topics affects scope.  Defaults to normal.\n"
963     . "#    normal        - Topics stay within the current scope.\n"
964     . "#    start         - Topics start a new scope for all the topics beneath it,\n"
965     . "#                    like class topics.\n"
966     . "#    end           - Topics reset the scope back to global for all the topics\n"
967     . "#                    beneath it.\n"
968     . "#    always global - Topics are defined as global, but do not change the scope\n"
969     . "#                    for any other topics.\n"
970     . "#\n"
971     . "# Class Hierarchy: [yes|no]\n"
972     . "#    Whether the topics are part of the class hierarchy.  Defaults to no.\n"
973     . "#\n"
974     . "# Page Title If First: [yes|no]\n"
975     . "#    Whether the topic's title becomes the page title if it's the first one in\n"
976     . "#    a file.  Defaults to no.\n"
977     . "#\n"
978     . "# Break Lists: [yes|no]\n"
979     . "#    Whether list topics should be broken into individual topics in the output.\n"
980     . "#    Defaults to no.\n"
981     . "#\n"
982     . "# Can Group With: [type], [type], ...\n"
983     . "#    Defines a list of topic types that this one can possibly be grouped with.\n"
984     . "#    Defaults to none.\n"
985     . "#-------------------------------------------------------------------------------\n\n";
986
987     my $listToPrint;
988
989     if ($isMain)
990         {
991         print FH_TOPICS
992         "# The following topics MUST be defined in this file:\n"
993         . "#\n";
994         $listToPrint = \@requiredTypeNames;
995         }
996     else
997         {
998         print FH_TOPICS
999         "# The following topics are defined in the main file, if you'd like to alter\n"
1000         . "# their behavior or add keywords:\n"
1001         . "#\n";
1002         $listToPrint = \@mainTopicNames;
1003         }
1004
1005     print FH_TOPICS
1006     Text::Wrap::wrap('#    ', '#    ', join(', ', @$listToPrint)) . "\n"
1007     . "\n"
1008     . "# If you add something that you think would be useful to other developers\n"
1009     . "# and should be included in Natural Docs by default, please e-mail it to\n"
1010     . "# topics [at] naturaldocs [dot] org.\n";
1011
1012     # Existence hash.  We do this because we want the required ones to go first by adding them to @topicTypeOrder, but we don't
1013     # want them to appear twice.
1014     my %doneTopicTypes;
1015     my ($altering, $numberOfProperties);
1016
1017     if ($isMain)
1018         {  unshift @topicTypeOrder, @requiredTypeNames;  };
1019
1020     my @propertyOrder = ('Plural', 'Index', 'Scope', 'Class Hierarchy', 'Page Title If First', 'Break Lists');
1021
1022     foreach my $topicType (@topicTypeOrder)
1023         {
1024         if (!exists $doneTopicTypes{$topicType})
1025             {
1026             if (substr($topicType, -1) eq '*')
1027                 {
1028                 print FH_TOPICS "\n\n"
1029                 . 'Alter Topic Type: ' . substr($topicType, 0, -1) . "\n\n";
1030
1031                 $altering = 1;
1032                 $numberOfProperties = 0;
1033                 }
1034             else
1035                 {
1036                 print FH_TOPICS "\n\n"
1037                 . 'Topic Type: ' . $topicType . "\n\n";
1038
1039                 $altering = 0;
1040                 $numberOfProperties = 0;
1041                 };
1042
1043             foreach my $property (@propertyOrder)
1044                 {
1045                 if (exists $properties{$topicType}->{lc($property)})
1046                     {
1047                     print FH_TOPICS
1048                     '   ' . $property . ': ' . ucfirst( $properties{$topicType}->{lc($property)} ) . "\n";
1049
1050                     $numberOfProperties++;
1051                     };
1052                 };
1053
1054             if (exists $properties{$topicType}->{'can group with'})
1055                 {
1056                 my @typeStrings = split(/ ?, ?/, lc($properties{$topicType}->{'can group with'}));
1057                 my @types;
1058
1059                 foreach my $typeString (@typeStrings)
1060                     {
1061                     if (exists $names{$typeString})
1062                         {  push @types, $names{$typeString};  };
1063                     };
1064
1065                 if (scalar @types)
1066                     {
1067                     for (my $i = 0; $i < scalar @types; $i++)
1068                         {
1069                         my $name = NaturalDocs::Topics->NameOfType($types[$i], 1);
1070
1071                         if ($i == 0)
1072                             {  print FH_TOPICS '   Can Group With: ' . $name;  }
1073                         else
1074                             {  print FH_TOPICS ', ' . $name;  };
1075                         };
1076
1077                     print FH_TOPICS "\n";
1078                     $numberOfProperties++;
1079                     };
1080                 };
1081
1082             if (scalar @{$properties{$topicType}->{'keywords'}})
1083                 {
1084                 if ($numberOfProperties > 1)
1085                     {  print FH_TOPICS "\n";  };
1086
1087                 print FH_TOPICS
1088                 '   ' . ($altering ? 'Add ' : '') . 'Keywords:' . "\n";
1089
1090                 my $keywords = $properties{$topicType}->{'keywords'};
1091
1092                 for (my $i = 0; $i < scalar @$keywords; $i += 2)
1093                     {
1094                     print FH_TOPICS '      ' . $keywords->[$i];
1095
1096                     if (defined $keywords->[$i + 1])
1097                         {  print FH_TOPICS ', ' . $keywords->[$i + 1];  };
1098
1099                     print FH_TOPICS "\n";
1100                     };
1101                 };
1102
1103             $doneTopicTypes{$topicType} = 1;
1104             };
1105         };
1106
1107     close(FH_TOPICS);
1108     };
1109
1110
1111
1112 ###############################################################################
1113 # Group: Functions
1114
1115
1116 #
1117 #   Function: KeywordInfo
1118 #
1119 #   Returns information about a topic keyword.
1120 #
1121 #   Parameters:
1122 #
1123 #       keyword - The keyword, which may be plural.
1124 #
1125 #   Returns:
1126 #
1127 #       The array ( topicType, info, isPlural ), or an empty array if the keyword doesn't exist.
1128 #
1129 #       topicType - The <TopicType> of the keyword.
1130 #       info - The <NaturalDocs::Topics::Type> of its type.
1131 #       isPlural - Whether the keyword was plural or not.
1132 #
1133 sub KeywordInfo #(keyword)
1134     {
1135     my ($self, $keyword) = @_;
1136
1137     $keyword = lc($keyword);
1138
1139     my $type = $keywords{$keyword};
1140
1141     if (defined $type)
1142         {  return ( $type, $types{$type}, undef );  };
1143
1144     $type = $pluralKeywords{$keyword};
1145
1146     if (defined $type)
1147         {  return ( $type, $types{$type}, 1 );  };
1148
1149     return ( );
1150     };
1151
1152
1153 #
1154 #   Function: NameInfo
1155 #
1156 #   Returns information about a topic name.
1157 #
1158 #   Parameters:
1159 #
1160 #      name - The topic type name, which can be plural and/or alphanumeric only.
1161 #
1162 #   Returns:
1163 #
1164 #       The array ( topicType, info ), or an empty array if the name doesn't exist.  Note that unlike <KeywordInfo()>, this
1165 #       does *not* tell you whether the name is plural or not.
1166 #
1167 #       topicType - The <TopicType> of the name.
1168 #       info - The <NaturalDocs::Topics::Type> of the type.
1169 #
1170 sub NameInfo #(name)
1171     {
1172     my ($self, $name) = @_;
1173
1174     my $type = $names{lc($name)};
1175
1176     if (defined $type)
1177         {  return ( $type, $types{$type} );  }
1178     else
1179         {  return ( );  };
1180     };
1181
1182
1183 #
1184 #   Function: TypeInfo
1185 #
1186 #   Returns information about a <TopicType>.
1187 #
1188 #   Parameters:
1189 #
1190 #      type - The <TopicType>.
1191 #
1192 #   Returns:
1193 #
1194 #       The <NaturalDocs::Topics::Type> of the type, or undef if it didn't exist.
1195 #
1196 sub TypeInfo #(type)
1197     {
1198     my ($self, $type) = @_;
1199     return $types{$type};
1200     };
1201
1202
1203 #
1204 #   Function: NameOfType
1205 #
1206 #   Returns the name of the passed <TopicType>, or undef if it doesn't exist.
1207 #
1208 #   Parameters:
1209 #
1210 #       topicType - The <TopicType>.
1211 #       plural - Whether to return the plural instead of the singular.
1212 #       alphanumericOnly - Whether to strips everything but alphanumeric characters out.  Case isn't modified.
1213 #
1214 #   Returns:
1215 #
1216 #       The topic type name, according to what was specified in the parameters, or undef if it doesn't exist.
1217 #
1218 sub NameOfType #(topicType, plural, alphanumericOnly)
1219     {
1220     my ($self, $topicType, $plural, $alphanumericOnly) = @_;
1221
1222     my $topicObject = $types{$topicType};
1223
1224     if (!defined $topicObject)
1225         {  return undef;  };
1226
1227     my $topicName = ($plural ? $topicObject->PluralName() : $topicObject->Name());
1228
1229     if ($alphanumericOnly)
1230         {  $topicName =~ tr/a-zA-Z0-9//cd;  };
1231
1232     return $topicName;
1233     };
1234
1235
1236 #
1237 #   Function: TypeFromName
1238 #
1239 #   Returns a <TopicType> for the passed topic name.
1240 #
1241 #   Parameters:
1242 #
1243 #       topicName - The name of the topic, which can be plural and/or alphanumeric only.
1244 #
1245 #   Returns:
1246 #
1247 #       The <TopicType>.  It does not specify whether the name was plural or not.
1248 #
1249 sub TypeFromName #(topicName)
1250     {
1251     my ($self, $topicName) = @_;
1252
1253     return $names{lc($topicName)};
1254     };
1255
1256
1257 #
1258 #   Function: IsValidType
1259 #
1260 #   Returns whether the passed <TopicType> is defined.
1261 #
1262 sub IsValidType #(type)
1263     {
1264     my ($self, $type) = @_;
1265     return exists $types{$type};
1266     };
1267
1268
1269 #
1270 #   Function: TypeFromLegacy
1271 #
1272 #   Returns a <TopicType> for the passed legacy topic type integer.  <TopicTypes> were changed from integer constants to
1273 #   strings in 1.3.
1274 #
1275 sub TypeFromLegacy #(legacyInt)
1276     {
1277     my ($self, $int) = @_;
1278     return $legacyTypes[$int];
1279     };
1280
1281
1282 #
1283 #   Function: AllIndexableTypes
1284 #
1285 #   Returns an array of all possible indexable <TopicTypes>.
1286 #
1287 sub AllIndexableTypes
1288     {
1289     my ($self) = @_;
1290     return keys %indexable;
1291     };
1292
1293
1294
1295 ###############################################################################
1296 # Group: Support Functions
1297
1298
1299 #
1300 #   Function: MakeTopicType
1301 #
1302 #   Returns a <TopicType> for the passed topic name.  It does not check to see if it exists already.
1303 #
1304 #   Parameters:
1305 #
1306 sub MakeTopicType #(topicName)
1307     {
1308     my ($self, $topicName) = @_;
1309
1310     # Dependency: The values of the default topic type constants must match what is generated here.
1311
1312     # Turn everything to lowercase and strip non-alphanumeric characters.
1313     $topicName = lc($topicName);
1314     $topicName =~ tr/a-z0-9//cd;
1315
1316     return $topicName;
1317     };
1318
1319
1320
1321 1;