OSDN Git Service

Version 5.91
[vbslib/main.git] / GPL_bin_fullset / NaturalDocs / Modules / NaturalDocs / Languages / ActionScript.pm
1 ###############################################################################
2 #
3 #   Class: NaturalDocs::Languages::ActionScript
4 #
5 ###############################################################################
6 #
7 #   A subclass to handle the language variations of Flash ActionScript.
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
15 use strict;
16 use integer;
17
18 package NaturalDocs::Languages::ActionScript;
19
20 use base 'NaturalDocs::Languages::Advanced';
21
22
23 ################################################################################
24 # Group: Constants and Types
25
26
27 #
28 #   Constants: XML Tag Type
29 #
30 #   XML_OPENING_TAG - The tag is an opening one, such as <tag>.
31 #   XML_CLOSING_TAG - The tag is a closing one, such as </tag>.
32 #   XML_SELF_CONTAINED_TAG - The tag is self contained, such as <tag />.
33 #
34 use constant XML_OPENING_TAG => 1;
35 use constant XML_CLOSING_TAG => 2;
36 use constant XML_SELF_CONTAINED_TAG => 3;
37
38
39 ################################################################################
40 # Group: Package Variables
41
42 #
43 #   hash: classModifiers
44 #   An existence hash of all the acceptable class modifiers.  The keys are in all lowercase.
45 #
46 my %classModifiers = ( 'dynamic' => 1,
47                                    'intrinsic' => 1,
48                                    'final' => 1,
49                                    'internal' => 1,
50                                    'public' => 1 );
51
52 #
53 #   hash: memberModifiers
54 #   An existence hash of all the acceptable class member modifiers.  The keys are in all lowercase.
55 #
56 my %memberModifiers = ( 'public' => 1,
57                                         'private' => 1,
58                                         'protected' => 1,
59                                         'static' => 1,
60                                         'internal' => 1,
61                                         'override' => 1 );
62
63
64 #
65 #   hash: declarationEnders
66 #   An existence hash of all the tokens that can end a declaration.  This is important because statements don't require a semicolon
67 #   to end.  The keys are in all lowercase.
68 #
69 my %declarationEnders = ( ';' => 1,
70                                         '}' => 1,
71                                         '{' => 1,
72                                         'public' => 1,
73                                         'private' => 1,
74                                         'protected' => 1,
75                                         'static' => 1,
76                                         'internal' => 1,
77                                         'dynamic' => 1,
78                                         'intrinsic' => 1,
79                                         'final' => 1,
80                                         'override' => 1,
81                                         'class' => 1,
82                                         'interface' => 1,
83                                         'var' => 1,
84                                         'function' => 1,
85                                         'const' => 1,
86                                         'namespace' => 1,
87                                         'import' => 1 );
88
89
90 #
91 #   var: isEscaped
92 #   Whether the current file being parsed uses escapement.
93 #
94 my $isEscaped;
95
96
97
98 ################################################################################
99 # Group: Interface Functions
100
101
102 #
103 #   Function: PackageSeparator
104 #   Returns the package separator symbol.
105 #
106 sub PackageSeparator
107     {  return '.';  };
108
109
110 #
111 #   Function: EnumValues
112 #   Returns the <EnumValuesType> that describes how the language handles enums.
113 #
114 sub EnumValues
115     {  return ::ENUM_GLOBAL();  };
116
117
118 #
119 #   Function: ParseParameterLine
120 #   Parses a prototype parameter line and returns it as a <NaturalDocs::Languages::Prototype::Parameter> object.
121 #
122 sub ParseParameterLine #(line)
123     {
124     my ($self, $line) = @_;
125
126     if ($line =~ /^ ?\.\.\.\ (.+)$/)
127         {
128         # This puts them in the wrong fields as $1 should be the name and ... should be the type.  However, this is necessary
129         # because the order in the source is reversed from other parameter declarations and it's more important for the output
130         # to match the source.
131         return NaturalDocs::Languages::Prototype::Parameter->New($1, undef, '...', undef, undef, undef);
132         }
133     else
134         {  return $self->ParsePascalParameterLine($line);  };
135     };
136
137
138 #
139 #   Function: TypeBeforeParameter
140 #   Returns whether the type appears before the parameter in prototypes.
141 #
142 sub TypeBeforeParameter
143     {  return 0;  };
144
145
146 #
147 #   Function: PreprocessFile
148 #
149 #   If the file is escaped, strips out all unescaped code.  Will translate any unescaped comments into comments surrounded by
150 #   "\x1C\x1D\x1E\x1F" and "\x1F\x1E\x1D" characters, so chosen because they are the same character lengths as <!-- and -->
151 #   and will not appear in normal code.
152 #
153 sub PreprocessFile
154     {
155     my ($self, $lines) = @_;
156
157     if (!$isEscaped)
158         {  return;  };
159
160     use constant MODE_UNESCAPED_REGULAR => 1;
161     use constant MODE_UNESCAPED_PI => 2;
162     use constant MODE_UNESCAPED_CDATA => 3;
163     use constant MODE_UNESCAPED_COMMENT => 4;
164     use constant MODE_ESCAPED_UNKNOWN_CDATA => 5;
165     use constant MODE_ESCAPED_CDATA => 6;
166     use constant MODE_ESCAPED_NO_CDATA => 7;
167
168     my $mode = MODE_UNESCAPED_REGULAR;
169
170     for (my $i = 0; $i < scalar @$lines; $i++)
171         {
172         my @tokens = split(/(<[ \t]*\/?[ \t]*mx:Script[^>]*>|<\?|\?>|<\!--|-->|<\!\[CDATA\[|\]\]\>)/, $lines->[$i]);
173         my $newLine;
174
175         foreach my $token (@tokens)
176             {
177             if ($mode == MODE_UNESCAPED_REGULAR)
178                 {
179                 if ($token eq '<?')
180                     {  $mode = MODE_UNESCAPED_PI;  }
181                 elsif ($token eq '<![CDATA[')
182                     {  $mode = MODE_UNESCAPED_CDATA;  }
183                 elsif ($token eq '<!--')
184                     {
185                     $mode = MODE_UNESCAPED_COMMENT;
186                     $newLine .= "\x1C\x1D\x1E\x1F";
187                     }
188                 elsif ($token =~ /^<[ \t]*mx:Script/)
189                     {  $mode = MODE_ESCAPED_UNKNOWN_CDATA;  };
190                 }
191
192             elsif ($mode == MODE_UNESCAPED_PI)
193                 {
194                 if ($token eq '?>')
195                     {  $mode = MODE_UNESCAPED_REGULAR;  };
196                 }
197
198             elsif ($mode == MODE_UNESCAPED_CDATA)
199                 {
200                 if ($token eq ']]>')
201                     {  $mode = MODE_UNESCAPED_REGULAR;  };
202                 }
203
204             elsif ($mode == MODE_UNESCAPED_COMMENT)
205                 {
206                 if ($token eq '-->')
207                     {
208                     $mode = MODE_UNESCAPED_REGULAR;
209                     $newLine .= "\x1F\x1E\x1D";
210                     }
211                 else
212                     {  $newLine .= $token;  };
213                 }
214
215             elsif ($mode == MODE_ESCAPED_UNKNOWN_CDATA)
216                 {
217                 if ($token eq '<![CDATA[')
218                     {  $mode = MODE_ESCAPED_CDATA;  }
219                 elsif ($token =~ /^<[ \t]*\/[ \t]*mx:Script/)
220                     {
221                     $mode = MODE_UNESCAPED_REGULAR;
222                     $newLine .= '; ';
223                     }
224                 elsif ($token !~ /^[ \t]*$/)
225                     {
226                     $mode = MODE_ESCAPED_NO_CDATA;
227                     $newLine .= $token;
228                     };
229                 }
230
231             elsif ($mode == MODE_ESCAPED_CDATA)
232                 {
233                 if ($token eq ']]>')
234                     {
235                     $mode = MODE_UNESCAPED_REGULAR;
236                     $newLine .= '; ';
237                     }
238                 else
239                     {  $newLine .= $token;  };
240                 }
241
242             else #($mode == MODE_ESCAPED_NO_CDATA)
243                 {
244                 if ($token =~ /^<[ \t]*\/[ \t]*mx:Script/)
245                     {
246                     $mode = MODE_UNESCAPED_REGULAR;
247                     $newLine .= '; ';
248                     }
249                 else
250                     {  $newLine .= $token;  };
251                 };
252
253             };
254
255         $lines->[$i] = $newLine;
256         };
257     };
258
259
260 #
261 #   Function: ParseFile
262 #
263 #   Parses the passed source file, sending comments acceptable for documentation to <NaturalDocs::Parser->OnComment()>.
264 #
265 #   Parameters:
266 #
267 #       sourceFile - The <FileName> to parse.
268 #       topicList - A reference to the list of <NaturalDocs::Parser::ParsedTopics> being built by the file.
269 #
270 #   Returns:
271 #
272 #       The array ( autoTopics, scopeRecord ).
273 #
274 #       autoTopics - An arrayref of automatically generated topics from the file, or undef if none.
275 #       scopeRecord - An arrayref of <NaturalDocs::Languages::Advanced::ScopeChanges>, or undef if none.
276 #
277 sub ParseFile #(sourceFile, topicsList)
278     {
279     my ($self, $sourceFile, $topicsList) = @_;
280
281     # The \x1# comment symbols are inserted by PreprocessFile() to stand in for XML comments in escaped files.
282     my @parseParameters = ( [ '//' ], [ '/*', '*/', "\x1C\x1D\x1E\x1F", "\x1F\x1E\x1D" ], [ '///' ], [ '/**', '*/' ] );
283
284     my $extension = lc(NaturalDocs::File->ExtensionOf($sourceFile));
285     $isEscaped = ($extension eq 'mxml');
286
287     $self->ParseForCommentsAndTokens($sourceFile, @parseParameters);
288
289     my $tokens = $self->Tokens();
290     my $index = 0;
291     my $lineNumber = 1;
292
293     while ($index < scalar @$tokens)
294         {
295         if ($self->TryToSkipWhitespace(\$index, \$lineNumber) ||
296             $self->TryToGetImport(\$index, \$lineNumber) ||
297             $self->TryToGetClass(\$index, \$lineNumber) ||
298             $self->TryToGetFunction(\$index, \$lineNumber) ||
299             $self->TryToGetVariable(\$index, \$lineNumber) )
300             {
301             # The functions above will handle everything.
302             }
303
304         elsif ($tokens->[$index] eq '{')
305             {
306             $self->StartScope('}', $lineNumber, undef, undef, undef);
307             $index++;
308             }
309
310         elsif ($tokens->[$index] eq '}')
311             {
312             if ($self->ClosingScopeSymbol() eq '}')
313                 {  $self->EndScope($lineNumber);  };
314
315             $index++;
316             }
317
318         else
319             {
320             $self->SkipToNextStatement(\$index, \$lineNumber);
321             };
322         };
323
324
325     # Don't need to keep these around.
326     $self->ClearTokens();
327
328
329     my $autoTopics = $self->AutoTopics();
330
331     my $scopeRecord = $self->ScopeRecord();
332     if (defined $scopeRecord && !scalar @$scopeRecord)
333         {  $scopeRecord = undef;  };
334
335     return ( $autoTopics, $scopeRecord );
336     };
337
338
339
340 ################################################################################
341 # Group: Statement Parsing Functions
342 # All functions here assume that the current position is at the beginning of a statement.
343 #
344 # Note for developers: I am well aware that the code in these functions do not check if we're past the end of the tokens as
345 # often as it should.  We're making use of the fact that Perl will always return undef in these cases to keep the code simpler.
346
347
348 #
349 #   Function: TryToGetIdentifier
350 #
351 #   Determines whether the position is at an identifier, and if so, skips it and returns the complete identifier as a string.  Returns
352 #   undef otherwise.
353 #
354 #   Parameters:
355 #
356 #       indexRef - A reference to the current token index.
357 #       lineNumberRef - A reference to the current line number.
358 #       allowStar - If set, allows the last identifier to be a star.
359 #
360 sub TryToGetIdentifier #(indexRef, lineNumberRef, allowStar)
361     {
362     my ($self, $indexRef, $lineNumberRef, $allowStar) = @_;
363     my $tokens = $self->Tokens();
364
365     my $index = $$indexRef;
366
367     use constant MODE_IDENTIFIER_START => 1;
368     use constant MODE_IN_IDENTIFIER => 2;
369     use constant MODE_AFTER_STAR => 3;
370
371     my $identifier;
372     my $mode = MODE_IDENTIFIER_START;
373
374     while ($index < scalar @$tokens)
375         {
376         if ($mode == MODE_IDENTIFIER_START)
377             {
378             if ($tokens->[$index] =~ /^[a-z\$\_]/i)
379                 {
380                 $identifier .= $tokens->[$index];
381                 $index++;
382
383                 $mode = MODE_IN_IDENTIFIER;
384                 }
385             elsif ($allowStar && $tokens->[$index] eq '*')
386                 {
387                 $identifier .= '*';
388                 $index++;
389
390                 $mode = MODE_AFTER_STAR;
391                 }
392             else
393                 {  return undef;  };
394             }
395
396         elsif ($mode == MODE_IN_IDENTIFIER)
397             {
398             if ($tokens->[$index] eq '.')
399                 {
400                 $identifier .= '.';
401                 $index++;
402
403                 $mode = MODE_IDENTIFIER_START;
404                 }
405             elsif ($tokens->[$index] =~ /^[a-z0-9\$\_]/i)
406                 {
407                 $identifier .= $tokens->[$index];
408                 $index++;
409                 }
410             else
411                 {  last;  };
412             }
413
414         else #($mode == MODE_AFTER_STAR)
415             {
416             if ($tokens->[$index] =~ /^[a-z0-9\$\_\.]/i)
417                 {  return undef;  }
418             else
419                 {  last;  };
420             };
421         };
422
423     # We need to check again because we may have run out of tokens after a dot.
424     if ($mode != MODE_IDENTIFIER_START)
425         {
426         $$indexRef = $index;
427         return $identifier;
428         }
429     else
430         {  return undef;  };
431     };
432
433
434 #
435 #   Function: TryToGetImport
436 #
437 #   Determines whether the position is at a import statement, and if so, adds it as a Using statement to the current scope, skips
438 #   it, and returns true.
439 #
440 sub TryToGetImport #(indexRef, lineNumberRef)
441     {
442     my ($self, $indexRef, $lineNumberRef) = @_;
443     my $tokens = $self->Tokens();
444
445     my $index = $$indexRef;
446     my $lineNumber = $$lineNumberRef;
447
448     if ($tokens->[$index] ne 'import')
449         {  return undef;  };
450
451     $index++;
452     $self->TryToSkipWhitespace(\$index, \$lineNumber);
453
454     my $identifier = $self->TryToGetIdentifier(\$index, \$lineNumber, 1);
455     if (!$identifier)
456         {  return undef;  };
457
458
459     # Currently we implement importing by stripping the last package level and treating it as a using.  So "import p1.p2.p3" makes
460     # p1.p2 the using path, which is over-tolerant but that's okay.  "import p1.p2.*" is treated the same way, but in this case it's
461     # not over-tolerant.  If there's no dot, there's no point to including it.
462
463     if (index($identifier, '.') != -1)
464         {
465         $identifier =~ s/\.[^\.]+$//;
466         $self->AddUsing( NaturalDocs::SymbolString->FromText($identifier) );
467         };
468
469     $$indexRef = $index;
470     $$lineNumberRef = $lineNumber;
471
472     return 1;
473     };
474
475
476 #
477 #   Function: TryToGetClass
478 #
479 #   Determines whether the position is at a class declaration statement, and if so, generates a topic for it, skips it, and
480 #   returns true.
481 #
482 #   Supported Syntaxes:
483 #
484 #       - Classes
485 #       - Interfaces
486 #       - Classes and interfaces with _global
487 #
488 sub TryToGetClass #(indexRef, lineNumberRef)
489     {
490     my ($self, $indexRef, $lineNumberRef) = @_;
491     my $tokens = $self->Tokens();
492
493     my $index = $$indexRef;
494     my $lineNumber = $$lineNumberRef;
495
496     my @modifiers;
497
498     while ($tokens->[$index] =~ /^[a-z]/i &&
499               exists $classModifiers{lc($tokens->[$index])} )
500         {
501         push @modifiers, lc($tokens->[$index]);
502         $index++;
503
504         $self->TryToSkipWhitespace(\$index, \$lineNumber);
505         };
506
507     my $type;
508
509     if ($tokens->[$index] eq 'class' || $tokens->[$index] eq 'interface')
510         {
511         $type = $tokens->[$index];
512
513         $index++;
514         $self->TryToSkipWhitespace(\$index, \$lineNumber);
515         }
516     else
517         {  return undef;  };
518
519     my $className = $self->TryToGetIdentifier(\$index, \$lineNumber);
520
521     if (!$className)
522         {  return undef;  };
523
524     $self->TryToSkipWhitespace(\$index, \$lineNumber);
525
526     my @parents;
527
528     if ($tokens->[$index] eq 'extends')
529         {
530         $index++;
531         $self->TryToSkipWhitespace(\$index, \$lineNumber);
532
533         # Interfaces can extend multiple other interfaces, which is NOT clearly mentioned in the docs.
534
535         for (;;)
536                 {
537                 my $parent = $self->TryToGetIdentifier(\$index, \$lineNumber);
538                 if (!$parent)
539                     {  return undef;  };
540
541                 push @parents, $parent;
542
543                 $self->TryToSkipWhitespace(\$index, \$lineNumber);
544
545             if ($tokens->[$index] ne ',')
546                 {  last;  }
547             else
548                 {
549                 $index++;
550                 $self->TryToSkipWhitespace(\$index, \$lineNumber);
551                 };
552                 }
553         };
554
555     if ($type eq 'class' && $tokens->[$index] eq 'implements')
556         {
557         $index++;
558         $self->TryToSkipWhitespace(\$index, \$lineNumber);
559
560         for (;;)
561             {
562             my $parent = $self->TryToGetIdentifier(\$index, \$lineNumber);
563             if (!$parent)
564                 {  return undef;  };
565
566             push @parents, $parent;
567
568             $self->TryToSkipWhitespace(\$index, \$lineNumber);
569
570             if ($tokens->[$index] ne ',')
571                 {  last;  }
572             else
573                 {
574                 $index++;
575                 $self->TryToSkipWhitespace(\$index, \$lineNumber);
576                 };
577             };
578         };
579
580     if ($tokens->[$index] ne '{')
581         {  return undef;  };
582
583     $index++;
584
585
586     # If we made it this far, we have a valid class declaration.
587
588     my $topicType;
589
590     if ($type eq 'interface')
591         {  $topicType = ::TOPIC_INTERFACE();  }
592     else
593         {  $topicType = ::TOPIC_CLASS();  };
594
595     $className =~ s/^_global.//;
596
597     my $autoTopic = NaturalDocs::Parser::ParsedTopic->New($topicType, $className,
598                                                                                          undef, $self->CurrentUsing(),
599                                                                                          undef,
600                                                                                          undef, undef, $$lineNumberRef);
601
602     $self->AddAutoTopic($autoTopic);
603     NaturalDocs::Parser->OnClass($autoTopic->Package());
604
605     foreach my $parent (@parents)
606         {
607         NaturalDocs::Parser->OnClassParent($autoTopic->Package(), NaturalDocs::SymbolString->FromText($parent),
608                                                                undef, $self->CurrentUsing(), ::RESOLVE_ABSOLUTE());
609         };
610
611     $self->StartScope('}', $lineNumber, $autoTopic->Package());
612
613     $$indexRef = $index;
614     $$lineNumberRef = $lineNumber;
615
616     return 1;
617     };
618
619
620 #
621 #   Function: TryToGetFunction
622 #
623 #   Determines if the position is on a function declaration, and if so, generates a topic for it, skips it, and returns true.
624 #
625 #   Supported Syntaxes:
626 #
627 #       - Functions
628 #       - Constructors
629 #       - Properties
630 #       - Functions with _global
631 #       - Functions with namespaces
632 #
633 sub TryToGetFunction #(indexRef, lineNumberRef)
634     {
635     my ($self, $indexRef, $lineNumberRef) = @_;
636     my $tokens = $self->Tokens();
637
638     my $index = $$indexRef;
639     my $lineNumber = $$lineNumberRef;
640
641     my $startIndex = $index;
642     my $startLine = $lineNumber;
643
644     my @modifiers;
645     my $namespace;
646
647     while ($tokens->[$index] =~ /^[a-z]/i)
648         {
649         if ($tokens->[$index] eq 'function')
650             {  last;  }
651
652         elsif (exists $memberModifiers{lc($tokens->[$index])})
653             {
654             push @modifiers, lc($tokens->[$index]);
655             $index++;
656
657             $self->TryToSkipWhitespace(\$index, \$lineNumber);
658             }
659
660         elsif (!$namespace)
661             {
662             do
663                 {
664                 $namespace .= $tokens->[$index];
665                 $index++;
666                 }
667             while ($tokens->[$index] =~ /^[a-z0-9_]/i);
668
669             $self->TryToSkipWhitespace(\$index, \$lineNumber);
670             }
671
672         else
673             {  last;  };
674         };
675
676     if ($tokens->[$index] ne 'function')
677         {  return undef;  };
678     $index++;
679
680     $self->TryToSkipWhitespace(\$index, \$lineNumber);
681
682     my $type;
683
684     if ($tokens->[$index] eq 'get' || $tokens->[$index] eq 'set')
685         {
686         # This can either be a property ("function get Something()") or a function name ("function get()").
687
688         my $nextIndex = $index;
689         my $nextLineNumber = $lineNumber;
690
691         $nextIndex++;
692         $self->TryToSkipWhitespace(\$nextIndex, \$nextLineNumber);
693
694         if ($tokens->[$nextIndex] eq '(')
695             {
696             $type = ::TOPIC_FUNCTION();
697             # Ignore the movement and let the code ahead pick it up as the name.
698             }
699         else
700             {
701             $type = ::TOPIC_PROPERTY();
702             $index = $nextIndex;
703             $lineNumber = $nextLineNumber;
704             };
705         }
706     else
707         {  $type = ::TOPIC_FUNCTION();  };
708
709     my $name = $self->TryToGetIdentifier(\$index, \$lineNumber);
710     if (!$name)
711         {  return undef;  };
712
713     $self->TryToSkipWhitespace(\$index, \$lineNumber);
714
715     if ($tokens->[$index] ne '(')
716         {  return undef;  };
717
718     $index++;
719     $self->GenericSkipUntilAfter(\$index, \$lineNumber, ')');
720
721     $self->TryToSkipWhitespace(\$index, \$lineNumber);
722
723     if ($tokens->[$index] eq ':')
724         {
725         $index++;
726
727         $self->TryToSkipWhitespace(\$index, \$lineNumber);
728
729         $self->TryToGetIdentifier(\$index, \$lineNumber, 1);
730
731         $self->TryToSkipWhitespace(\$index, \$lineNumber);
732         };
733
734
735     my $prototype = $self->NormalizePrototype( $self->CreateString($startIndex, $index) );
736
737     if ($tokens->[$index] eq '{')
738         {  $self->GenericSkip(\$index, \$lineNumber);  }
739     elsif (!exists $declarationEnders{$tokens->[$index]})
740         {  return undef;  };
741
742
743     my $scope = $self->CurrentScope();
744
745     if ($name =~ s/^_global.//)
746         {  $scope = undef;  };
747     if ($namespace)
748         {  $scope = NaturalDocs::SymbolString->Join($scope, $namespace);  };
749
750     $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New($type, $name,
751                                                                                               $scope, $self->CurrentUsing(),
752                                                                                               $prototype,
753                                                                                               undef, undef, $startLine));
754
755
756     # We succeeded if we got this far.
757
758     $$indexRef = $index;
759     $$lineNumberRef = $lineNumber;
760
761     return 1;
762     };
763
764
765 #
766 #   Function: TryToGetVariable
767 #
768 #   Determines if the position is on a variable declaration statement, and if so, generates a topic for each variable, skips the
769 #   statement, and returns true.
770 #
771 #   Supported Syntaxes:
772 #
773 #       - Variables
774 #       - Variables with _global
775 #       - Variables with type * (untyped)
776 #       - Constants
777 #       - Variables and constants with namespaces
778 #
779 sub TryToGetVariable #(indexRef, lineNumberRef)
780     {
781     my ($self, $indexRef, $lineNumberRef) = @_;
782     my $tokens = $self->Tokens();
783
784     my $index = $$indexRef;
785     my $lineNumber = $$lineNumberRef;
786
787     my $startIndex = $index;
788     my $startLine = $lineNumber;
789
790     my @modifiers;
791     my $namespace;
792
793     while ($tokens->[$index] =~ /^[a-z]/i)
794         {
795         if ($tokens->[$index] eq 'var' || $tokens->[$index] eq 'const')
796             {  last;  }
797
798         elsif (exists $memberModifiers{lc($tokens->[$index])})
799             {
800             push @modifiers, lc($tokens->[$index]);
801             $index++;
802
803             $self->TryToSkipWhitespace(\$index, \$lineNumber);
804             }
805
806         elsif (!$namespace)
807             {
808             do
809                 {
810                 $namespace .= $tokens->[$index];
811                 $index++;
812                 }
813             while ($tokens->[$index] =~ /^[a-z0-9_]/i);
814
815             $self->TryToSkipWhitespace(\$index, \$lineNumber);
816             }
817
818         else
819             {  last;  };
820         };
821
822     my $type;
823
824     if ($tokens->[$index] eq 'var')
825         {  $type = ::TOPIC_VARIABLE();  }
826     elsif ($tokens->[$index] eq 'const')
827         {  $type = ::TOPIC_CONSTANT();  }
828     else
829         {  return undef;  };
830     $index++;
831
832     $self->TryToSkipWhitespace(\$index, \$lineNumber);
833
834     my $endTypeIndex = $index;
835     my @names;
836     my @types;
837
838     for (;;)
839         {
840         my $name = $self->TryToGetIdentifier(\$index, \$lineNumber);
841         if (!$name)
842             {  return undef;  };
843
844         $self->TryToSkipWhitespace(\$index, \$lineNumber);
845
846         my $type;
847
848         if ($tokens->[$index] eq ':')
849             {
850             $index++;
851             $self->TryToSkipWhitespace(\$index, \$lineNumber);
852
853             $type = ': ' . $self->TryToGetIdentifier(\$index, \$lineNumber, 1);
854
855             $self->TryToSkipWhitespace(\$index, \$lineNumber);
856             };
857
858         if ($tokens->[$index] eq '=')
859             {
860             do
861                 {
862                 $self->GenericSkip(\$index, \$lineNumber);
863                 }
864             while ($tokens->[$index] ne ',' && !exists $declarationEnders{$tokens->[$index]} && $index < scalar @$tokens);
865             };
866
867         push @names, $name;
868         push @types, $type;
869
870         if ($tokens->[$index] eq ',')
871             {
872             $index++;
873             $self->TryToSkipWhitespace(\$index, \$lineNumber);
874             }
875         elsif (exists $declarationEnders{$tokens->[$index]})
876             {  last;  }
877         else
878             {  return undef;  };
879         };
880
881
882     # We succeeded if we got this far.
883
884     my $prototypePrefix = $self->CreateString($startIndex, $endTypeIndex);
885
886     for (my $i = 0; $i < scalar @names; $i++)
887         {
888         my $prototype = $self->NormalizePrototype( $prototypePrefix . ' ' . $names[$i] . $types[$i]);
889         my $scope = $self->CurrentScope();
890
891         if ($names[$i] =~ s/^_global.//)
892             {  $scope = undef;  };
893         if ($namespace)
894             {  $scope = NaturalDocs::SymbolString->Join($scope, $namespace);  };
895
896         $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New($type, $names[$i],
897                                                                                                   $scope, $self->CurrentUsing(),
898                                                                                                   $prototype,
899                                                                                                   undef, undef, $startLine));
900         };
901
902     $$indexRef = $index;
903     $$lineNumberRef = $lineNumber;
904
905     return 1;
906     };
907
908
909
910 ################################################################################
911 # Group: Low Level Parsing Functions
912
913
914 #
915 #   Function: GenericSkip
916 #
917 #   Advances the position one place through general code.
918 #
919 #   - If the position is on a string, it will skip it completely.
920 #   - If the position is on an opening symbol, it will skip until the past the closing symbol.
921 #   - If the position is on whitespace (including comments), it will skip it completely.
922 #   - Otherwise it skips one token.
923 #
924 #   Parameters:
925 #
926 #       indexRef - A reference to the current index.
927 #       lineNumberRef - A reference to the current line number.
928 #
929 sub GenericSkip #(indexRef, lineNumberRef)
930     {
931     my ($self, $indexRef, $lineNumberRef) = @_;
932     my $tokens = $self->Tokens();
933
934     # We can ignore the scope stack because we're just skipping everything without parsing, and we need recursion anyway.
935     if ($tokens->[$$indexRef] eq '{')
936         {
937         $$indexRef++;
938         $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}');
939         }
940     elsif ($tokens->[$$indexRef] eq '(')
941         {
942         $$indexRef++;
943         $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ')');
944         }
945     elsif ($tokens->[$$indexRef] eq '[')
946         {
947         $$indexRef++;
948         $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ']');
949         }
950
951     elsif ($self->TryToSkipWhitespace($indexRef, $lineNumberRef) ||
952             $self->TryToSkipString($indexRef, $lineNumberRef) ||
953             $self->TryToSkipRegExp($indexRef, $lineNumberRef) ||
954             $self->TryToSkipXML($indexRef, $lineNumberRef) )
955         {
956         }
957
958     else
959         {  $$indexRef++;  };
960     };
961
962
963 #
964 #   Function: GenericSkipUntilAfter
965 #
966 #   Advances the position via <GenericSkip()> until a specific token is reached and passed.
967 #
968 sub GenericSkipUntilAfter #(indexRef, lineNumberRef, token)
969     {
970     my ($self, $indexRef, $lineNumberRef, $token) = @_;
971     my $tokens = $self->Tokens();
972
973     while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] ne $token)
974         {  $self->GenericSkip($indexRef, $lineNumberRef);  };
975
976     if ($tokens->[$$indexRef] eq "\n")
977         {  $$lineNumberRef++;  };
978     $$indexRef++;
979     };
980
981
982 #
983 #   Function: IndiscriminateSkipUntilAfterSequence
984 #
985 #   Advances the position indiscriminately until a specific token sequence is reached and passed.
986 #
987 sub IndiscriminateSkipUntilAfterSequence #(indexRef, lineNumberRef, token, token, ...)
988     {
989     my ($self, $indexRef, $lineNumberRef, @sequence) = @_;
990     my $tokens = $self->Tokens();
991
992     while ($$indexRef < scalar @$tokens && !$self->IsAtSequence($$indexRef, @sequence))
993         {
994         if ($tokens->[$$indexRef] eq "\n")
995             {  $$lineNumberRef++;  };
996         $$indexRef++;
997         };
998
999     if ($self->IsAtSequence($$indexRef, @sequence))
1000         {
1001         $$indexRef += scalar @sequence;
1002         foreach my $token (@sequence)
1003             {
1004             if ($token eq "\n")
1005                 {  $$lineNumberRef++;  };
1006             };
1007         };
1008     };
1009
1010
1011 #
1012 #   Function: SkipToNextStatement
1013 #
1014 #   Advances the position via <GenericSkip()> until the next statement, which is defined as anything in <declarationEnders> not
1015 #   appearing in brackets or strings.  It will always advance at least one token.
1016 #
1017 sub SkipToNextStatement #(indexRef, lineNumberRef)
1018     {
1019     my ($self, $indexRef, $lineNumberRef) = @_;
1020     my $tokens = $self->Tokens();
1021
1022     if ($tokens->[$$indexRef] eq ';')
1023         {  $$indexRef++;  }
1024
1025     else
1026         {
1027         do
1028             {
1029             $self->GenericSkip($indexRef, $lineNumberRef);
1030             }
1031         while ( $$indexRef < scalar @$tokens &&
1032                   !exists $declarationEnders{$tokens->[$$indexRef]} );
1033         };
1034     };
1035
1036
1037 #
1038 #   Function: TryToSkipRegExp
1039 #   If the current position is on a regular expression, skip past it and return true.
1040 #
1041 sub TryToSkipRegExp #(indexRef, lineNumberRef)
1042     {
1043     my ($self, $indexRef, $lineNumberRef) = @_;
1044     my $tokens = $self->Tokens();
1045
1046     if ($tokens->[$$indexRef] eq '/')
1047         {
1048         # A slash can either start a regular expression or be a divide symbol.  Skip backwards to see what the previous symbol is.
1049         my $index = $$indexRef - 1;
1050
1051         while ($index >= 0 && $tokens->[$index] =~ /^(?: |\t|\n)/)
1052             {  $index--;  };
1053
1054         if ($index < 0 || $tokens->[$index] !~ /^[\:\=\(\[\,]/)
1055             {  return 0;  };
1056
1057         $$indexRef++;
1058
1059         while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] ne '/')
1060             {
1061             if ($tokens->[$$indexRef] eq '\\')
1062                 {  $$indexRef += 2;  }
1063             elsif ($tokens->[$$indexRef] eq "\n")
1064                 {
1065                 $$indexRef++;
1066                 $$lineNumberRef++;
1067                 }
1068             else
1069                 {  $$indexRef++;  }
1070             };
1071
1072         if ($$indexRef < scalar @$tokens)
1073             {
1074             $$indexRef++;
1075
1076             if ($tokens->[$$indexRef] =~ /^[gimsx]+$/i)
1077                 {  $$indexRef++;  };
1078             };
1079
1080         return 1;
1081         }
1082     else
1083         {  return 0;  };
1084     };
1085
1086
1087 #
1088 #   Function: TryToSkipXML
1089 #   If the current position is on an XML literal, skip past it and return true.
1090 #
1091 sub TryToSkipXML #(indexRef, lineNumberRef)
1092     {
1093     my ($self, $indexRef, $lineNumberRef) = @_;
1094     my $tokens = $self->Tokens();
1095
1096     if ($tokens->[$$indexRef] eq '<')
1097         {
1098         # A < can either start an XML literal or be a comparison or shift operator.  First check the next character for << or <=.
1099
1100         my $index = $$indexRef + 1;
1101
1102         while ($index < scalar @$tokens && $tokens->[$index] =~ /^[\=\<]$/)
1103             {  return 0;  };
1104
1105
1106         # Next try the previous character.
1107
1108         $index = $$indexRef - 1;
1109
1110         while ($index >= 0 && $tokens->[$index] =~ /^[ |\t|\n]/)
1111             {  $index--;  };
1112
1113         if ($index < 0 || $tokens->[$index] !~ /^[\=\(\[\,\>]/)
1114             {  return 0;  };
1115         }
1116     else
1117         {  return 0;  };
1118
1119
1120     # Only handle the tag here if it's not an irregular XML section.
1121     if (!$self->TryToSkipIrregularXML($indexRef, $lineNumberRef))
1122         {
1123         my @tagStack;
1124
1125         my ($tagType, $tagIdentifier) = $self->GetAndSkipXMLTag($indexRef, $lineNumberRef);
1126         if ($tagType == XML_OPENING_TAG)
1127             {  push @tagStack, $tagIdentifier;  };
1128
1129         while (scalar @tagStack && $$indexRef < scalar @$tokens)
1130             {
1131             $self->SkipToNextXMLTag($indexRef, $lineNumberRef);
1132             ($tagType, $tagIdentifier) = $self->GetAndSkipXMLTag($indexRef, $lineNumberRef);
1133
1134             if ($tagType == XML_OPENING_TAG)
1135                 {  push @tagStack, $tagIdentifier;  }
1136             elsif ($tagType == XML_CLOSING_TAG && $tagIdentifier eq $tagStack[-1])
1137                 {  pop @tagStack;  };
1138             };
1139         };
1140
1141
1142     return 1;
1143     };
1144
1145
1146 #
1147 #   Function: TryToSkipIrregularXML
1148 #
1149 #   If the current position is on an irregular XML tag, skip past it and return true.  Irregular XML tags are defined as
1150 #
1151 #       CDATA - <![CDATA[ ... ]]>
1152 #       Comments - <!-- ... -->
1153 #       PI - <? ... ?>
1154 #
1155 sub TryToSkipIrregularXML #(indexRef, lineNumberRef)
1156     {
1157     my ($self, $indexRef, $lineNumberRef) = @_;
1158
1159     if ($self->IsAtSequence($$indexRef, '<', '!', '[', 'CDATA', '['))
1160         {
1161         $$indexRef += 5;
1162         $self->IndiscriminateSkipUntilAfterSequence($indexRef, $lineNumberRef, ']', ']', '>');
1163         return 1;
1164         }
1165
1166     elsif ($self->IsAtSequence($$indexRef, '<', '!', '-', '-'))
1167         {
1168         $$indexRef += 4;
1169         $self->IndiscriminateSkipUntilAfterSequence($indexRef, $lineNumberRef, '-', '-', '>');
1170         return 1;
1171         }
1172
1173     elsif ($self->IsAtSequence($$indexRef, '<', '?'))
1174         {
1175         $$indexRef += 2;
1176         $self->IndiscriminateSkipUntilAfterSequence($indexRef, $lineNumberRef, '?', '>');
1177         return 1;
1178         }
1179
1180     else
1181         {  return 0;  };
1182     };
1183
1184
1185 #
1186 #   Function: GetAndSkipXMLTag
1187 #
1188 #   Processes the XML tag at the current position, moves beyond it, and returns information about it.  Assumes the position is on
1189 #   the opening angle bracket of the tag and the tag is a normal XML tag, not one of the ones handled by
1190 #   <TryToSkipIrregularXML()>.
1191 #
1192 #   Parameters:
1193 #
1194 #       indexRef - A reference to the index of the position of the opening angle bracket.
1195 #       lineNumberRef - A reference to the line number of the position of the opening angle bracket.
1196 #
1197 #   Returns:
1198 #
1199 #       The array ( tagType, name ).
1200 #
1201 #       tagType - One of the <XML Tag Type> constants.
1202 #       identifier - The identifier of the tag.  If it's an empty tag (<> or </>), this will be "(anonymous)".
1203 #
1204 sub GetAndSkipXMLTag #(indexRef, lineNumberRef)
1205     {
1206     my ($self, $indexRef, $lineNumberRef) = @_;
1207     my $tokens = $self->Tokens();
1208
1209     if ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] ne '<')
1210         {  die "Tried to call GetXMLTag when the position isn't on an opening bracket.";  };
1211
1212     # Get the anonymous ones out of the way so we don't have to worry about them below, since they're rather exceptional.
1213
1214     if ($self->IsAtSequence($$indexRef, '<', '>'))
1215         {
1216         $$indexRef += 2;
1217         return ( XML_OPENING_TAG, '(anonymous)' );
1218         }
1219     elsif ($self->IsAtSequence($$indexRef, '<', '/', '>'))
1220         {
1221         $$indexRef += 3;
1222         return ( XML_CLOSING_TAG, '(anonymous)' );
1223         };
1224
1225
1226     # Grab the identifier.
1227
1228     my $tagType = XML_OPENING_TAG;
1229     my $identifier;
1230
1231     $$indexRef++;
1232
1233     if ($tokens->[$$indexRef] eq '/')
1234         {
1235         $$indexRef++;
1236         $tagType = XML_CLOSING_TAG;
1237         };
1238
1239     $self->TryToSkipXMLWhitespace($indexRef, $lineNumberRef);
1240
1241
1242     # The identifier could be a native expression in braces.
1243
1244     if ($tokens->[$$indexRef] eq '{')
1245         {
1246         my $startOfIdentifier = $$indexRef;
1247
1248         $$indexRef++;
1249         $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}');
1250
1251         $identifier = $self->CreateString($startOfIdentifier, $$indexRef);
1252         }
1253
1254
1255     # Otherwise just grab content until whitespace or the end of the tag.
1256
1257     else
1258         {
1259         while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] !~ /^[\/\>\ \t]$/)
1260             {
1261             $identifier .= $tokens->[$$indexRef];
1262             $$indexRef++;
1263             };
1264         };
1265
1266
1267     # Skip to the end of the tag.
1268
1269     while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] !~ /^[\/\>]$/)
1270         {
1271         if ($tokens->[$$indexRef] eq '{')
1272             {
1273             $$indexRef++;
1274             $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}');
1275             }
1276
1277         elsif ($self->TryToSkipXMLWhitespace($indexRef, $lineNumberRef))
1278             {  }
1279
1280         # We don't need to do special handling for attribute quotes or anything like that because there's no backslashing in
1281         # XML.  It's all handled with entity characters.
1282         else
1283             {  $$indexRef++;  };
1284         };
1285
1286
1287     if ($tokens->[$$indexRef] eq '/')
1288         {
1289         if ($tagType == XML_OPENING_TAG)
1290             {  $tagType = XML_SELF_CONTAINED_TAG;  };
1291
1292         $$indexRef++;
1293         };
1294
1295     if ($tokens->[$$indexRef] eq '>')
1296         {  $$indexRef++;  };
1297
1298     if (!$identifier)
1299         {  $identifier = '(anonymous)';  };
1300
1301
1302     return ( $tagType, $identifier );
1303     };
1304
1305
1306 #
1307 #   Function: SkipToNextXMLTag
1308 #   Skips to the next normal XML tag.  It will not stop at elements handled by <TryToSkipIrregularXML()>.  Note that if the
1309 #   position is already at an XML tag, it will not move.
1310 #
1311 sub SkipToNextXMLTag #(indexRef, lineNumberRef)
1312     {
1313     my ($self, $indexRef, $lineNumberRef) = @_;
1314     my $tokens = $self->Tokens();
1315
1316     while ($$indexRef < scalar @$tokens)
1317         {
1318         if ($tokens->[$$indexRef] eq '{')
1319             {
1320             $$indexRef++;
1321             $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}');
1322             }
1323
1324         elsif ($self->TryToSkipIrregularXML($indexRef, $lineNumberRef))
1325             {  }
1326
1327         elsif ($tokens->[$$indexRef] eq '<')
1328             {  last;  }
1329
1330         else
1331             {
1332             if ($tokens->[$$indexRef] eq "\n")
1333                 {  $$lineNumberRef++;  };
1334
1335             $$indexRef++;
1336             };
1337         };
1338     };
1339
1340
1341 #
1342 #   Function: TryToSkipXMLWhitespace
1343 #   If the current position is on XML whitespace, skip past it and return true.
1344 #
1345 sub TryToSkipXMLWhitespace #(indexRef, lineNumberRef)
1346     {
1347     my ($self, $indexRef, $lineNumberRef) = @_;
1348     my $tokens = $self->Tokens();
1349
1350     my $result;
1351
1352     while ($$indexRef < scalar @$tokens)
1353         {
1354         if ($tokens->[$$indexRef] =~ /^[ \t]/)
1355             {
1356             $$indexRef++;
1357             $result = 1;
1358             }
1359         elsif ($tokens->[$$indexRef] eq "\n")
1360             {
1361             $$indexRef++;
1362             $$lineNumberRef++;
1363             $result = 1;
1364             }
1365         else
1366             {  last;  };
1367         };
1368
1369     return $result;
1370     };
1371
1372
1373 #
1374 #   Function: TryToSkipString
1375 #   If the current position is on a string delimiter, skip past the string and return true.
1376 #
1377 #   Parameters:
1378 #
1379 #       indexRef - A reference to the index of the position to start at.
1380 #       lineNumberRef - A reference to the line number of the position.
1381 #
1382 #   Returns:
1383 #
1384 #       Whether the position was at a string.
1385 #
1386 #   Syntax Support:
1387 #
1388 #       - Supports quotes and apostrophes.
1389 #
1390 sub TryToSkipString #(indexRef, lineNumberRef)
1391     {
1392     my ($self, $indexRef, $lineNumberRef) = @_;
1393
1394     return ($self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '\'') ||
1395                $self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '"') );
1396     };
1397
1398
1399 #
1400 #   Function: TryToSkipWhitespace
1401 #   If the current position is on a whitespace token, a line break token, or a comment, it skips them and returns true.  If there are
1402 #   a number of these in a row, it skips them all.
1403 #
1404 sub TryToSkipWhitespace #(indexRef, lineNumberRef)
1405     {
1406     my ($self, $indexRef, $lineNumberRef) = @_;
1407     my $tokens = $self->Tokens();
1408
1409     my $result;
1410
1411     while ($$indexRef < scalar @$tokens)
1412         {
1413         if ($tokens->[$$indexRef] =~ /^[ \t]/)
1414             {
1415             $$indexRef++;
1416             $result = 1;
1417             }
1418         elsif ($tokens->[$$indexRef] eq "\n")
1419             {
1420             $$indexRef++;
1421             $$lineNumberRef++;
1422             $result = 1;
1423             }
1424         elsif ($self->TryToSkipComment($indexRef, $lineNumberRef))
1425             {
1426             $result = 1;
1427             }
1428         else
1429             {  last;  };
1430         };
1431
1432     return $result;
1433     };
1434
1435
1436 #
1437 #   Function: TryToSkipComment
1438 #   If the current position is on a comment, skip past it and return true.
1439 #
1440 sub TryToSkipComment #(indexRef, lineNumberRef)
1441     {
1442     my ($self, $indexRef, $lineNumberRef) = @_;
1443
1444     return ( $self->TryToSkipLineComment($indexRef, $lineNumberRef) ||
1445                 $self->TryToSkipMultilineComment($indexRef, $lineNumberRef) );
1446     };
1447
1448
1449 #
1450 #   Function: TryToSkipLineComment
1451 #   If the current position is on a line comment symbol, skip past it and return true.
1452 #
1453 sub TryToSkipLineComment #(indexRef, lineNumberRef)
1454     {
1455     my ($self, $indexRef, $lineNumberRef) = @_;
1456     my $tokens = $self->Tokens();
1457
1458     if ($tokens->[$$indexRef] eq '/' && $tokens->[$$indexRef+1] eq '/')
1459         {
1460         $self->SkipRestOfLine($indexRef, $lineNumberRef);
1461         return 1;
1462         }
1463     else
1464         {  return undef;  };
1465     };
1466
1467
1468 #
1469 #   Function: TryToSkipMultilineComment
1470 #   If the current position is on an opening comment symbol, skip past it and return true.
1471 #
1472 sub TryToSkipMultilineComment #(indexRef, lineNumberRef)
1473     {
1474     my ($self, $indexRef, $lineNumberRef) = @_;
1475     my $tokens = $self->Tokens();
1476
1477     if ($tokens->[$$indexRef] eq '/' && $tokens->[$$indexRef+1] eq '*')
1478         {
1479         $self->SkipUntilAfter($indexRef, $lineNumberRef, '*', '/');
1480         return 1;
1481         }
1482     else
1483         {  return undef;  };
1484     };
1485
1486
1487 1;