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
15use strict;
16use integer;
17
18package NaturalDocs::Languages::ActionScript;
19
20use 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#
34use constant XML_OPENING_TAG => 1;
35use constant XML_CLOSING_TAG => 2;
36use 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#
46my %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#
56my %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#
69my %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#
94my $isEscaped;
95
96
97
98################################################################################
99# Group: Interface Functions
100
101
102#
103#   Function: PackageSeparator
104#   Returns the package separator symbol.
105#
106sub PackageSeparator
107    {  return '.';  };
108
109
110#
111#   Function: EnumValues
112#   Returns the <EnumValuesType> that describes how the language handles enums.
113#
114sub 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#
122sub 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#
142sub 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#
153sub 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#
277sub 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#
360sub 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#
440sub 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#
488sub 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#
633sub 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#
779sub 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#
929sub 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#
968sub 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#
987sub 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#
1017sub 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#
1041sub 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#
1091sub 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#
1155sub 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#
1204sub 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#
1311sub 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#
1345sub 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#
1390sub 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#
1404sub 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#
1440sub 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#
1453sub 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#
1472sub 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
14871;
1488