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