1 ###############################################################################
3 # Class: NaturalDocs::Languages::ActionScript
5 ###############################################################################
7 # A subclass to handle the language variations of Flash ActionScript.
9 ###############################################################################
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
18 package NaturalDocs::Languages::ActionScript;
20 use base 'NaturalDocs::Languages::Advanced';
23 ################################################################################
24 # Group: Constants and Types
28 # Constants: XML Tag Type
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 />.
34 use constant XML_OPENING_TAG => 1;
35 use constant XML_CLOSING_TAG => 2;
36 use constant XML_SELF_CONTAINED_TAG => 3;
39 ################################################################################
40 # Group: Package Variables
43 # hash: classModifiers
44 # An existence hash of all the acceptable class modifiers. The keys are in all lowercase.
46 my %classModifiers = ( 'dynamic' => 1,
53 # hash: memberModifiers
54 # An existence hash of all the acceptable class member modifiers. The keys are in all lowercase.
56 my %memberModifiers = ( 'public' => 1,
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.
69 my %declarationEnders = ( ';' => 1,
92 # Whether the current file being parsed uses escapement.
98 ################################################################################
99 # Group: Interface Functions
103 # Function: PackageSeparator
104 # Returns the package separator symbol.
111 # Function: EnumValues
112 # Returns the <EnumValuesType> that describes how the language handles enums.
115 { return ::ENUM_GLOBAL(); };
119 # Function: ParseParameterLine
120 # Parses a prototype parameter line and returns it as a <NaturalDocs::Languages::Prototype::Parameter> object.
122 sub ParseParameterLine #(line)
124 my ($self, $line) = @_;
126 if ($line =~ /^ ?\.\.\.\ (.+)$/)
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);
134 { return $self->ParsePascalParameterLine($line); };
139 # Function: TypeBeforeParameter
140 # Returns whether the type appears before the parameter in prototypes.
142 sub TypeBeforeParameter
147 # Function: PreprocessFile
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.
155 my ($self, $lines) = @_;
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;
168 my $mode = MODE_UNESCAPED_REGULAR;
170 for (my $i = 0; $i < scalar @$lines; $i++)
172 my @tokens = split(/(<[ \t]*\/?[ \t]*mx:Script[^>]*>|<\?|\?>|<\!--|-->|<\!\[CDATA\[|\]\]\>)/, $lines->[$i]);
175 foreach my $token (@tokens)
177 if ($mode == MODE_UNESCAPED_REGULAR)
180 { $mode = MODE_UNESCAPED_PI; }
181 elsif ($token eq '<![CDATA[')
182 { $mode = MODE_UNESCAPED_CDATA; }
183 elsif ($token eq '<!--')
185 $mode = MODE_UNESCAPED_COMMENT;
186 $newLine .= "\x1C\x1D\x1E\x1F";
188 elsif ($token =~ /^<[ \t]*mx:Script/)
189 { $mode = MODE_ESCAPED_UNKNOWN_CDATA; };
192 elsif ($mode == MODE_UNESCAPED_PI)
195 { $mode = MODE_UNESCAPED_REGULAR; };
198 elsif ($mode == MODE_UNESCAPED_CDATA)
201 { $mode = MODE_UNESCAPED_REGULAR; };
204 elsif ($mode == MODE_UNESCAPED_COMMENT)
208 $mode = MODE_UNESCAPED_REGULAR;
209 $newLine .= "\x1F\x1E\x1D";
212 { $newLine .= $token; };
215 elsif ($mode == MODE_ESCAPED_UNKNOWN_CDATA)
217 if ($token eq '<![CDATA[')
218 { $mode = MODE_ESCAPED_CDATA; }
219 elsif ($token =~ /^<[ \t]*\/[ \t]*mx:Script/)
221 $mode = MODE_UNESCAPED_REGULAR;
224 elsif ($token !~ /^[ \t]*$/)
226 $mode = MODE_ESCAPED_NO_CDATA;
231 elsif ($mode == MODE_ESCAPED_CDATA)
235 $mode = MODE_UNESCAPED_REGULAR;
239 { $newLine .= $token; };
242 else #($mode == MODE_ESCAPED_NO_CDATA)
244 if ($token =~ /^<[ \t]*\/[ \t]*mx:Script/)
246 $mode = MODE_UNESCAPED_REGULAR;
250 { $newLine .= $token; };
255 $lines->[$i] = $newLine;
261 # Function: ParseFile
263 # Parses the passed source file, sending comments acceptable for documentation to <NaturalDocs::Parser->OnComment()>.
267 # sourceFile - The <FileName> to parse.
268 # topicList - A reference to the list of <NaturalDocs::Parser::ParsedTopics> being built by the file.
272 # The array ( autoTopics, scopeRecord ).
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.
277 sub ParseFile #(sourceFile, topicsList)
279 my ($self, $sourceFile, $topicsList) = @_;
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" ], [ '///' ], [ '/**', '*/' ] );
284 my $extension = lc(NaturalDocs::File->ExtensionOf($sourceFile));
285 $isEscaped = ($extension eq 'mxml');
287 $self->ParseForCommentsAndTokens($sourceFile, @parseParameters);
289 my $tokens = $self->Tokens();
293 while ($index < scalar @$tokens)
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) )
301 # The functions above will handle everything.
304 elsif ($tokens->[$index] eq '{')
306 $self->StartScope('}', $lineNumber, undef, undef, undef);
310 elsif ($tokens->[$index] eq '}')
312 if ($self->ClosingScopeSymbol() eq '}')
313 { $self->EndScope($lineNumber); };
320 $self->SkipToNextStatement(\$index, \$lineNumber);
325 # Don't need to keep these around.
326 $self->ClearTokens();
329 my $autoTopics = $self->AutoTopics();
331 my $scopeRecord = $self->ScopeRecord();
332 if (defined $scopeRecord && !scalar @$scopeRecord)
333 { $scopeRecord = undef; };
335 return ( $autoTopics, $scopeRecord );
340 ################################################################################
341 # Group: Statement Parsing Functions
342 # All functions here assume that the current position is at the beginning of a statement.
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.
349 # Function: TryToGetIdentifier
351 # Determines whether the position is at an identifier, and if so, skips it and returns the complete identifier as a string. Returns
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.
360 sub TryToGetIdentifier #(indexRef, lineNumberRef, allowStar)
362 my ($self, $indexRef, $lineNumberRef, $allowStar) = @_;
363 my $tokens = $self->Tokens();
365 my $index = $$indexRef;
367 use constant MODE_IDENTIFIER_START => 1;
368 use constant MODE_IN_IDENTIFIER => 2;
369 use constant MODE_AFTER_STAR => 3;
372 my $mode = MODE_IDENTIFIER_START;
374 while ($index < scalar @$tokens)
376 if ($mode == MODE_IDENTIFIER_START)
378 if ($tokens->[$index] =~ /^[a-z\$\_]/i)
380 $identifier .= $tokens->[$index];
383 $mode = MODE_IN_IDENTIFIER;
385 elsif ($allowStar && $tokens->[$index] eq '*')
390 $mode = MODE_AFTER_STAR;
396 elsif ($mode == MODE_IN_IDENTIFIER)
398 if ($tokens->[$index] eq '.')
403 $mode = MODE_IDENTIFIER_START;
405 elsif ($tokens->[$index] =~ /^[a-z0-9\$\_]/i)
407 $identifier .= $tokens->[$index];
414 else #($mode == MODE_AFTER_STAR)
416 if ($tokens->[$index] =~ /^[a-z0-9\$\_\.]/i)
423 # We need to check again because we may have run out of tokens after a dot.
424 if ($mode != MODE_IDENTIFIER_START)
435 # Function: TryToGetImport
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.
440 sub TryToGetImport #(indexRef, lineNumberRef)
442 my ($self, $indexRef, $lineNumberRef) = @_;
443 my $tokens = $self->Tokens();
445 my $index = $$indexRef;
446 my $lineNumber = $$lineNumberRef;
448 if ($tokens->[$index] ne 'import')
452 $self->TryToSkipWhitespace(\$index, \$lineNumber);
454 my $identifier = $self->TryToGetIdentifier(\$index, \$lineNumber, 1);
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.
463 if (index($identifier, '.') != -1)
465 $identifier =~ s/\.[^\.]+$//;
466 $self->AddUsing( NaturalDocs::SymbolString->FromText($identifier) );
470 $$lineNumberRef = $lineNumber;
477 # Function: TryToGetClass
479 # Determines whether the position is at a class declaration statement, and if so, generates a topic for it, skips it, and
482 # Supported Syntaxes:
486 # - Classes and interfaces with _global
488 sub TryToGetClass #(indexRef, lineNumberRef)
490 my ($self, $indexRef, $lineNumberRef) = @_;
491 my $tokens = $self->Tokens();
493 my $index = $$indexRef;
494 my $lineNumber = $$lineNumberRef;
498 while ($tokens->[$index] =~ /^[a-z]/i &&
499 exists $classModifiers{lc($tokens->[$index])} )
501 push @modifiers, lc($tokens->[$index]);
504 $self->TryToSkipWhitespace(\$index, \$lineNumber);
509 if ($tokens->[$index] eq 'class' || $tokens->[$index] eq 'interface')
511 $type = $tokens->[$index];
514 $self->TryToSkipWhitespace(\$index, \$lineNumber);
519 my $className = $self->TryToGetIdentifier(\$index, \$lineNumber);
524 $self->TryToSkipWhitespace(\$index, \$lineNumber);
528 if ($tokens->[$index] eq 'extends')
531 $self->TryToSkipWhitespace(\$index, \$lineNumber);
533 # Interfaces can extend multiple other interfaces, which is NOT clearly mentioned in the docs.
537 my $parent = $self->TryToGetIdentifier(\$index, \$lineNumber);
541 push @parents, $parent;
543 $self->TryToSkipWhitespace(\$index, \$lineNumber);
545 if ($tokens->[$index] ne ',')
550 $self->TryToSkipWhitespace(\$index, \$lineNumber);
555 if ($type eq 'class' && $tokens->[$index] eq 'implements')
558 $self->TryToSkipWhitespace(\$index, \$lineNumber);
562 my $parent = $self->TryToGetIdentifier(\$index, \$lineNumber);
566 push @parents, $parent;
568 $self->TryToSkipWhitespace(\$index, \$lineNumber);
570 if ($tokens->[$index] ne ',')
575 $self->TryToSkipWhitespace(\$index, \$lineNumber);
580 if ($tokens->[$index] ne '{')
586 # If we made it this far, we have a valid class declaration.
590 if ($type eq 'interface')
591 { $topicType = ::TOPIC_INTERFACE(); }
593 { $topicType = ::TOPIC_CLASS(); };
595 $className =~ s/^_global.//;
597 my $autoTopic = NaturalDocs::Parser::ParsedTopic->New($topicType, $className,
598 undef, $self->CurrentUsing(),
600 undef, undef, $$lineNumberRef);
602 $self->AddAutoTopic($autoTopic);
603 NaturalDocs::Parser->OnClass($autoTopic->Package());
605 foreach my $parent (@parents)
607 NaturalDocs::Parser->OnClassParent($autoTopic->Package(), NaturalDocs::SymbolString->FromText($parent),
608 undef, $self->CurrentUsing(), ::RESOLVE_ABSOLUTE());
611 $self->StartScope('}', $lineNumber, $autoTopic->Package());
614 $$lineNumberRef = $lineNumber;
621 # Function: TryToGetFunction
623 # Determines if the position is on a function declaration, and if so, generates a topic for it, skips it, and returns true.
625 # Supported Syntaxes:
630 # - Functions with _global
631 # - Functions with namespaces
633 sub TryToGetFunction #(indexRef, lineNumberRef)
635 my ($self, $indexRef, $lineNumberRef) = @_;
636 my $tokens = $self->Tokens();
638 my $index = $$indexRef;
639 my $lineNumber = $$lineNumberRef;
641 my $startIndex = $index;
642 my $startLine = $lineNumber;
647 while ($tokens->[$index] =~ /^[a-z]/i)
649 if ($tokens->[$index] eq 'function')
652 elsif (exists $memberModifiers{lc($tokens->[$index])})
654 push @modifiers, lc($tokens->[$index]);
657 $self->TryToSkipWhitespace(\$index, \$lineNumber);
664 $namespace .= $tokens->[$index];
667 while ($tokens->[$index] =~ /^[a-z0-9_]/i);
669 $self->TryToSkipWhitespace(\$index, \$lineNumber);
676 if ($tokens->[$index] ne 'function')
680 $self->TryToSkipWhitespace(\$index, \$lineNumber);
684 if ($tokens->[$index] eq 'get' || $tokens->[$index] eq 'set')
686 # This can either be a property ("function get Something()") or a function name ("function get()").
688 my $nextIndex = $index;
689 my $nextLineNumber = $lineNumber;
692 $self->TryToSkipWhitespace(\$nextIndex, \$nextLineNumber);
694 if ($tokens->[$nextIndex] eq '(')
696 $type = ::TOPIC_FUNCTION();
697 # Ignore the movement and let the code ahead pick it up as the name.
701 $type = ::TOPIC_PROPERTY();
703 $lineNumber = $nextLineNumber;
707 { $type = ::TOPIC_FUNCTION(); };
709 my $name = $self->TryToGetIdentifier(\$index, \$lineNumber);
713 $self->TryToSkipWhitespace(\$index, \$lineNumber);
715 if ($tokens->[$index] ne '(')
719 $self->GenericSkipUntilAfter(\$index, \$lineNumber, ')');
721 $self->TryToSkipWhitespace(\$index, \$lineNumber);
723 if ($tokens->[$index] eq ':')
727 $self->TryToSkipWhitespace(\$index, \$lineNumber);
729 $self->TryToGetIdentifier(\$index, \$lineNumber, 1);
731 $self->TryToSkipWhitespace(\$index, \$lineNumber);
735 my $prototype = $self->NormalizePrototype( $self->CreateString($startIndex, $index) );
737 if ($tokens->[$index] eq '{')
738 { $self->GenericSkip(\$index, \$lineNumber); }
739 elsif (!exists $declarationEnders{$tokens->[$index]})
743 my $scope = $self->CurrentScope();
745 if ($name =~ s/^_global.//)
748 { $scope = NaturalDocs::SymbolString->Join($scope, $namespace); };
750 $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New($type, $name,
751 $scope, $self->CurrentUsing(),
753 undef, undef, $startLine));
756 # We succeeded if we got this far.
759 $$lineNumberRef = $lineNumber;
766 # Function: TryToGetVariable
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.
771 # Supported Syntaxes:
774 # - Variables with _global
775 # - Variables with type * (untyped)
777 # - Variables and constants with namespaces
779 sub TryToGetVariable #(indexRef, lineNumberRef)
781 my ($self, $indexRef, $lineNumberRef) = @_;
782 my $tokens = $self->Tokens();
784 my $index = $$indexRef;
785 my $lineNumber = $$lineNumberRef;
787 my $startIndex = $index;
788 my $startLine = $lineNumber;
793 while ($tokens->[$index] =~ /^[a-z]/i)
795 if ($tokens->[$index] eq 'var' || $tokens->[$index] eq 'const')
798 elsif (exists $memberModifiers{lc($tokens->[$index])})
800 push @modifiers, lc($tokens->[$index]);
803 $self->TryToSkipWhitespace(\$index, \$lineNumber);
810 $namespace .= $tokens->[$index];
813 while ($tokens->[$index] =~ /^[a-z0-9_]/i);
815 $self->TryToSkipWhitespace(\$index, \$lineNumber);
824 if ($tokens->[$index] eq 'var')
825 { $type = ::TOPIC_VARIABLE(); }
826 elsif ($tokens->[$index] eq 'const')
827 { $type = ::TOPIC_CONSTANT(); }
832 $self->TryToSkipWhitespace(\$index, \$lineNumber);
834 my $endTypeIndex = $index;
840 my $name = $self->TryToGetIdentifier(\$index, \$lineNumber);
844 $self->TryToSkipWhitespace(\$index, \$lineNumber);
848 if ($tokens->[$index] eq ':')
851 $self->TryToSkipWhitespace(\$index, \$lineNumber);
853 $type = ': ' . $self->TryToGetIdentifier(\$index, \$lineNumber, 1);
855 $self->TryToSkipWhitespace(\$index, \$lineNumber);
858 if ($tokens->[$index] eq '=')
862 $self->GenericSkip(\$index, \$lineNumber);
864 while ($tokens->[$index] ne ',' && !exists $declarationEnders{$tokens->[$index]} && $index < scalar @$tokens);
870 if ($tokens->[$index] eq ',')
873 $self->TryToSkipWhitespace(\$index, \$lineNumber);
875 elsif (exists $declarationEnders{$tokens->[$index]})
882 # We succeeded if we got this far.
884 my $prototypePrefix = $self->CreateString($startIndex, $endTypeIndex);
886 for (my $i = 0; $i < scalar @names; $i++)
888 my $prototype = $self->NormalizePrototype( $prototypePrefix . ' ' . $names[$i] . $types[$i]);
889 my $scope = $self->CurrentScope();
891 if ($names[$i] =~ s/^_global.//)
894 { $scope = NaturalDocs::SymbolString->Join($scope, $namespace); };
896 $self->AddAutoTopic(NaturalDocs::Parser::ParsedTopic->New($type, $names[$i],
897 $scope, $self->CurrentUsing(),
899 undef, undef, $startLine));
903 $$lineNumberRef = $lineNumber;
910 ################################################################################
911 # Group: Low Level Parsing Functions
915 # Function: GenericSkip
917 # Advances the position one place through general code.
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.
926 # indexRef - A reference to the current index.
927 # lineNumberRef - A reference to the current line number.
929 sub GenericSkip #(indexRef, lineNumberRef)
931 my ($self, $indexRef, $lineNumberRef) = @_;
932 my $tokens = $self->Tokens();
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 '{')
938 $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}');
940 elsif ($tokens->[$$indexRef] eq '(')
943 $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ')');
945 elsif ($tokens->[$$indexRef] eq '[')
948 $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, ']');
951 elsif ($self->TryToSkipWhitespace($indexRef, $lineNumberRef) ||
952 $self->TryToSkipString($indexRef, $lineNumberRef) ||
953 $self->TryToSkipRegExp($indexRef, $lineNumberRef) ||
954 $self->TryToSkipXML($indexRef, $lineNumberRef) )
964 # Function: GenericSkipUntilAfter
966 # Advances the position via <GenericSkip()> until a specific token is reached and passed.
968 sub GenericSkipUntilAfter #(indexRef, lineNumberRef, token)
970 my ($self, $indexRef, $lineNumberRef, $token) = @_;
971 my $tokens = $self->Tokens();
973 while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] ne $token)
974 { $self->GenericSkip($indexRef, $lineNumberRef); };
976 if ($tokens->[$$indexRef] eq "\n")
977 { $$lineNumberRef++; };
983 # Function: IndiscriminateSkipUntilAfterSequence
985 # Advances the position indiscriminately until a specific token sequence is reached and passed.
987 sub IndiscriminateSkipUntilAfterSequence #(indexRef, lineNumberRef, token, token, ...)
989 my ($self, $indexRef, $lineNumberRef, @sequence) = @_;
990 my $tokens = $self->Tokens();
992 while ($$indexRef < scalar @$tokens && !$self->IsAtSequence($$indexRef, @sequence))
994 if ($tokens->[$$indexRef] eq "\n")
995 { $$lineNumberRef++; };
999 if ($self->IsAtSequence($$indexRef, @sequence))
1001 $$indexRef += scalar @sequence;
1002 foreach my $token (@sequence)
1005 { $$lineNumberRef++; };
1012 # Function: SkipToNextStatement
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.
1017 sub SkipToNextStatement #(indexRef, lineNumberRef)
1019 my ($self, $indexRef, $lineNumberRef) = @_;
1020 my $tokens = $self->Tokens();
1022 if ($tokens->[$$indexRef] eq ';')
1029 $self->GenericSkip($indexRef, $lineNumberRef);
1031 while ( $$indexRef < scalar @$tokens &&
1032 !exists $declarationEnders{$tokens->[$$indexRef]} );
1038 # Function: TryToSkipRegExp
1039 # If the current position is on a regular expression, skip past it and return true.
1041 sub TryToSkipRegExp #(indexRef, lineNumberRef)
1043 my ($self, $indexRef, $lineNumberRef) = @_;
1044 my $tokens = $self->Tokens();
1046 if ($tokens->[$$indexRef] eq '/')
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;
1051 while ($index >= 0 && $tokens->[$index] =~ /^(?: |\t|\n)/)
1054 if ($index < 0 || $tokens->[$index] !~ /^[\:\=\(\[\,]/)
1059 while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] ne '/')
1061 if ($tokens->[$$indexRef] eq '\\')
1062 { $$indexRef += 2; }
1063 elsif ($tokens->[$$indexRef] eq "\n")
1072 if ($$indexRef < scalar @$tokens)
1076 if ($tokens->[$$indexRef] =~ /^[gimsx]+$/i)
1088 # Function: TryToSkipXML
1089 # If the current position is on an XML literal, skip past it and return true.
1091 sub TryToSkipXML #(indexRef, lineNumberRef)
1093 my ($self, $indexRef, $lineNumberRef) = @_;
1094 my $tokens = $self->Tokens();
1096 if ($tokens->[$$indexRef] eq '<')
1098 # A < can either start an XML literal or be a comparison or shift operator. First check the next character for << or <=.
1100 my $index = $$indexRef + 1;
1102 while ($index < scalar @$tokens && $tokens->[$index] =~ /^[\=\<]$/)
1106 # Next try the previous character.
1108 $index = $$indexRef - 1;
1110 while ($index >= 0 && $tokens->[$index] =~ /^[ |\t|\n]/)
1113 if ($index < 0 || $tokens->[$index] !~ /^[\=\(\[\,\>]/)
1120 # Only handle the tag here if it's not an irregular XML section.
1121 if (!$self->TryToSkipIrregularXML($indexRef, $lineNumberRef))
1125 my ($tagType, $tagIdentifier) = $self->GetAndSkipXMLTag($indexRef, $lineNumberRef);
1126 if ($tagType == XML_OPENING_TAG)
1127 { push @tagStack, $tagIdentifier; };
1129 while (scalar @tagStack && $$indexRef < scalar @$tokens)
1131 $self->SkipToNextXMLTag($indexRef, $lineNumberRef);
1132 ($tagType, $tagIdentifier) = $self->GetAndSkipXMLTag($indexRef, $lineNumberRef);
1134 if ($tagType == XML_OPENING_TAG)
1135 { push @tagStack, $tagIdentifier; }
1136 elsif ($tagType == XML_CLOSING_TAG && $tagIdentifier eq $tagStack[-1])
1147 # Function: TryToSkipIrregularXML
1149 # If the current position is on an irregular XML tag, skip past it and return true. Irregular XML tags are defined as
1151 # CDATA - <![CDATA[ ... ]]>
1152 # Comments - <!-- ... -->
1155 sub TryToSkipIrregularXML #(indexRef, lineNumberRef)
1157 my ($self, $indexRef, $lineNumberRef) = @_;
1159 if ($self->IsAtSequence($$indexRef, '<', '!', '[', 'CDATA', '['))
1162 $self->IndiscriminateSkipUntilAfterSequence($indexRef, $lineNumberRef, ']', ']', '>');
1166 elsif ($self->IsAtSequence($$indexRef, '<', '!', '-', '-'))
1169 $self->IndiscriminateSkipUntilAfterSequence($indexRef, $lineNumberRef, '-', '-', '>');
1173 elsif ($self->IsAtSequence($$indexRef, '<', '?'))
1176 $self->IndiscriminateSkipUntilAfterSequence($indexRef, $lineNumberRef, '?', '>');
1186 # Function: GetAndSkipXMLTag
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()>.
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.
1199 # The array ( tagType, name ).
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)".
1204 sub GetAndSkipXMLTag #(indexRef, lineNumberRef)
1206 my ($self, $indexRef, $lineNumberRef) = @_;
1207 my $tokens = $self->Tokens();
1209 if ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] ne '<')
1210 { die "Tried to call GetXMLTag when the position isn't on an opening bracket."; };
1212 # Get the anonymous ones out of the way so we don't have to worry about them below, since they're rather exceptional.
1214 if ($self->IsAtSequence($$indexRef, '<', '>'))
1217 return ( XML_OPENING_TAG, '(anonymous)' );
1219 elsif ($self->IsAtSequence($$indexRef, '<', '/', '>'))
1222 return ( XML_CLOSING_TAG, '(anonymous)' );
1226 # Grab the identifier.
1228 my $tagType = XML_OPENING_TAG;
1233 if ($tokens->[$$indexRef] eq '/')
1236 $tagType = XML_CLOSING_TAG;
1239 $self->TryToSkipXMLWhitespace($indexRef, $lineNumberRef);
1242 # The identifier could be a native expression in braces.
1244 if ($tokens->[$$indexRef] eq '{')
1246 my $startOfIdentifier = $$indexRef;
1249 $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}');
1251 $identifier = $self->CreateString($startOfIdentifier, $$indexRef);
1255 # Otherwise just grab content until whitespace or the end of the tag.
1259 while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] !~ /^[\/\>\ \t]$/)
1261 $identifier .= $tokens->[$$indexRef];
1267 # Skip to the end of the tag.
1269 while ($$indexRef < scalar @$tokens && $tokens->[$$indexRef] !~ /^[\/\>]$/)
1271 if ($tokens->[$$indexRef] eq '{')
1274 $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}');
1277 elsif ($self->TryToSkipXMLWhitespace($indexRef, $lineNumberRef))
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.
1287 if ($tokens->[$$indexRef] eq '/')
1289 if ($tagType == XML_OPENING_TAG)
1290 { $tagType = XML_SELF_CONTAINED_TAG; };
1295 if ($tokens->[$$indexRef] eq '>')
1299 { $identifier = '(anonymous)'; };
1302 return ( $tagType, $identifier );
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.
1311 sub SkipToNextXMLTag #(indexRef, lineNumberRef)
1313 my ($self, $indexRef, $lineNumberRef) = @_;
1314 my $tokens = $self->Tokens();
1316 while ($$indexRef < scalar @$tokens)
1318 if ($tokens->[$$indexRef] eq '{')
1321 $self->GenericSkipUntilAfter($indexRef, $lineNumberRef, '}');
1324 elsif ($self->TryToSkipIrregularXML($indexRef, $lineNumberRef))
1327 elsif ($tokens->[$$indexRef] eq '<')
1332 if ($tokens->[$$indexRef] eq "\n")
1333 { $$lineNumberRef++; };
1342 # Function: TryToSkipXMLWhitespace
1343 # If the current position is on XML whitespace, skip past it and return true.
1345 sub TryToSkipXMLWhitespace #(indexRef, lineNumberRef)
1347 my ($self, $indexRef, $lineNumberRef) = @_;
1348 my $tokens = $self->Tokens();
1352 while ($$indexRef < scalar @$tokens)
1354 if ($tokens->[$$indexRef] =~ /^[ \t]/)
1359 elsif ($tokens->[$$indexRef] eq "\n")
1374 # Function: TryToSkipString
1375 # If the current position is on a string delimiter, skip past the string and return true.
1379 # indexRef - A reference to the index of the position to start at.
1380 # lineNumberRef - A reference to the line number of the position.
1384 # Whether the position was at a string.
1388 # - Supports quotes and apostrophes.
1390 sub TryToSkipString #(indexRef, lineNumberRef)
1392 my ($self, $indexRef, $lineNumberRef) = @_;
1394 return ($self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '\'') ||
1395 $self->SUPER::TryToSkipString($indexRef, $lineNumberRef, '"') );
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.
1404 sub TryToSkipWhitespace #(indexRef, lineNumberRef)
1406 my ($self, $indexRef, $lineNumberRef) = @_;
1407 my $tokens = $self->Tokens();
1411 while ($$indexRef < scalar @$tokens)
1413 if ($tokens->[$$indexRef] =~ /^[ \t]/)
1418 elsif ($tokens->[$$indexRef] eq "\n")
1424 elsif ($self->TryToSkipComment($indexRef, $lineNumberRef))
1437 # Function: TryToSkipComment
1438 # If the current position is on a comment, skip past it and return true.
1440 sub TryToSkipComment #(indexRef, lineNumberRef)
1442 my ($self, $indexRef, $lineNumberRef) = @_;
1444 return ( $self->TryToSkipLineComment($indexRef, $lineNumberRef) ||
1445 $self->TryToSkipMultilineComment($indexRef, $lineNumberRef) );
1450 # Function: TryToSkipLineComment
1451 # If the current position is on a line comment symbol, skip past it and return true.
1453 sub TryToSkipLineComment #(indexRef, lineNumberRef)
1455 my ($self, $indexRef, $lineNumberRef) = @_;
1456 my $tokens = $self->Tokens();
1458 if ($tokens->[$$indexRef] eq '/' && $tokens->[$$indexRef+1] eq '/')
1460 $self->SkipRestOfLine($indexRef, $lineNumberRef);
1469 # Function: TryToSkipMultilineComment
1470 # If the current position is on an opening comment symbol, skip past it and return true.
1472 sub TryToSkipMultilineComment #(indexRef, lineNumberRef)
1474 my ($self, $indexRef, $lineNumberRef) = @_;
1475 my $tokens = $self->Tokens();
1477 if ($tokens->[$$indexRef] eq '/' && $tokens->[$$indexRef+1] eq '*')
1479 $self->SkipUntilAfter($indexRef, $lineNumberRef, '*', '/');