############################################################################### # # Package: NaturalDocs::Builder::HTMLBase # ############################################################################### # # A base package for all the shared functionality in and # . # ############################################################################### # This file is part of Natural Docs, which is Copyright (C) 2003-2005 Greg Valure # Natural Docs is licensed under the GPL use Tie::RefHash; use strict; use integer; package NaturalDocs::Builder::HTMLBase; use base 'NaturalDocs::Builder::Base'; ############################################################################### # Group: Package Variables # These variables are shared by all instances of the package so don't change them. # # handle: FH_CSS_FILE # # The file handle to use when updating CSS files. # # # Hash: abbreviations # # An existence hash of acceptable abbreviations. These are words that won't put a second space # after when followed by period-whitespace-capital letter. Yes, this is seriously over-engineered. # my %abbreviations = ( mr => 1, mrs => 1, ms => 1, dr => 1, rev => 1, fr => 1, 'i.e' => 1, maj => 1, gen => 1, pres => 1, sen => 1, rep => 1, n => 1, s => 1, e => 1, w => 1, ne => 1, se => 1, nw => 1, sw => 1 ); # # array: indexHeadings # # An array of the headings of all the index sections. First is for symbols, second for numbers, and the rest for each letter. # my @indexHeadings = ( '$#!', '0-9', 'A' .. 'Z' ); # # array: indexAnchors # # An array of the HTML anchors of all the index sections. First is for symbols, second for numbers, and the rest for each letter. # my @indexAnchors = ( 'Symbols', 'Numbers', 'A' .. 'Z' ); # # bool: saidUpdatingCSSFile # # Whether the status message "Updating CSS file..." has been displayed. We only want to print it once, no matter how many # HTML-based targets we are building. # my $saidUpdatingCSSFile; # # constant: ADD_HIDDEN_BREAKS # # Just a synonym for "1" so that setting the flag on is clearer in the calling code. # use constant ADD_HIDDEN_BREAKS => 1; ############################################################################### # Group: ToolTip Package Variables # # These variables are for the tooltip generation functions only. Since they're reset on every call to and # , and are only used by them and their support functions, they can be shared by all instances of the # package. # # int: tooltipLinkNumber # # A number used as part of the ID for each link that has a tooltip. Should be incremented whenever one is made. # my $tooltipLinkNumber; # # int: tooltipNumber # # A number used as part of the ID for each tooltip. Should be incremented whenever one is made. # my $tooltipNumber; # # hash: tooltipSymbolsToNumbers # # A hash that maps the tooltip symbols to their assigned numbers. # my %tooltipSymbolsToNumbers; # # string: tooltipHTML # # The generated tooltip HTML. # my $tooltipHTML; ############################################################################### # Group: Menu Package Variables # # These variables are for the menu generation functions only. Since they're reset on every call to and are # only used by it and its support functions, they can be shared by all instances of the package. # # # hash: prebuiltMenus # # A hash that maps output directonies to menu HTML already built for it. There will be no selection or JavaScript in the menus. # my %prebuiltMenus; # # bool: menuNumbersAndLengthsDone # # Set when the variables that only need to be calculated for the menu once are done. This includes , # , , and , and . # my $menuNumbersAndLengthsDone; # # int: menuGroupNumber # # The current menu group number. Each time a group is created, this is incremented so that each one will be unique. # my $menuGroupNumber; # # int: menuLength # # The length of the entire menu, fully expanded. The value is computed from the . # my $menuLength; # # hash: menuGroupLengths # # A hash of the length of each group, *not* including any subgroup contents. The keys are references to each groups' # object, and the values are their lengths computed from the . # my %menuGroupLengths; tie %menuGroupLengths, 'Tie::RefHash'; # # hash: menuGroupNumbers # # A hash of the number of each group, as managed by . The keys are references to each groups' # object, and the values are the number. # my %menuGroupNumbers; tie %menuGroupNumbers, 'Tie::RefHash'; # # int: menuRootLength # # The length of the top-level menu entries without expansion. The value is computed from the . # my $menuRootLength; # # constants: Menu Length Constants # # Constants used to approximate the lengths of the menu or its groups. # # MENU_TITLE_LENGTH - The length of the title. # MENU_SUBTITLE_LENGTH - The length of the subtitle. # MENU_FILE_LENGTH - The length of one file entry. # MENU_GROUP_LENGTH - The length of one group entry. # MENU_TEXT_LENGTH - The length of one text entry. # MENU_LINK_LENGTH - The length of one link entry. # # MENU_LENGTH_LIMIT - The limit of the menu's length. If the total length surpasses this limit, groups that aren't required # to be open to show the selection will default to closed on browsers that support it. # use constant MENU_TITLE_LENGTH => 3; use constant MENU_SUBTITLE_LENGTH => 1; use constant MENU_FILE_LENGTH => 1; use constant MENU_GROUP_LENGTH => 2; # because it's a line and a blank space use constant MENU_TEXT_LENGTH => 1; use constant MENU_LINK_LENGTH => 1; use constant MENU_INDEX_LENGTH => 1; use constant MENU_LENGTH_LIMIT => 35; ############################################################################### # Group: Implemented Interface Functions # # The behavior of these functions is shared between HTML output formats. # # # Function: PurgeFiles # # Deletes the output files associated with the purged source files. # sub PurgeFiles { my $self = shift; my $filesToPurge = NaturalDocs::Project->FilesToPurge(); # Combine directories into a hash to remove duplicate work. my %directoriesToPurge; foreach my $file (keys %$filesToPurge) { # It's possible that there may be files there that aren't in a valid input directory anymore. They won't generate an output # file name so we need to check for undef. my $outputFile = $self->OutputFileOf($file); if (defined $outputFile) { unlink($outputFile); $directoriesToPurge{ NaturalDocs::File->NoFileName($outputFile) } = 1; }; }; foreach my $directory (keys %directoriesToPurge) { NaturalDocs::File->RemoveEmptyTree($directory, NaturalDocs::Settings->OutputDirectoryOf($self)); }; }; # # Function: PurgeIndexes # # Deletes the output files associated with the purged source files. # # Parameters: # # indexes - An existence hashref of the index types to purge. The keys are the or * for the general index. # sub PurgeIndexes #(indexes) { my ($self, $indexes) = @_; foreach my $index (keys %$indexes) { $self->PurgeIndexFiles($index, undef); }; }; # # Function: BeginBuild # # Creates the necessary subdirectories in the output directory. # sub BeginBuild #(hasChanged) { my ($self, $hasChanged) = @_; foreach my $directory ( $self->JavaScriptDirectory(), $self->CSSDirectory(), $self->IndexDirectory() ) { if (!-d $directory) { NaturalDocs::File->CreatePath($directory); }; }; }; # # Function: EndBuild # # Synchronizes the projects CSS and JavaScript files. # sub EndBuild #(hasChanged) { my ($self, $hasChanged) = @_; # Update the style sheets. my $styles = NaturalDocs::Settings->Styles(); my $changed; my $cssDirectory = $self->CSSDirectory(); my $mainCSSFile = $self->MainCSSFile(); for (my $i = 0; $i < scalar @$styles; $i++) { my $outputCSSFile; if (scalar @$styles == 1) { $outputCSSFile = $mainCSSFile; } else { $outputCSSFile = NaturalDocs::File->JoinPaths($cssDirectory, ($i + 1) . '.css'); }; my $masterCSSFile = NaturalDocs::File->JoinPaths( NaturalDocs::Settings->ProjectDirectory(), $styles->[$i] . '.css' ); if (! -e $masterCSSFile) { $masterCSSFile = NaturalDocs::File->JoinPaths( NaturalDocs::Settings->StyleDirectory(), $styles->[$i] . '.css' ); }; # We check both the date and the size in case the user switches between two styles which just happen to have the same # date. Should rarely happen, but it might. if (! -e $outputCSSFile || (stat($masterCSSFile))[9] != (stat($outputCSSFile))[9] || -s $masterCSSFile != -s $outputCSSFile) { if (!NaturalDocs::Settings->IsQuiet() && !$saidUpdatingCSSFile) { print "Updating CSS file...\n"; $saidUpdatingCSSFile = 1; }; NaturalDocs::File->Copy($masterCSSFile, $outputCSSFile); $changed = 1; }; }; my $deleteFrom; if (scalar @$styles == 1) { $deleteFrom = 1; } else { $deleteFrom = scalar @$styles + 1; }; for (;;) { my $file = NaturalDocs::File->JoinPaths($cssDirectory, $deleteFrom . '.css'); if (! -e $file) { last; }; unlink ($file); $deleteFrom++; $changed = 1; }; if ($changed) { if (scalar @$styles > 1) { open(FH_CSS_FILE, '>' . $mainCSSFile); for (my $i = 0; $i < scalar @$styles; $i++) { print FH_CSS_FILE '@import URL("' . ($i + 1) . '.css");' . "\n"; }; close(FH_CSS_FILE); }; }; # Update the JavaScript file. my $jsMaster = NaturalDocs::File->JoinPaths( NaturalDocs::Settings->JavaScriptDirectory(), 'NaturalDocs.js' ); my $jsOutput = $self->MainJavaScriptFile(); # We check both the date and the size in case the user switches between two styles which just happen to have the same # date. Should rarely happen, but it might. if (! -e $jsOutput || (stat($jsMaster))[9] != (stat($jsOutput))[9] || -s $jsMaster != -s $jsOutput) { NaturalDocs::File->Copy($jsMaster, $jsOutput); }; }; ############################################################################### # Group: Section Functions # # function: BuildTitle # # Builds and returns the HTML page title of a file. # # Parameters: # # sourceFile - The source to build the title of. # # Returns: # # The source file's title in HTML. # sub BuildTitle #(sourceFile) { my ($self, $sourceFile) = @_; # If we have a menu title, the page title is [menu title] - [file title]. Otherwise it is just [file title]. my $title = NaturalDocs::Project->DefaultMenuTitleOf($sourceFile); my $menuTitle = NaturalDocs::Menu->Title(); if (defined $menuTitle && $menuTitle ne $title) { $title .= ' - ' . $menuTitle; }; $title = $self->StringToHTML($title); return $title; }; # # function: BuildMenu # # Builds and returns the side menu of a file. # # Parameters: # # sourceFile - The source to use if you're looking for a source file. # indexType - The index to use if you're looking for an index. # isFramed - Whether the menu will appear in a frame. If so, it assumes the HTML tag is set to make links go to the # appropriate frame. # # Both sourceFile and indexType may be undef. # # Returns: # # The side menu in HTML. # sub BuildMenu #(FileName sourceFile, TopicType indexType, bool isFramed) -> string htmlMenu { my ($self, $sourceFile, $indexType, $isFramed) = @_; if (!$menuNumbersAndLengthsDone) { $menuGroupNumber = 1; $menuLength = 0; %menuGroupLengths = ( ); %menuGroupNumbers = ( ); $menuRootLength = 0; }; my $outputDirectory; if ($sourceFile) { $outputDirectory = NaturalDocs::File->NoFileName( $self->OutputFileOf($sourceFile) ); } elsif ($indexType) { $outputDirectory = NaturalDocs::File->NoFileName( $self->IndexFileOf($indexType) ); } else { $outputDirectory = NaturalDocs::Settings->OutputDirectoryOf($self); }; # Comment needed for UpdateFile(). my $output = ''; if (!exists $prebuiltMenus{$outputDirectory}) { my $segmentOutput; ($segmentOutput, $menuRootLength) = $self->BuildMenuSegment($outputDirectory, $isFramed, NaturalDocs::Menu->Content()); my $titleOutput; my $menuTitle = NaturalDocs::Menu->Title(); if (defined $menuTitle) { if (!$menuNumbersAndLengthsDone) { $menuLength += MENU_TITLE_LENGTH; }; $menuRootLength += MENU_TITLE_LENGTH; $titleOutput .= '
' . $self->StringToHTML($menuTitle); my $menuSubTitle = NaturalDocs::Menu->SubTitle(); if (defined $menuSubTitle) { if (!$menuNumbersAndLengthsDone) { $menuLength += MENU_SUBTITLE_LENGTH; }; $menuRootLength += MENU_SUBTITLE_LENGTH; $titleOutput .= '
' . $self->StringToHTML($menuSubTitle) . '
'; }; $titleOutput .= '
'; }; $prebuiltMenus{$outputDirectory} = $titleOutput . $segmentOutput; $output .= $titleOutput . $segmentOutput; } else { $output .= $prebuiltMenus{$outputDirectory}; }; # Highlight the menu selection. if ($sourceFile) { # Dependency: This depends on how BuildMenuSegment() formats file entries. my $outputFile = $self->OutputFileOf($sourceFile); my $tag = '
'; my $tagIndex = index($output, $tag); if ($tagIndex != -1) { my $endIndex = index($output, '', $tagIndex); substr($output, $endIndex, 4, ''); substr($output, $tagIndex, length($tag), '
'); }; } elsif ($indexType) { # Dependency: This depends on how BuildMenuSegment() formats index entries. my $outputFile = $self->IndexFileOf($indexType); my $tag = '
'; my $tagIndex = index($output, $tag); if ($tagIndex != -1) { my $endIndex = index($output, '', $tagIndex); substr($output, $endIndex, 4, ''); substr($output, $tagIndex, length($tag), '
'); }; }; # If the completely expanded menu is too long, collapse all the groups that aren't in the selection hierarchy or near the # selection. By doing this instead of having them default to closed via CSS, any browser that doesn't support changing this at # runtime will keep the menu entirely open so that its still usable. if ($menuLength > MENU_LENGTH_LIMIT()) { my $menuSelectionHierarchy = $self->GetMenuSelectionHierarchy($sourceFile, $indexType); my $toExpand = $self->ExpandMenu($sourceFile, $indexType, $menuSelectionHierarchy, $menuRootLength); $output .= ''; }; # Comment needed for UpdateFile(). $output .= ''; $menuNumbersAndLengthsDone = 1; return $output; }; # # Function: BuildMenuSegment # # A recursive function to build a segment of the menu. *Remember to reset the before calling this # for the first time.* # # Parameters: # # outputDirectory - The output directory the menu is being built for. # isFramed - Whether the menu will be in a HTML frame or not. Assumes that if it is, the HTML tag will be set so that # links are directed to the proper frame. # menuSegment - An arrayref specifying the segment of the menu to build. Either pass the menu itself or the contents # of a group. # # Returns: # # The array ( menuHTML, length ). # # menuHTML - The menu segment in HTML. # groupLength - The length of the group, *not* including the contents of any subgroups, as computed from the # . # sub BuildMenuSegment #(outputDirectory, isFramed, menuSegment) { my ($self, $outputDirectory, $isFramed, $menuSegment) = @_; my ($output, $groupLength); foreach my $entry (@$menuSegment) { if ($entry->Type() == ::MENU_GROUP()) { my ($entryOutput, $entryLength) = $self->BuildMenuSegment($outputDirectory, $isFramed, $entry->GroupContent()); my $entryNumber; if (!$menuNumbersAndLengthsDone) { $entryNumber = $menuGroupNumber; $menuGroupNumber++; $menuGroupLengths{$entry} = $entryLength; $menuGroupNumbers{$entry} = $entryNumber; } else { $entryNumber = $menuGroupNumbers{$entry}; }; $output .= '
' . '
' . '' . $self->StringToHTML($entry->Title()) . '' . '
' . $entryOutput . '
' . '
' . '
'; $groupLength += MENU_GROUP_LENGTH; } elsif ($entry->Type() == ::MENU_FILE()) { my $targetOutputFile = $self->OutputFileOf($entry->Target()); # Dependency: BuildMenu() depends on how this formats file entries. $output .= ''; $groupLength += MENU_FILE_LENGTH; } elsif ($entry->Type() == ::MENU_TEXT()) { $output .= '
' . '
' . $self->StringToHTML( $entry->Title() ) . '
' . '
'; $groupLength += MENU_TEXT_LENGTH; } elsif ($entry->Type() == ::MENU_LINK()) { $output .= ''; $groupLength += MENU_LINK_LENGTH; } elsif ($entry->Type() == ::MENU_INDEX()) { my $indexFile = $self->IndexFileOf($entry->Target); # Dependency: BuildMenu() depends on how this formats index entries. $output .= ''; $groupLength += MENU_INDEX_LENGTH; }; }; if (!$menuNumbersAndLengthsDone) { $menuLength += $groupLength; }; return ($output, $groupLength); }; # # Function: BuildContent # # Builds and returns the main page content. # # Parameters: # # sourceFile - The source . # parsedFile - The parsed source file as an arrayref of objects. # # Returns: # # The page content in HTML. # sub BuildContent #(sourceFile, parsedFile) { my ($self, $sourceFile, $parsedFile) = @_; $self->ResetToolTips(); my $output; my $i = 0; while ($i < scalar @$parsedFile) { my $anchor = $self->SymbolToHTMLSymbol($parsedFile->[$i]->Symbol()); my $scope = NaturalDocs::Topics->TypeInfo($parsedFile->[$i]->Type())->Scope(); # The anchors are closed, but not around the text, so the :hover CSS style won't accidentally kick in. my $headerType; if ($i == 0) { $headerType = 'h1'; } elsif ($scope == ::SCOPE_START() || $scope == ::SCOPE_END()) { $headerType = 'h2'; } else { $headerType = 'h3'; }; $output .= '
NameOfType($parsedFile->[$i]->Type(), 0, 1) . ($i == 0 ? ' id=MainTopic' : '') . '>' . '
' . '<' . $headerType . ' class=CTitle>' . '' . $self->StringToHTML( $parsedFile->[$i]->Title(), ADD_HIDDEN_BREAKS) . ''; my $hierarchy; if (NaturalDocs::Topics->TypeInfo( $parsedFile->[$i]->Type() )->ClassHierarchy()) { $hierarchy = $self->BuildClassHierarchy($sourceFile, $parsedFile->[$i]->Symbol()); }; my $summary; if ($i == 0 || $scope == ::SCOPE_START() || $scope == ::SCOPE_END()) { $summary .= $self->BuildSummary($sourceFile, $parsedFile, $i); }; my $hasBody; if (defined $hierarchy || defined $summary || defined $parsedFile->[$i]->Prototype() || defined $parsedFile->[$i]->Body()) { $output .= '
'; $hasBody = 1; }; $output .= $hierarchy; if (defined $parsedFile->[$i]->Prototype()) { $output .= $self->BuildPrototype($parsedFile->[$i]->Type(), $parsedFile->[$i]->Prototype(), $sourceFile); }; if (defined $parsedFile->[$i]->Body()) { $output .= $self->NDMarkupToHTML( $sourceFile, $parsedFile->[$i]->Body(), $parsedFile->[$i]->Symbol(), $parsedFile->[$i]->Package(), $parsedFile->[$i]->Type(), $parsedFile->[$i]->Using() ); }; $output .= $summary; if ($hasBody) { $output .= '
'; }; $output .= '
' # CTopic . '
' # CType . "\n\n"; $i++; }; return $output; }; # # Function: BuildSummary # # Builds a summary, either for the entire file or the current class/section. # # Parameters: # # sourceFile - The source the summary appears in. # # parsedFile - A reference to the parsed source file. # # index - The index into the parsed file to start at. If undef or zero, it builds a summary for the entire file. If it's the # index of a that starts or ends a scope, it builds a summary for that scope # # Returns: # # The summary in HTML. # sub BuildSummary #(sourceFile, parsedFile, index) { my ($self, $sourceFile, $parsedFile, $index) = @_; my $completeSummary; if (!defined $index || $index == 0) { $index = 0; $completeSummary = 1; } else { # Skip the scope entry. $index++; }; if ($index + 1 >= scalar @$parsedFile) { return undef; }; my $scope = NaturalDocs::Topics->TypeInfo($parsedFile->[$index]->Type())->Scope(); # Return nothing if there's only one entry. if (!$completeSummary && ($scope == ::SCOPE_START() || $scope == ::SCOPE_END()) ) { return undef; }; my $indent = 0; my $inGroup; # In a nice efficiency twist, these buggers will hold the opening div tags if true, undef if false. Not that this script is known # for its efficiency. Not that Perl is known for its efficiency. Anyway... my $isMarkedAttr; my $entrySizeAttr = ' class=SEntrySize'; my $descriptionSizeAttr = ' class=SDescriptionSize'; my $output = '' . '
Summary
' # Not all browsers get table padding right, so we need a div to apply the border. . '
' . ''; while ($index < scalar @$parsedFile) { my $topic = $parsedFile->[$index]; my $scope = NaturalDocs::Topics->TypeInfo($topic->Type())->Scope(); if (!$completeSummary && ($scope == ::SCOPE_START() || $scope == ::SCOPE_END()) ) { last; }; # Remove modifiers as appropriate for the current entry. if ($scope == ::SCOPE_START() || $scope == ::SCOPE_END()) { $indent = 0; $inGroup = 0; $isMarkedAttr = undef; } elsif ($topic->Type() eq ::TOPIC_GROUP()) { if ($inGroup) { $indent--; }; $inGroup = 0; $isMarkedAttr = undef; }; $output .= '' . '
NameOfType($topic->Type(), 0, 1)) . '>' . '
'; # Add any remaining modifiers to the HTML in the form of div tags. This modifier approach isn't the most elegant # thing, but there's not a lot of options. It works. if ($indent) { $output .= '
'; }; # Add the entry itself. my $toolTipProperties; # We only want a tooltip here if there's a protoype. Otherwise it's redundant. if (defined $topic->Prototype()) { my $tooltipID = $self->BuildToolTip($topic->Symbol(), $sourceFile, $topic->Type(), $topic->Prototype(), $topic->Summary()); $toolTipProperties = $self->BuildToolTipLinkProperties($tooltipID); }; $output .= '' . $self->StringToHTML( $parsedFile->[$index]->Title(), ADD_HIDDEN_BREAKS) . ''; # Close the modifiers. if ($indent) { $output .= '
'; }; $output .= '
' # Entry . '
' # Type . '' . '
NameOfType($topic->Type(), 0, 1)) . '>' . '
'; # Add the modifiers to the HTML yet again. if ($indent) { $output .= '
'; }; if (defined $parsedFile->[$index]->Body()) { $output .= $self->NDMarkupToHTML($sourceFile, $parsedFile->[$index]->Summary(), $parsedFile->[$index]->Symbol(), $parsedFile->[$index]->Package(), $parsedFile->[$index]->Type(), $parsedFile->[$index]->Using()); }; # Close the modifiers again. if ($indent) { $output .= '
'; }; $output .= '
' # Description . '
' # Type . ''; # Prepare the modifiers for the next entry. if ($scope == ::SCOPE_START() || $scope == ::SCOPE_END()) { $indent = 1; $inGroup = 0; } elsif ($topic->Type() eq ::TOPIC_GROUP()) { if (!$inGroup) { $indent++; $inGroup = 1; }; }; if (!defined $isMarkedAttr) { $isMarkedAttr = ' class=SMarked'; } else { $isMarkedAttr = undef; }; $entrySizeAttr = undef; $descriptionSizeAttr = undef; $index++; }; $output .= '
' . '
' # Border . '
' # Summary . ""; return $output; }; # # Function: BuildPrototype # # Builds and returns the prototype as HTML. # # Parameters: # # type - The the prototype is from. # prototype - The prototype to format. # file - The the prototype was defined in. # # Returns: # # The prototype in HTML. # sub BuildPrototype #(type, prototype, file) { my ($self, $type, $prototype, $file) = @_; my $language = NaturalDocs::Languages->LanguageOf($file); my $prototypeObject = $language->ParsePrototype($type, $prototype); my $output; if ($prototypeObject->OnlyBeforeParameters()) { $output = # A blockquote to scroll it if it's too long. '
' # A surrounding table as a hack to make the div form-fit. . '
' . $self->ConvertAmpChars($prototypeObject->BeforeParameters()) . '
' . '
'; } else { my $params = $prototypeObject->Parameters(); my $beforeParams = $prototypeObject->BeforeParameters(); my $afterParams = $prototypeObject->AfterParameters(); # Determine what features the prototype has and its length. my ($hasType, $hasTypePrefix, $hasNamePrefix, $hasDefaultValue, $hasDefaultValuePrefix); my $maxParamLength = 0; foreach my $param (@$params) { my $paramLength = length($param->Name()); if ($param->Type()) { $hasType = 1; $paramLength += length($param->Type()) + 1; }; if ($param->TypePrefix()) { $hasTypePrefix = 1; $paramLength += length($param->TypePrefix()) + 1; }; if ($param->NamePrefix()) { $hasNamePrefix = 1; $paramLength += length($param->NamePrefix()); }; if ($param->DefaultValue()) { $hasDefaultValue = 1; # The length of the default value part is either the longest word, or 1/3 the total, whichever is longer. We do this # because we don't want parameter lines wrapping to more than three lines, and there's no guarantee that the line will # wrap at all. There's a small possibility that it could still wrap to four lines with this code, but we don't need to go # crazy(er) here. my $thirdLength = length($param->DefaultValue()) / 3; my @words = split(/ +/, $param->DefaultValue()); my $maxWordLength = 0; foreach my $word (@words) { if (length($word) > $maxWordLength) { $maxWordLength = length($word); }; }; $paramLength += ($maxWordLength > $thirdLength ? $maxWordLength : $thirdLength) + 1; }; if ($param->DefaultValuePrefix()) { $hasDefaultValuePrefix = 1; $paramLength += length($param->DefaultValuePrefix()) + 1; }; if ($paramLength > $maxParamLength) { $maxParamLength = $paramLength; }; }; my $useCondensed = (length($beforeParams) + $maxParamLength + length($afterParams) > 80 ? 1 : 0); my $parameterColumns = 1 + $hasType + $hasTypePrefix + $hasNamePrefix + $hasDefaultValue + $hasDefaultValuePrefix + $useCondensed; $output = '
' # Stupid hack to get it to work right in IE. . '' . ''; for (my $i = 0; $i < scalar @$params; $i++) { if ($useCondensed) { $output .= ''; } elsif ($i > 0) { # Go to the next row and and skip the BeforeParameters cell. $output .= ''; }; if ($language->TypeBeforeParameter()) { if ($hasTypePrefix) { my $htmlTypePrefix = $self->ConvertAmpChars($params->[$i]->TypePrefix()); $htmlTypePrefix =~ s/ $/ /; $output .= ''; }; if ($hasType) { $output .= ''; }; if ($hasNamePrefix) { $output .= ''; }; $output .= ''; } else # !$language->TypeBeforeParameter() { $output .= ''; if ($hasType || $hasTypePrefix) { my $typePrefix = $params->[$i]->TypePrefix(); if ($typePrefix) { $typePrefix .= ' '; }; $output .= ''; }; }; if ($hasDefaultValuePrefix) { $output .= ''; }; if ($hasDefaultValue) { $output .= ''; }; }; if ($useCondensed) { $output .= ''; }; $output .= '' . '
' . $self->ConvertAmpChars($beforeParams); if ($beforeParams && $beforeParams !~ /[\(\[\{\<]$/) { $output .= ' '; }; $output .= '
   
' . $htmlTypePrefix . '' . $self->ConvertAmpChars($params->[$i]->Type()) . ' ' . '' . $self->ConvertAmpChars($params->[$i]->NamePrefix()) . '' . $self->ConvertAmpChars($params->[$i]->Name()) . '' . $self->ConvertAmpChars( $params->[$i]->NamePrefix() . $params->[$i]->Name() ) . '' . ' ' . $self->ConvertAmpChars( $typePrefix . $params->[$i]->Type() ) . '' . ' ' . $self->ConvertAmpChars( $params->[$i]->DefaultValuePrefix() ) . ' ' . '' . ($hasDefaultValuePrefix ? '' : ' ') . $self->ConvertAmpChars( $params->[$i]->DefaultValue() ) . '
' . $self->ConvertAmpChars($afterParams); if ($afterParams && $afterParams !~ /^[\)\]\}\>]/) { $output .= ' '; }; $output .= '
' # Hack. . '
'; }; return $output; }; # # Function: BuildFooter # # Builds and returns the HTML footer for the page. # sub BuildFooter { my $self = shift; my $footer = NaturalDocs::Menu->Footer(); if (defined $footer) { if (substr($footer, -1, 1) ne '.') { $footer .= '.'; }; $footer =~ s/\(c\)/©/gi; $footer =~ s/\(tm\)/™/gi; $footer =~ s/\(r\)/®/gi; $footer .= '  Generated by Natural Docs.' } else { $footer = 'Generated by Natural Docs'; }; return '' . $footer . ''; }; # # Function: BuildToolTip # # Builds the HTML for a symbol's tooltip and stores it in . # # Parameters: # # symbol - The target . # file - The the target's defined in. # type - The symbol . # prototype - The target prototype, or undef for none. # summary - The target summary, or undef for none. # # Returns: # # If a tooltip is necessary for the link, returns the tooltip ID. If not, returns undef. # sub BuildToolTip #(symbol, file, type, prototype, summary) { my ($self, $symbol, $file, $type, $prototype, $summary) = @_; if (defined $prototype || defined $summary) { my $htmlSymbol = $self->SymbolToHTMLSymbol($symbol); my $number = $tooltipSymbolsToNumbers{$htmlSymbol}; if (!defined $number) { $number = $tooltipNumber; $tooltipNumber++; $tooltipSymbolsToNumbers{$htmlSymbol} = $number; $tooltipHTML .= '
' . '
NameOfType($type, 0, 1) . '>'; if (defined $prototype) { $tooltipHTML .= $self->BuildPrototype($type, $prototype, $file); }; if (defined $summary) { # Remove links, since people can't/shouldn't be clicking on tooltips anyway. $summary =~ s/<\/?(?:link|url)>//g; # The fact that we don't have scope or using shouldn't matter because we removed the links. $summary = $self->NDMarkupToHTML($file, $summary, undef, undef, $type, undef); # XXX - Hack. We want to remove e-mail links as well, but keep their obfuscation. So we leave the tags in there for # the NDMarkupToHTML call, then strip out the link part afterwards. The text obfuscation should still be in place. $summary =~ s/<\/?a[^>]+>//g; $tooltipHTML .= $summary; }; $tooltipHTML .= '
' . '
'; }; return 'tt' . $number; } else { return undef; }; }; # # Function: BuildToolTips # # Builds and returns the tooltips for the page in HTML. # sub BuildToolTips { my $self = shift; return "\n\n" . $tooltipHTML . "\n\n"; }; # # Function: BuildClassHierarchy # # Builds and returns a class hierarchy diagram for the passed class, if applicable. # # Parameters: # # file - The source . # class - The class to build the hierarchy of. # sub BuildClassHierarchy #(file, symbol) { my ($self, $file, $symbol) = @_; my @parents = NaturalDocs::ClassHierarchy->ParentsOf($symbol); @parents = sort { ::StringCompare($a, $b) } @parents; my @children = NaturalDocs::ClassHierarchy->ChildrenOf($symbol); @children = sort { ::StringCompare($a, $b) } @children; if (!scalar @parents && !scalar @children) { return undef; }; my $output = '
'; if (scalar @parents) { $output .='
'; foreach my $parent (@parents) { $output .= $self->BuildClassHierarchyEntry($file, $parent, 'CHParent', 1); }; $output .= '
'; }; $output .= '
' . $self->BuildClassHierarchyEntry($file, $symbol, 'CHCurrent', undef) . '
'; if (scalar @children) { $output .= '
' . '
'; if (scalar @children <= 5) { for (my $i = 0; $i < scalar @children; $i++) { $output .= $self->BuildClassHierarchyEntry($file, $children[$i], 'CHChild', 1); }; } else { for (my $i = 0; $i < 4; $i++) { $output .= $self->BuildClassHierarchyEntry($file, $children[$i], 'CHChild', 1); }; $output .= '
' . (scalar @children - 4) . ' other children
'; }; $output .= '
' . '
'; }; if (scalar @parents) { $output .= '
'; }; $output .= '
'; return $output; }; # # Function: BuildClassHierarchyEntry # # Builds and returns a single class hierarchy entry. # # Parameters: # # file - The source . # symbol - The class whose entry is getting built. # style - The style to apply to the entry, such as . # link - Whether to build a link for this class or not. When building the selected class' entry, this should be false. It will # automatically handle whether the symbol is defined or not. # sub BuildClassHierarchyEntry #(file, symbol, style, link) { my ($self, $file, $symbol, $style, $link) = @_; my @identifiers = NaturalDocs::SymbolString->IdentifiersOf($symbol); my $name = join(NaturalDocs::Languages->LanguageOf($file)->PackageSeparator(), @identifiers); $name = $self->StringToHTML($name); my $output = '
'; if ($link) { my $target = NaturalDocs::SymbolTable->Lookup($symbol, $file); if (defined $target) { my $targetFile; if ($target->File() ne $file) { $targetFile = $self->MakeRelativeURL( $self->OutputFileOf($file), $self->OutputFileOf($target->File()), 1 ); }; # else leave it undef my $targetTooltipID = $self->BuildToolTip($symbol, $target->File(), $target->Type(), $target->Prototype(), $target->Summary()); my $toolTipProperties = $self->BuildToolTipLinkProperties($targetTooltipID); $output .= 'NameOfType($target->Type(), 0, 1) . ' ' . $toolTipProperties . '>' . $name . ''; } else { $output .= $name; }; } else { $output .= $name; }; $output .= '
'; return $output; }; # # Function: OpeningBrowserStyles # # Returns the JavaScript that will add opening browser styles if necessary. # sub OpeningBrowserStyles { my $self = shift; return ''; }; # # Function: ClosingBrowserStyles # # Returns the JavaScript that will close browser styles if necessary. # sub ClosingBrowserStyles { my $self = shift; return ''; }; # # Function: StandardComments # # Returns the standard HTML comments that should be included in every generated file. This includes , so this # really is required for proper functionality. # sub StandardComments { my $self = shift; return "\n\n" . '' . "\n" . '' . "\n\n" . $self->IEWebMark() . "\n\n"; }; # # Function: IEWebMark # # Returns the HTML comment necessary to get around the security warnings in IE starting with Windows XP Service Pack 2. # # With this mark, the HTML page is treated as if it were in the Internet security zone instead of the Local Machine zone. This # prevents the lockdown on scripting that causes an error message to appear with each page. # # More Information: # # - http://www.microsoft.com/technet/prodtechnol/winxppro/maintain/sp2brows.mspx#EHAA # - http://www.phdcc.com/xpsp2.htm#markoftheweb # sub IEWebMark { my $self = shift; return ''; }; ############################################################################### # Group: Index Functions # # Function: BuildIndexPages # # Builds an index file or files. # # Parameters: # # type - The the index is limited to, or undef for none. # index - An arrayref of sections, each section being an arrayref objects. # The first section is for symbols, the second for numbers, and the rest for A through Z. # beginPage - All the content of the HTML page up to where the index content should appear. # endPage - All the content of the HTML page past where the index should appear. # # Returns: # # The number of pages in the index. # sub BuildIndexPages #(type, index, beginPage, endPage) { my ($self, $type, $indexSections, $beginPage, $endPage) = @_; # Build the content. my ($indexHTMLSections, $tooltipHTMLSections) = $self->BuildIndexSections($indexSections, $self->IndexFileOf($type, 1)); my $page = 1; my $pageSize = 0; my @pageLocations; # The maximum page size acceptable before starting a new page. Note that this doesn't include beginPage and endPage, # because we don't want something like a large menu screwing up the calculations. use constant PAGESIZE_LIMIT => 50000; # File the pages. for (my $i = 0; $i < scalar @$indexHTMLSections; $i++) { if (!defined $indexHTMLSections->[$i]) { next; }; $pageSize += length($indexHTMLSections->[$i]) + length($tooltipHTMLSections->[$i]); $pageLocations[$i] = $page; if ($pageSize + length($indexHTMLSections->[$i+1]) + length($tooltipHTMLSections->[$i+1]) > PAGESIZE_LIMIT) { $page++; $pageSize = 0; }; }; # Build the pages. my $indexFileName; $page = -1; my $oldPage = -1; my $tooltips; my $firstHeading; for (my $i = 0; $i < scalar @$indexHTMLSections; $i++) { if (!defined $indexHTMLSections->[$i]) { next; }; $page = $pageLocations[$i]; # Switch files if we need to. if ($page != $oldPage) { if ($oldPage != -1) { print INDEXFILEHANDLE '' . $tooltips . $endPage; close(INDEXFILEHANDLE); $tooltips = undef; }; $indexFileName = $self->IndexFileOf($type, $page); open(INDEXFILEHANDLE, '>' . $indexFileName) or die "Couldn't create output file " . $indexFileName . ".\n"; print INDEXFILEHANDLE $beginPage . $self->BuildIndexNavigationBar($type, $page, \@pageLocations) . ''; $oldPage = $page; $firstHeading = 1; }; print INDEXFILEHANDLE '' . '' . '' . '' . $indexHTMLSections->[$i]; $firstHeading = 0; $tooltips .= $tooltipHTMLSections->[$i]; }; if ($page != -1) { print INDEXFILEHANDLE '
' . '' . $indexHeadings[$i] . '
' . $tooltips . $endPage; close(INDEXFILEHANDLE); } # Build a dummy page so there's something at least. else { $indexFileName = $self->IndexFileOf($type, 1); open(INDEXFILEHANDLE, '>' . $indexFileName) or die "Couldn't create output file " . $indexFileName . ".\n"; print INDEXFILEHANDLE $beginPage . $self->BuildIndexNavigationBar($type, 1, \@pageLocations) . 'There are no entries in the ' . lc( NaturalDocs::Topics->NameOfType($type) ) . ' index.' . $endPage; close(INDEXFILEHANDLE); }; return $page; }; # # Function: BuildIndexSections # # Builds and returns index's sections in HTML. # # Parameters: # # index - An arrayref of sections, each section being an arrayref objects. # The first section is for symbols, the second for numbers, and the rest for A through Z. # outputFile - The output file the index is going to be stored in. Since there may be multiple files, just send the first file. The # path is what matters, not the file name. # # Returns: # # The arrayref ( indexSections, tooltipSections ). # # Index 0 is the symbols, index 1 is the numbers, and each following index is A through Z. The content of each section # is its HTML, or undef if there is nothing for that section. # sub BuildIndexSections #(index, outputFile) { my ($self, $indexSections, $outputFile) = @_; $self->ResetToolTips(); my $contentSections = [ ]; my $tooltipSections = [ ]; for (my $section = 0; $section < scalar @$indexSections; $section++) { if (defined $indexSections->[$section]) { my $total = scalar @{$indexSections->[$section]}; for (my $i = 0; $i < $total; $i++) { my $id; if ($i == 0) { if ($total == 1) { $id = 'IOnlySymbolPrefix'; } else { $id = 'IFirstSymbolPrefix'; }; } elsif ($i == $total - 1) { $id = 'ILastSymbolPrefix'; }; $contentSections->[$section] .= $self->BuildIndexElement($indexSections->[$section]->[$i], $outputFile, $id); }; $tooltipSections->[$section] .= $self->BuildToolTips(); $self->ResetToolTips(1); }; }; return ( $contentSections, $tooltipSections ); }; # # Function: BuildIndexElement # # Converts a to HTML and returns it. It will handle all sub-elements automatically. # # Parameters: # # element - The to build. # outputFile - The output this is appearing in. # id - The CSS ID to apply to the prefix. # # Recursion-Only Parameters: # # These parameters are used internally for recursion, and should not be set. # # symbol - If the element is below symbol level, the to use. # package - If the element is below package level, the package to use. # hasPackage - Whether the element is below package level. Is necessary because package may need to be undef. # sub BuildIndexElement #(element, outputFile, id, symbol, package, hasPackage) { my ($self, $element, $outputFile, $id, $symbol, $package, $hasPackage) = @_; my $output; # If we're doing a file sub-index entry... if ($hasPackage) { my ($inputDirectory, $relativePath) = NaturalDocs::Settings->SplitFromInputDirectory($element->File()); $output = $self->BuildIndexLink($self->StringToHTML($relativePath, ADD_HIDDEN_BREAKS), $symbol, $package, $element->File(), $element->Type(), $element->Prototype(), $element->Summary(), $outputFile, 'IFile') } # If we're doing a package sub-index entry... elsif (defined $symbol) { my $text; if ($element->Package()) { $text = NaturalDocs::SymbolString->ToText($element->Package(), $element->PackageSeparator()); $text = $self->StringToHTML($text, ADD_HIDDEN_BREAKS); } else { $text = 'Global'; }; if (!$element->HasMultipleFiles()) { $output .= $self->BuildIndexLink($text, $symbol, $element->Package(), $element->File(), $element->Type(), $element->Prototype(), $element->Summary(), $outputFile, 'IParent'); } else { $output .= '' . $text . '' . '
'; my $fileElements = $element->File(); foreach my $fileElement (@$fileElements) { $output .= $self->BuildIndexElement($fileElement, $outputFile, $id, $symbol, $element->Package(), 1); }; $output .= '
'; }; } # If we're doing a top-level symbol entry... else { my $symbolText = $self->StringToHTML($element->SortableSymbol(), ADD_HIDDEN_BREAKS); my $symbolPrefix = $self->StringToHTML($element->IgnoredPrefix()); $output .= '' . '' . ($symbolPrefix || ' ') . ''; if (!$element->HasMultiplePackages()) { my $packageText; if (defined $element->Package()) { $packageText = NaturalDocs::SymbolString->ToText($element->Package(), $element->PackageSeparator()); $packageText = $self->StringToHTML($packageText, ADD_HIDDEN_BREAKS); }; if (!$element->HasMultipleFiles()) { $output .= $self->BuildIndexLink($symbolText, $element->Symbol(), $element->Package(), $element->File(), $element->Type(), $element->Prototype(), $element->Summary(), $outputFile, 'ISymbol'); if (defined $packageText) { $output .= ', ' . $packageText . ''; }; } else # hasMultipleFiles but not mulitplePackages { $output .= '' . $symbolText . ''; if (defined $packageText) { $output .= ', ' . $packageText . ''; }; $output .= '
'; my $fileElements = $element->File(); foreach my $fileElement (@$fileElements) { $output .= $self->BuildIndexElement($fileElement, $outputFile, $id, $element->Symbol(), $element->Package(), 1); }; $output .= '
'; }; } else # hasMultiplePackages { $output .= '' . $symbolText . '' . '
'; my $packageElements = $element->Package(); foreach my $packageElement (@$packageElements) { $output .= $self->BuildIndexElement($packageElement, $outputFile, $id, $element->Symbol()); }; $output .= '
'; }; $output .= ''; }; return $output; }; # # Function: BuildIndexLink # # Builds and returns the HTML associated with an index link. The HTML will be the a href tag, the text, and the closing tag. # # Parameters: # # text - The text of the link *in HTML*. Use if necessary. # symbol - The partial to link to. # package - The package of the symbol. # file - The the symbol is defined in. # type - The of the symbol. # prototype - The prototype of the symbol, or undef if none. # summary - The summary of the symbol, or undef if none. # outputFile - The HTML this link will appear in. # style - The CSS style to apply to the link. # sub BuildIndexLink #(text, symbol, package, file, type, prototype, summary, outputFile, style) { my ($self, $text, $symbol, $package, $file, $type, $prototype, $summary, $outputFile, $style) = @_; $symbol = NaturalDocs::SymbolString->Join($package, $symbol); my $targetTooltipID = $self->BuildToolTip($symbol, $file, $type, $prototype, $summary); my $toolTipProperties = $self->BuildToolTipLinkProperties($targetTooltipID); return '' . $text . ''; }; # # Function: BuildIndexNavigationBar # # Builds a navigation bar for a page of the index. # # Parameters: # # type - The of the index, or undef for general. # page - The page of the index the navigation bar is for. # locations - An arrayref of the locations of each section. Index 0 is for the symbols, index 1 for the numbers, and the rest # for each letter. The values are the page numbers where the sections are located. # sub BuildIndexNavigationBar #(type, page, locations) { my ($self, $type, $page, $locations) = @_; my $output = '
'; for (my $i = 0; $i < scalar @indexHeadings; $i++) { if ($i != 0) { $output .= ' · '; }; if (defined $locations->[$i]) { $output .= '' . $indexHeadings[$i] . ''; } else { $output .= $indexHeadings[$i]; }; }; $output .= '
'; return $output; }; ############################################################################### # Group: File Functions # # function: PurgeIndexFiles # # Removes all or some of the output files for an index. # # Parameters: # # type - The index . # startingPage - If defined, only pages starting with this number will be removed. Otherwise all pages will be removed. # sub PurgeIndexFiles #(type, startingPage) { my ($self, $type, $page) = @_; if (!defined $page) { $page = 1; }; for (;;) { my $file = $self->IndexFileOf($type, $page); if (-e $file) { unlink($file); $page++; } else { last; }; }; }; # # function: OutputFileOf # # Returns the output file name of the source file. Will be undef if it is not a file from a valid input directory. # sub OutputFileOf #(sourceFile) { my ($self, $sourceFile) = @_; my ($inputDirectory, $relativeSourceFile) = NaturalDocs::Settings->SplitFromInputDirectory($sourceFile); if (!defined $inputDirectory) { return undef; }; my $outputDirectory = NaturalDocs::Settings->OutputDirectoryOf($self); my $inputDirectoryName = NaturalDocs::Settings->InputDirectoryNameOf($inputDirectory); $outputDirectory = NaturalDocs::File->JoinPaths( $outputDirectory, 'files' . ($inputDirectoryName != 1 ? $inputDirectoryName : ''), 1 ); # We need to change any extensions to dashes because Apache will think file.pl.html is a script. # We also need to add a dash if the file doesn't have an extension so there'd be no conflicts with index.html, # FunctionIndex.html, etc. if (!($relativeSourceFile =~ tr/./-/)) { $relativeSourceFile .= '-'; }; $relativeSourceFile =~ tr/ /_/; $relativeSourceFile .= '.html'; return NaturalDocs::File->JoinPaths($outputDirectory, $relativeSourceFile); }; # # Function: IndexDirectory # # Returns the directory of the index files. # sub IndexDirectory { my $self = shift; return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->OutputDirectoryOf($self), 'index', 1); }; # # function: IndexFileOf # # Returns the output file name of the index file. # # Parameters: # # type - The of the index. # page - The page number. Undef is the same as one. # sub IndexFileOf #(type, page) { my ($self, $type, $page) = @_; return NaturalDocs::File->JoinPaths( $self->IndexDirectory(), $self->RelativeIndexFileOf($type, $page) ); }; # # function: RelativeIndexFileOf # # Returns the output file name of the index file, relative to other index files. # # Parameters: # # type - The of the index. # page - The page number. Undef is the same as one. # sub RelativeIndexFileOf #(type, page) { my ($self, $type, $page) = @_; return NaturalDocs::Topics->NameOfType($type, 1, 1) . (defined $page && $page != 1 ? $page : '') . '.html'; }; # # function: CSSDirectory # # Returns the directory of the CSS files. # sub CSSDirectory { my $self = shift; return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->OutputDirectoryOf($self), 'styles', 1); }; # # Function: MainCSSFile # # Returns the location of the main CSS file. # sub MainCSSFile { my $self = shift; return NaturalDocs::File->JoinPaths( $self->CSSDirectory(), 'main.css' ); }; # # function: JavaScriptDirectory # # Returns the directory of the JavaScript files. # sub JavaScriptDirectory { my $self = shift; return NaturalDocs::File->JoinPaths( NaturalDocs::Settings->OutputDirectoryOf($self), 'javascript', 1); }; # # Function: MainJavaScriptFile # # Returns the location of the main JavaScript file. # sub MainJavaScriptFile { my $self = shift; return NaturalDocs::File->JoinPaths( $self->JavaScriptDirectory(), 'main.js' ); }; ############################################################################### # Group: Support Functions # # function:IndexTitleOf # # Returns the page title of the index file. # # Parameters: # # type - The type of index. # sub IndexTitleOf #(type) { my ($self, $type) = @_; return ($type eq ::TOPIC_GENERAL() ? '' : NaturalDocs::Topics->NameOfType($type) . ' ') . 'Index'; }; # # function: MakeRelativeURL # # Returns a relative path between two files in the output tree and returns it in URL format. # # Parameters: # # baseFile - The base in local format, *not* in URL format. # targetFile - The target of the link in local format, *not* in URL format. # baseHasFileName - Whether baseFile has a file name attached or is just a path. # # Returns: # # The relative URL to the target. # sub MakeRelativeURL #(FileName baseFile, FileName targetFile, bool baseHasFileName) -> string relativeURL { my ($self, $baseFile, $targetFile, $baseHasFileName) = @_; if ($baseHasFileName) { $baseFile = NaturalDocs::File->NoFileName($baseFile) }; my $relativePath = NaturalDocs::File->MakeRelativePath($baseFile, $targetFile); return $self->ConvertAmpChars( NaturalDocs::File->ConvertToURL($relativePath) ); }; # # Function: StringToHTML # # Converts a text string to HTML. Does not apply paragraph tags or accept formatting tags. # # Parameters: # # string - The string to convert. # addHiddenBreaks - Whether to add hidden breaks to the string. You can use for this parameter # if you want to make the calling code clearer. # # Returns: # # The string in HTML. # sub StringToHTML #(string, addHiddenBreaks) { my ($self, $string, $addHiddenBreaks) = @_; $string =~ s/&/&/g; $string =~ s//>/g; # Me likey the fancy quotes. They work in IE 4+, Mozilla, and Opera 5+. We've already abandoned NS4 with the CSS # styles, so might as well. $string =~ s/^\'/‘/gm; $string =~ s/([\ \(\[\{])\'/$1‘/g; $string =~ s/\'/’/g; $string =~ s/^\"/“/gm; $string =~ s/([\ \(\[\{])\"/$1“/g; $string =~ s/\"/”/g; # Me likey the double spaces too. As you can probably tell, I like print-formatting better than web-formatting. The indented # paragraphs without blank lines in between them do become readable when you have fancy quotes and double spaces too. $string = $self->AddDoubleSpaces($string); if ($addHiddenBreaks) { $string = $self->AddHiddenBreaks($string); }; return $string; }; # # Function: SymbolToHTMLSymbol # # Converts a to a HTML symbol, meaning one that is safe to include in anchor and link tags. You don't need # to pass the result to . # sub SymbolToHTMLSymbol #(symbol) { my ($self, $symbol) = @_; my @identifiers = NaturalDocs::SymbolString->IdentifiersOf($symbol); my $htmlSymbol = join('.', @identifiers); # If only Mozilla was nice about putting special characters in URLs like IE and Opera are, I could leave spaces in and replace # "<>& with their amp chars. But alas, Mozilla shows them as %20, etc. instead. It would have made for nice looking URLs. $htmlSymbol =~ tr/ \"<>\?&%/_/d; return $htmlSymbol; }; # # Function: NDMarkupToHTML # # Converts a block of to HTML. # # Parameters: # # sourceFile - The source the appears in. # text - The text to convert. # symbol - The topic the appears in. # package - The package the appears in. # type - The the appears in. # using - An arrayref of scope the also has access to, or undef if none. # # Returns: # # The text in HTML. # sub NDMarkupToHTML #(sourceFile, text, symbol, package, type, using) { my ($self, $sourceFile, $text, $symbol, $package, $type, $using) = @_; my $dlSymbolBehavior; if ($type == ::TOPIC_ENUMERATION()) { $dlSymbolBehavior = NaturalDocs::Languages->LanguageOf($sourceFile)->EnumValues(); } elsif (NaturalDocs::Topics->TypeInfo($type)->Scope() == ::SCOPE_ALWAYS_GLOBAL()) { $dlSymbolBehavior = ::ENUM_GLOBAL(); } else { $dlSymbolBehavior = ::ENUM_UNDER_PARENT(); }; my $output; my $inCode; my @splitText = split(/(<\/?code>)/, $text); while (scalar @splitText) { $text = shift @splitText; if ($text eq '') { $output .= '
';
            $inCode = 1;
            }
        elsif ($text eq '')
            {
            $output .= '
'; $inCode = undef; } elsif ($inCode) { $text =~ s/\n/
/g; $output .= $text; } else { # Format non-code text. # Convert quotes to fancy quotes. $text =~ s/^\'/‘/gm; $text =~ s/([\ \(\[\{])\'/$1‘/g; $text =~ s/\'/’/g; $text =~ s/^"/“/gm; $text =~ s/([\ \(\[\{])"/$1“/g; $text =~ s/"/”/g; # Copyright symbols. Prevent conversion when part of (a), (b), (c) lists. if ($text !~ /\(a\)/i) { $text =~ s/\(c\)/©/gi; }; # Trademark symbols. $text =~ s/\(tm\)/™/gi; $text =~ s/\(r\)/®/gi; # Resolve and convert links. $text =~ s/([^<]+)<\/link>/$self->BuildTextLink($1, $package, $using, $sourceFile)/ge; $text =~ s/([^<]+)<\/url>/$self->BuildURLLink($1)/ge; $text =~ s/([^<]+)<\/email>/$self->BuildEMailLink($1)/eg; # Add double spaces too. $text = $self->AddDoubleSpaces($text); # Paragraphs $text =~ s/

/

/g; # Bulleted lists $text =~ s/

    /
      /g; # Headings $text =~ s//

      /g; $text =~ s/<\/h>/<\/h4>/g; # Description Lists $text =~ s/
      //g; $text =~ s/<\/dl>/<\/table>/g; $text =~ s//' . ''; }; $text =~ s/
      /
      /g; $text =~ s/<\/de>/<\/td>/g; if ($dlSymbolBehavior == ::ENUM_GLOBAL()) { $text =~ s/([^<]+)<\/ds>/$self->MakeDescriptionListSymbol(undef, $1)/ge; } elsif ($dlSymbolBehavior == ::ENUM_UNDER_PARENT()) { $text =~ s/([^<]+)<\/ds>/$self->MakeDescriptionListSymbol($package, $1)/ge; } else # ($dlSymbolBehavior == ::ENUM_UNDER_TYPE()) { $text =~ s/([^<]+)<\/ds>/$self->MakeDescriptionListSymbol($symbol, $1)/ge; } sub MakeDescriptionListSymbol #(package, text) { my ($self, $package, $text) = @_; $text = NaturalDocs::NDMarkup->RestoreAmpChars($text); my $symbol = NaturalDocs::SymbolString->FromText($text); if (defined $package) { $symbol = NaturalDocs::SymbolString->Join($package, $symbol); }; return '
      ' # The anchors are closed, but not around the text, to prevent the :hover CSS style from kicking in. . '' . $text . '/g; $text =~ s/<\/dd>/<\/td><\/tr>/g; $output .= $text; }; }; return $output; }; # # Function: BuildTextLink # # Creates a HTML link to a symbol, if it exists. # # Parameters: # # text - The link text # package - The package the link appears in, or undef if none. # using - An arrayref of additional scope the link has access to, or undef if none. # sourceFile - The the link appears in. # # Returns: # # The link in HTML, including tags. If the link doesn't resolve to anything, returns the HTML that should be substituted for it. # sub BuildTextLink #(text, package, using, sourceFile) { my ($self, $text, $package, $using, $sourceFile) = @_; my $plainText = $self->RestoreAmpChars($text); my $symbol = NaturalDocs::SymbolString->FromText($plainText); my $target = NaturalDocs::SymbolTable->References(::REFERENCE_TEXT(), $symbol, $package, $using, $sourceFile); if (defined $target) { my $targetFile; if ($target->File() ne $sourceFile) { $targetFile = $self->MakeRelativeURL( $self->OutputFileOf($sourceFile), $self->OutputFileOf($target->File()), 1 ); }; # else leave it undef my $targetTooltipID = $self->BuildToolTip($target->Symbol(), $sourceFile, $target->Type(), $target->Prototype(), $target->Summary()); my $toolTipProperties = $self->BuildToolTipLinkProperties($targetTooltipID); return 'NameOfType($target->Type(), 0, 1) . ' ' . $toolTipProperties . '>' . $text . ''; } else { return '<' . $text . '>'; }; }; # # Function: BuildURLLink # # Creates a HTML link to an external URL. Long URLs will have hidden breaks to allow them to wrap. # # Parameters: # # url - The URL to link to. # # Returns: # # The HTML link, complete with tags. # sub BuildURLLink #(url) { my ($self, $url) = @_; $url = $self->RestoreAmpChars($url); if (length $url < 50) { return '' . $self->ConvertAmpChars($url) . ''; }; my @segments = split(/([\,\&\/])/, $url); my $output = ''; # Get past the first batch of slashes, since we don't want to break on things like http://. $output .= $self->ConvertAmpChars($segments[0]); my $i = 1; while ($i < scalar @segments && ($segments[$i] eq '/' || !$segments[$i])) { $output .= $segments[$i]; $i++; }; # Now break on each one of those symbols. while ($i < scalar @segments) { # Spaces don't wrap in IE for some reason. Need to use dashes as well. if ($segments[$i] eq ',' || $segments[$i] eq '/' || $segments[$i] eq '&') { $output .= '- '; }; $output .= $self->ConvertAmpChars($segments[$i]); $i++; }; $output .= ''; return $output; }; # # Function: BuildEMailLink # # Creates a HTML link to an e-mail address. The address will be transparently munged to protect it (hopefully) from spambots. # # Parameters: # # address - The e-mail address. # # Returns: # # The HTML e-mail link, complete with tags. # sub BuildEMailLink #(address) { my ($self, $address) = @_; my @splitAddress; # Hack the address up. We want two user pieces and two host pieces. my ($user, $host) = split(/\@/, $address); my $userSplit = length($user) / 2; push @splitAddress, substr($user, 0, $userSplit); push @splitAddress, substr($user, $userSplit); push @splitAddress, '@'; my $hostSplit = length($host) / 2; push @splitAddress, substr($host, 0, $hostSplit); push @splitAddress, substr($host, $hostSplit); # Now put it back together again. We'll use spans to split the text transparently and JavaScript to split and join the link. return "" . $splitAddress[0] . '.nosp@m.' . $splitAddress[1] . '@' . $splitAddress[3] . '.nosp@m.' . $splitAddress[4] . ''; }; # # Function: BuildToolTipLinkProperties # # Returns the properties that should go in the link tag to add a tooltip to it. Because the function accepts undef, you can # call it without checking if returned undef or not. # # Parameters: # # toolTipID - The ID of the tooltip. If undef, the function will return undef. # # Returns: # # The properties that should be put in the link tag, or undef if toolTipID wasn't specified. # sub BuildToolTipLinkProperties #(toolTipID) { my ($self, $toolTipID) = @_; if (defined $toolTipID) { my $currentNumber = $tooltipLinkNumber; $tooltipLinkNumber++; return 'id=link' . $currentNumber . ' ' . 'onMouseOver="ShowTip(event, \'' . $toolTipID . '\', \'link' . $currentNumber . '\')" ' . 'onMouseOut="HideTip(\'' . $toolTipID . '\')"'; } else { return undef; }; }; # # Function: AddDoubleSpaces # # Adds second spaces after the appropriate punctuation with   so they show up in HTML. They don't occur if there isn't at # least one space after the punctuation, so things like class.member notation won't be affected. # # Parameters: # # text - The text to convert. # # Returns: # # The text with double spaces as necessary. # sub AddDoubleSpaces #(text) { my ($self, $text) = @_; # Question marks and exclamation points get double spaces unless followed by a lowercase letter. $text =~ s/ ([^\ \t\r\n] [\!\?]) # Must appear after a non-whitespace character to apply. ("|&[lr][sd]quo;|[\'\"\]\}\)]?) # Tolerate closing quotes, parenthesis, etc. ((?:<[^>]+>)*) # Tolerate tags \ # The space (?![a-z]) # Not followed by a lowercase character. /$1$2$3 \ /gx; # Periods get double spaces if it's not followed by a lowercase letter. However, if it's followed by a capital letter and the # preceding word is in the list of acceptable abbreviations, it won't get the double space. Yes, I do realize I am seriously # over-engineering this. $text =~ s/ ([^\ \t\r\n]+) # The word prior to the period. \. ("|&[lr][sd]quo;|[\'\"\]\}\)]?) # Tolerate closing quotes, parenthesis, etc. ((?:<[^>]+>)*) # Tolerate tags \ # The space ([^a-z]) # The next character, if it's not a lowercase letter. /$1 . '.' . $2 . $3 . MaybeExpand($1, $4) . $4/gex; sub MaybeExpand #(leadWord, nextLetter) { my ($leadWord, $nextLetter) = @_; if ($nextLetter =~ /^[A-Z]$/ && exists $abbreviations{ lc($leadWord) } ) { return ' '; } else { return '  '; }; }; return $text; }; # # Function: ConvertAmpChars # # Converts certain characters to their HTML amp char equivalents. # # Parameters: # # text - The text to convert. # # Returns: # # The converted text. # sub ConvertAmpChars #(text) { my ($self, $text) = @_; $text =~ s/&/&/g; $text =~ s/\"/"/g; $text =~ s//>/g; return $text; }; # # Function: RestoreAmpChars # # Restores all amp characters to their original state. This works with both amp chars and fancy quotes. # # Parameters: # # text - The text to convert. # # Returns: # # The converted text. # sub RestoreAmpChars #(text) { my ($self, $text) = @_; $text = NaturalDocs::NDMarkup->RestoreAmpChars($text); $text =~ s/&[lr]squo;/'/g; $text =~ s/&[lr]dquo;/"/g; return $text; }; # # Function: AddHiddenBreaks # # Adds hidden breaks to symbols. Puts them after symbol and directory separators so long names won't screw up the layout. # # Parameters: # # string - The string to break. # # Returns: # # The string with hidden breaks. # sub AddHiddenBreaks #(string) { my ($self, $string) = @_; # \.(?=.{5,}) instead of \. so file extensions don't get breaks. # :+ instead of :: because Mac paths are separated by a : and we want to get those too. $string =~ s/(\w(?:\.(?=.{5,})|:+|->|\\|\/))(\w)/$1 . ' <\/span>' . $2/ge; return $string; }; # # Function: FindFirstFile # # A function that finds and returns the first file entry in the menu, or undef if none. # sub FindFirstFile { # Hidden parameter: arrayref # Used for recursion only. my ($self, $arrayref) = @_; if (!defined $arrayref) { $arrayref = NaturalDocs::Menu->Content(); }; foreach my $entry (@$arrayref) { if ($entry->Type() == ::MENU_FILE()) { return $entry; } elsif ($entry->Type() == ::MENU_GROUP()) { my $result = $self->FindFirstFile($entry->GroupContent()); if (defined $result) { return $result; }; }; }; return undef; }; # # Function: ExpandMenu # # Determines which groups should be expanded. # # Parameters: # # sourceFile - The source to use if you're looking for a source file. # indexType - The index to use if you're looking for an index. # selectionHierarchy - The the menu is being built for. Does not have to be on the menu itself. # rootLength - The length of the menu's root group, *not* including the contents of subgroups. # # Returns: # # An arrayref of all the group numbers that should be expanded. At minimum, it will contain the numbers of the groups # present in , though it may contain more. # sub ExpandMenu #(FileName sourceFile, TopicType indexType, NaturalDocs::Menu::Entry[] selectionHierarchy, int rootLength) -> int[] groupsToExpand { my ($self, $sourceFile, $indexType, $menuSelectionHierarchy, $rootLength) = @_; my $toExpand = [ ]; # First expand everything in the selection hierarchy. my $length = $rootLength; foreach my $entry (@$menuSelectionHierarchy) { $length += $menuGroupLengths{$entry}; push @$toExpand, $menuGroupNumbers{$entry}; }; # Now do multiple passes of group expansion as necessary. We start from bottomIndex and expand outwards. We stop going # in a direction if a group there is too long -- we do not skip over it and check later groups as well. However, if one direction # stops, the other can keep going. my $pass = 1; my $hasSubGroups; while ($length < MENU_LENGTH_LIMIT) { my $content; my $topIndex; my $bottomIndex; if ($pass == 1) { # First pass, we expand the selection's siblings. if (scalar @$menuSelectionHierarchy) { $content = $menuSelectionHierarchy->[0]->GroupContent(); } else { $content = NaturalDocs::Menu->Content(); }; $bottomIndex = 0; while ($bottomIndex < scalar @$content && !($content->[$bottomIndex]->Type() == ::MENU_FILE() && $content->[$bottomIndex]->Target() eq $sourceFile) && !($content->[$bottomIndex]->Type() != ::MENU_INDEX() && $content->[$bottomIndex]->Target() eq $indexType) ) { $bottomIndex++; }; if ($bottomIndex == scalar @$content) { $bottomIndex = 0; }; $topIndex = $bottomIndex - 1; } elsif ($pass == 2) { # If the section we just expanded had no sub-groups, do another pass trying to expand the parent's sub-groups. The # net effect is that groups won't collapse as much unnecessarily. Someone can click on a file in a sub-group and the # groups in the parent will stay open. if (!$hasSubGroups && scalar @$menuSelectionHierarchy) { if (scalar @$menuSelectionHierarchy > 1) { $content = $menuSelectionHierarchy->[1]->GroupContent(); } else { $content = NaturalDocs::Menu->Content(); }; $bottomIndex = 0; while ($bottomIndex < scalar @$content && $content->[$bottomIndex] != $menuSelectionHierarchy->[0]) { $bottomIndex++; }; $topIndex = $bottomIndex - 1; $bottomIndex++; # Increment past our own group. $hasSubGroups = undef; } else { last; }; } # No more passes. else { last; }; while ( ($topIndex >= 0 || $bottomIndex < scalar @$content) && $length < MENU_LENGTH_LIMIT) { # We do the bottom first. while ($bottomIndex < scalar @$content && $content->[$bottomIndex]->Type() != ::MENU_GROUP()) { $bottomIndex++; }; if ($bottomIndex < scalar @$content) { my $bottomEntry = $content->[$bottomIndex]; $hasSubGroups = 1; if ($length + $menuGroupLengths{$bottomEntry} <= MENU_LENGTH_LIMIT) { $length += $menuGroupLengths{$bottomEntry}; push @$toExpand, $menuGroupNumbers{$bottomEntry}; $bottomIndex++; } else { $bottomIndex = scalar @$content; }; }; # Top next. while ($topIndex >= 0 && $content->[$topIndex]->Type() != ::MENU_GROUP()) { $topIndex--; }; if ($topIndex >= 0) { my $topEntry = $content->[$topIndex]; $hasSubGroups = 1; if ($length + $menuGroupLengths{$topEntry} <= MENU_LENGTH_LIMIT) { $length += $menuGroupLengths{$topEntry}; push @$toExpand, $menuGroupNumbers{$topEntry}; $topIndex--; } else { $topIndex = -1; }; }; }; $pass++; }; return $toExpand; }; # # Function: GetMenuSelectionHierarchy # # Finds the sequence of menu groups that contain the current selection. # # Parameters: # # sourceFile - The source to use if you're looking for a source file. # indexType - The index to use if you're looking for an index. # # Returns: # # An arrayref of the objects of each group surrounding the selected menu item. First entry is the # group immediately encompassing it, and each subsequent entry works its way towards the outermost group. # sub GetMenuSelectionHierarchy #(FileName sourceFile, TopicType indexType) -> NaturalDocs::Menu::Entry[] selectionHierarchy { my ($self, $sourceFile, $indexType) = @_; my $hierarchy = [ ]; $self->FindMenuSelection($sourceFile, $indexType, $hierarchy, NaturalDocs::Menu->Content()); return $hierarchy; }; # # Function: FindMenuSelection # # A recursive function that deterimes if it or any of its sub-groups has the menu selection. # # Parameters: # # sourceFile - The source to use if you're looking for a source file. # indexType - The index to use if you're looking for an index. # hierarchyRef - A reference to the menu selection hierarchy. # entries - An arrayref of to search. # # Returns: # # Whether this group or any of its subgroups had the selection. If true, it will add any subgroups to the menu selection # hierarchy but not itself. This prevents the topmost entry from being added. # sub FindMenuSelection #(FileName sourceFile, TopicType indexType, NaturalDocs::Menu::Entry[] hierarchyRef, NaturalDocs::Menu::Entry[] entries) -> bool hasSelection { my ($self, $sourceFile, $indexType, $hierarchyRef, $entries) = @_; foreach my $entry (@$entries) { if ($entry->Type() == ::MENU_GROUP()) { # If the subgroup has the selection... if ( $self->FindMenuSelection($sourceFile, $indexType, $hierarchyRef, $entry->GroupContent()) ) { push @$hierarchyRef, $entry; return 1; }; } elsif ($entry->Type() == ::MENU_FILE()) { if ($sourceFile eq $entry->Target()) { return 1; }; } elsif ($entry->Type() == ::MENU_INDEX()) { if ($indexType eq $entry->Target) { return 1; }; }; }; return 0; }; # # Function: ResetToolTips # # Resets the for a new page. # # Parameters: # # samePage - Set this flag if there's the possibility that the next batch of tooltips may be on the same page as the last. # sub ResetToolTips #(samePage) { my ($self, $samePage) = @_; if (!$samePage) { $tooltipLinkNumber = 1; $tooltipNumber = 1; }; $tooltipHTML = undef; %tooltipSymbolsToNumbers = ( ); }; 1;