Exemplo n.º 1
0
        /* Function: ExtractTypeLinks
         * Goes through the prototype of the passed <Topic> and adds any type links it finds to <LinkSet>.
         */
        protected void ExtractTypeLinks(Topic topic, LinkSet linkSet)
        {
            if (topic.Prototype == null)
            {
                return;
            }

            Language language = EngineInstance.Languages.FromID(topic.LanguageID);

            TokenIterator symbolStart = topic.ParsedPrototype.Tokenizer.FirstToken;
            TokenIterator symbolEnd;

            while (symbolStart.IsInBounds)
            {
                if (symbolStart.PrototypeParsingType == PrototypeParsingType.Type ||
                    symbolStart.PrototypeParsingType == PrototypeParsingType.TypeQualifier)
                {
                    symbolEnd = symbolStart;

                    do
                    {
                        symbolEnd.Next();
                    }while (symbolEnd.PrototypeParsingType == PrototypeParsingType.Type ||
                            symbolEnd.PrototypeParsingType == PrototypeParsingType.TypeQualifier);

                    if (language.IsBuiltInType(symbolStart, symbolEnd) == false)
                    {
                        Link link = new Link();

                        // ignore LinkID
                        link.Type    = LinkType.Type;
                        link.Symbol  = SymbolString.FromPlainText_NoParameters(symbolStart.Tokenizer.TextBetween(symbolStart, symbolEnd));
                        link.Context = topic.PrototypeContext;
                        // ignore contextID
                        link.FileID      = topic.FileID;
                        link.ClassString = topic.ClassString;
                        // ignore classID
                        link.LanguageID = topic.LanguageID;
                        // ignore EndingSymbol
                        // ignore TargetTopicID
                        // ignore TargetScore

                        linkSet.Add(link);
                    }

                    symbolStart = symbolEnd;
                }

                else
                {
                    symbolStart.Next();
                }
            }
        }
Exemplo n.º 2
0
        /* Function: GetParentList
         */
        protected List <Parent> GetParentList()
        {
            // First separate out all the class parent links that apply to this class.

            List <Link> parentLinks = null;

            if (links != null)
            {
                foreach (var link in links)
                {
                    if (link.Type == LinkType.ClassParent && link.ClassID == topic.ClassID)
                    {
                        if (parentLinks == null)
                        {
                            parentLinks = new List <Link>();
                        }

                        parentLinks.Add(link);
                    }
                }
            }

            // We don't have to worry about parents appearing in the prototype if there aren't any class parent links
            // because there would have been one generated for each of them in the parsing stage.
            if (parentLinks == null)
            {
                return(null);
            }


            // Now make entries for all the parents in the prototype.  Note that it's possible for there to be class parent
            // links yet no parents in the prototype.  Some languages define them separately, and some allow classes to
            // be defined across multiple files and the parents may only appear in one.

            List <Parent> parents = new List <Parent>();

            int           prototypeParentCount = topic.ParsedClassPrototype.NumberOfParents;
            TokenIterator start, end;

            for (int i = 0; i < prototypeParentCount; i++)
            {
                topic.ParsedClassPrototype.GetParentName(i, out start, out end);
                string parentName = start.Tokenizer.TextBetween(start, end);

                Parent parent = new Parent();
                parent.prototypeIndex  = i;
                parent.prototypeSymbol = SymbolString.FromPlainText_NoParameters(parentName);

                parents.Add(parent);
            }


            // Now we make one pass where we merge the class parent links with the prototype parents, if any.  Since
            // the links have been generated from the prototype, we don't have to do anything other than simple symbol
            // matching.  We don't have to worry about things like StringBuilder versus System.Text.StringBuilder yet.

            for (int i = 0; i < parentLinks.Count; /* don't auto-increment */)
            {
                bool foundMatch = false;

                foreach (var parent in parents)
                {
                    if (parent.prototypeSymbol == parentLinks[i].Symbol)
                    {
                        if (parent.link == null)
                        {
                            parent.link = parentLinks[i];
                        }

                        foundMatch = true;

                        // Keep going, don't break on the first match.  It's possible for multiple prototype parents to share
                        // the same link, such as IList and IList<T>.
                    }
                }

                if (foundMatch)
                {
                    parentLinks.RemoveAt(i);
                }
                else
                {
                    i++;
                }
            }


            // Now we do a second pass where we match links by their targets.  This is so if there's two links, one to
            // StringBuilder and one to System.Text.StringBuilder, and they both resolve to the same topic only one
            // will appear.  However, if neither resolve then just include them both.  We won't try to guess whether
            // partial symbol matches are probably the same parent.

            foreach (var parentLink in parentLinks)
            {
                bool found = false;

                if (parentLink.IsResolved)
                {
                    foreach (var parent in parents)
                    {
                        if (parent.link != null && parent.link.TargetTopicID == parentLink.TargetTopicID)
                        {
                            found = true;
                            break;
                        }
                    }
                }
                // If the link wasn't resolved we just leave found as false so it gets added.

                if (!found)
                {
                    Parent newParent = new Parent();
                    newParent.link = parentLink;
                    parents.Add(newParent);
                }
            }


            // Still not done.  Now go through the link targets and find the matches for each resolved link.

            foreach (var parent in parents)
            {
                if (parent.link != null && parent.link.IsResolved)
                {
                    foreach (var linkTarget in linkTargets)
                    {
                        if (linkTarget.TopicID == parent.link.TargetTopicID)
                        {
                            parent.targetTopic = linkTarget;
                            break;
                        }
                    }
                }
            }

            return(parents);
        }
Exemplo n.º 3
0
        // Group: Functions
        // __________________________________________________________________________


        public TopicEntry(Topic topic, SearchIndex.Manager manager) : base()
        {
            this.topic = topic;
            var commentType = manager.EngineInstance.CommentTypes.FromID(topic.CommentTypeID);
            var language    = manager.EngineInstance.Languages.FromID(topic.LanguageID);


            // Get the title without any parameters.  We don't want to include parameters in the index.  Multiple functions that
            // differ only by parameter will be treated as one entry.

            string title, ignore;

            ParameterString.SplitFromParameters(topic.Title, out title, out ignore);
            title = title.TrimEnd();


            // Figure out the extra scope text that should be added to the title to make it a fully resolved symbol.  We do this by
            // comparing the symbol from the topic to one generated from the title.  We don't just use the symbol to begin with
            // because we want to show the title as written; there's some normalization that occurs when generating symbols
            // that we want to bypass.

            string extraScope = null;

            SymbolString titleSymbol = SymbolString.FromPlainText_NoParameters(title);

            string titleSymbolString = titleSymbol.FormatWithSeparator(language.MemberOperator);
            string symbolString      = topic.Symbol.FormatWithSeparator(language.MemberOperator);

            if (symbolString.Length > titleSymbolString.Length)
            {
                // We have to go by LastIndexOf rather than EndsWith because operator<string> will have <string> cut off as a parameter.
                // We have to go by LastIndexOf instead of IndexOf so constructors don't get cut off (Package.Class.Class).
                int titleIndex = symbolString.LastIndexOf(titleSymbolString);

                                #if DEBUG
                if (titleIndex == -1)
                {
                    throw new Exception("Title symbol string \"" + titleSymbolString + "\" isn't part of symbol string \"" + symbolString + "\" which " +
                                        "was assumed when creating a search index entry.");
                }
                                #endif

                extraScope = symbolString.Substring(0, titleIndex);
            }


            // Remove the space in "operator <".  This prevents them from appearing as two keywords, and also makes sure "operator <" and
            // "operator<" are always displayed consistently, which will be important for sorting.

            title = SpaceAfterOperatorKeywordRegex.Replace(title, "");


            displayName = (extraScope == null ? title : extraScope + title);
            searchText  = Normalize(displayName);

            if (commentType.Flags.File)
            {
                endOfDisplayNameQualifiers = EndOfQualifiers(displayName, FileSplitSymbolsRegex.Matches(displayName));
                endOfSearchTextQualifiers  = EndOfQualifiers(searchText, FileSplitSymbolsRegex.Matches(searchText));
            }
            else if (commentType.Flags.Code)
            {
                endOfDisplayNameQualifiers = EndOfQualifiers(displayName, CodeSplitSymbolsRegex.Matches(displayName));
                endOfSearchTextQualifiers  = EndOfQualifiers(searchText, CodeSplitSymbolsRegex.Matches(searchText));
            }
            else             // documentation topic
            {
                if (extraScope == null)
                {
                    endOfDisplayNameQualifiers = 0;
                    endOfSearchTextQualifiers  = 0;
                }
                else
                {
                    endOfDisplayNameQualifiers = extraScope.Length;

                    // Don't need +1 because only leading separators are removed.  The trailing separator will still be there.
                    endOfSearchTextQualifiers = Normalize(extraScope).Length;
                }
            }

            keywords = new List <string>();

            if (endOfDisplayNameQualifiers == 0)
            {
                AddKeywords(displayName, commentType.Flags.Documentation);
            }
            else
            {
                AddKeywords(displayName.Substring(endOfDisplayNameQualifiers), commentType.Flags.Documentation);
            }
        }
Exemplo n.º 4
0
        /* Function: AppendSyntaxHighlightedTextWithTypeLinks
         *
         * Formats the text between the iterators with syntax highlighting and links for any tokens marked with
         * <PrototypeParsingType.Type> and <PrototypeParsingType.TypeQualifier>.  Appends the result to the passed StringBuilder.
         *
         * Parameters:
         *
         *		start - The first token of the text to convert.
         *		end - The end of the text to convert, which is one token past the last one included.
         *		output - The StringBuilder to append the output to.
         *
         *		links - A list of <Links> that should  contain any appearing in the code.
         *		linkTargets - A list of topics that should contain any used as targets in the list of links.
         *
         *		extendTypeSearch - If true, it will search beyond the bounds of the iterators to get the complete type.  This allows you to
         *									 format only a portion of the link with this function yet still have the link go to the complete destination.
         *
         * Requirements:
         *
         *		- The <Context>'s topic and page must be set.
         */
        public void AppendSyntaxHighlightedTextWithTypeLinks(TokenIterator start, TokenIterator end, StringBuilder output,
                                                             IList <Link> links, IList <Topics.Topic> linkTargets,
                                                             bool extendTypeSearch = false)
        {
                        #if DEBUG
            if (Context.Topic == null)
            {
                throw new Exception("Tried to call AppendSyntaxtHighlightedTextWithTypeLinks without setting the context's topic.");
            }
            if (Context.Page.IsNull)
            {
                throw new Exception("Tried to call AppendSyntaxtHighlightedTextWithTypeLinks without setting the context's page.");
            }
            if (links == null)
            {
                throw new Exception("Tried to call AppendSyntaxtHighlightedTextWithTypeLinks without setting the links variable.");
            }
            if (linkTargets == null)
            {
                throw new Exception("Tried to call AppendSyntaxtHighlightedTextWithTypeLinks without setting the linkTargets variable.");
            }
                        #endif

            Language language = EngineInstance.Languages.FromID(Context.Topic.LanguageID);


            // Find each Type/TypeQualifier stretch in the text

            TokenIterator iterator = start;

            while (iterator < end)
            {
                if (iterator.PrototypeParsingType == PrototypeParsingType.Type ||
                    iterator.PrototypeParsingType == PrototypeParsingType.TypeQualifier)
                {
                    TokenIterator textStart = iterator;
                    TokenIterator textEnd   = iterator;

                    do
                    {
                        textEnd.Next();
                    }while (textEnd < end &&
                            (textEnd.PrototypeParsingType == PrototypeParsingType.Type ||
                             textEnd.PrototypeParsingType == PrototypeParsingType.TypeQualifier));

                    TokenIterator symbolStart = textStart;
                    TokenIterator symbolEnd   = textEnd;


                    // Extend past start and end if the flag is set

                    if (extendTypeSearch && symbolStart == start)
                    {
                        TokenIterator temp = symbolStart;
                        temp.Previous();

                        while (temp.IsInBounds &&
                               (temp.PrototypeParsingType == PrototypeParsingType.Type ||
                                temp.PrototypeParsingType == PrototypeParsingType.TypeQualifier))
                        {
                            symbolStart = temp;
                            temp.Previous();
                        }
                    }

                    if (extendTypeSearch && symbolEnd == end)
                    {
                        while (symbolEnd.IsInBounds &&
                               (symbolEnd.PrototypeParsingType == PrototypeParsingType.Type ||
                                symbolEnd.PrototypeParsingType == PrototypeParsingType.TypeQualifier))
                        {
                            symbolEnd.Next();
                        }
                    }


                    // Built in types don't get links

                    if (language.IsBuiltInType(symbolStart, symbolEnd))
                    {
                        AppendSyntaxHighlightedText(textStart, textEnd, output);
                    }

                    else
                    {
                        // Create a link object with the identifying properties needed to look it up in the list of links.

                        Link linkStub = new Link();
                        linkStub.Type        = LinkType.Type;
                        linkStub.Symbol      = SymbolString.FromPlainText_NoParameters(symbolStart.TextBetween(symbolEnd));
                        linkStub.Context     = Context.Topic.PrototypeContext;
                        linkStub.ContextID   = Context.Topic.PrototypeContextID;
                        linkStub.FileID      = Context.Topic.FileID;
                        linkStub.ClassString = Context.Topic.ClassString;
                        linkStub.ClassID     = Context.Topic.ClassID;
                        linkStub.LanguageID  = Context.Topic.LanguageID;


                        // Find the actual link so we know if it resolved to anything.

                        Link fullLink = null;

                        foreach (Link link in links)
                        {
                            if (link.SameIdentifyingPropertiesAs(linkStub))
                            {
                                fullLink = link;
                                break;
                            }
                        }

                                                #if DEBUG
                        if (fullLink == null)
                        {
                            throw new Exception("All links in a topic must be in the list passed to AppendSyntaxtHighlightedTextWithTypeLinks.");
                        }
                                                #endif


                        // If it didn't resolve, we just output the original text.

                        if (!fullLink.IsResolved)
                        {
                            AppendSyntaxHighlightedText(textStart, textEnd, output);
                        }

                        else
                        {
                            // If it did resolve, find Topic it resolved to.

                            Topics.Topic targetTopic = null;

                            foreach (var linkTarget in linkTargets)
                            {
                                if (linkTarget.TopicID == fullLink.TargetTopicID)
                                {
                                    targetTopic = linkTarget;
                                    break;
                                }
                            }

                                                        #if DEBUG
                            if (targetTopic == null)
                            {
                                throw new Exception("All links targets for a topic must be in the list passed to AppendSyntaxtHighlightedTextWithTypeLinks.");
                            }
                                                        #endif

                            AppendOpeningLinkTag(targetTopic, output);
                            AppendSyntaxHighlightedText(textStart, textEnd, output);
                            output.Append("</a>");
                        }
                    }

                    iterator = textEnd;
                }

                else                 // not on a type
                {
                    TokenIterator startText = iterator;

                    do
                    {
                        iterator.Next();
                    }while (iterator < end &&
                            iterator.PrototypeParsingType != PrototypeParsingType.Type &&
                            iterator.PrototypeParsingType != PrototypeParsingType.TypeQualifier);

                    AppendSyntaxHighlightedText(startText, iterator, output);
                }
            }
        }
Exemplo n.º 5
0
        /* Function: GetPrototypeLinks
         * Goes through the prototype of the passed <Topic> and adds any type links it finds to <LinkSet>.
         */
        protected void GetPrototypeLinks(Topic topic, ref LinkSet linkSet)
        {
            if (topic.Prototype == null)
            {
                return;
            }

            Language language = EngineInstance.Languages.FromID(topic.LanguageID);

            // We do this even for topics in the class hierarchy because the HTML output falls back to regular prototypes
            // if there's no class prototype.  Also, if there's parameter lists in the description the HTML generator will require
            // type links to exist regardless of what type of prototype it creates.  For example, this SystemVerilog interface:
            //
            //    // Interface: myInterface
            //    //
            //    // Parameters:
            //    //    PARAMNAME - description
            //
            //    interface myInterface #(parameter PARAMNAME = 8) (input reset, clk);
            //
            // The HTML generation for the Parameters section will expect a type link to exist for PARAMNAME.

            TokenIterator symbolStart = topic.ParsedPrototype.Tokenizer.FirstToken;
            TokenIterator symbolEnd;

            while (symbolStart.IsInBounds)
            {
                if (symbolStart.PrototypeParsingType == PrototypeParsingType.Type ||
                    symbolStart.PrototypeParsingType == PrototypeParsingType.TypeQualifier)
                {
                    symbolEnd = symbolStart;

                    do
                    {
                        symbolEnd.Next();
                    }while (symbolEnd.PrototypeParsingType == PrototypeParsingType.Type ||
                            symbolEnd.PrototypeParsingType == PrototypeParsingType.TypeQualifier);

                    if (language.Parser.IsBuiltInType(symbolStart, symbolEnd) == false)
                    {
                        Link link = new Link();

                        // ignore LinkID
                        link.Type    = LinkType.Type;
                        link.Symbol  = SymbolString.FromPlainText_NoParameters(symbolStart.TextBetween(symbolEnd));
                        link.Context = topic.PrototypeContext;
                        // ignore contextID
                        link.FileID      = topic.FileID;
                        link.ClassString = topic.ClassString;
                        // ignore classID
                        link.LanguageID = topic.LanguageID;
                        // ignore EndingSymbol
                        // ignore TargetTopicID
                        // ignore TargetScore

                        linkSet.Add(link);
                    }

                    symbolStart = symbolEnd;
                }

                else
                {
                    symbolStart.Next();
                }
            }
        }
Exemplo n.º 6
0
        // Group: Link Scoring Functions
        // __________________________________________________________________________


        /* Function: Score
         *
         * Generates a numeric score representing how well the <Topic> serves as a match for the <Link>.  Higher scores are
         * better, and zero means they don't match at all.
         *
         * If a score has to beat a certain threshold to be relevant, you can pass it to lessen the processing load.  This function
         * may be able to tell it can't beat the score early and return without performing later steps.  In these cases it will return
         * -1.
         *
         * If scoring a Natural Docs link you must pass a list of interpretations.  It must include the literal form.
         */
        public long Score(Link link, Topic topic, long minimumScore = 0, List <LinkInterpretation> interpretations = null)
        {
            // DEPENDENCY: These things depend on the score's internal format:
            //   - EngineTests.LinkScoring
            //   - Link.TargetInterepretationIndex

            // Other than that the score's format should be treated as opaque.  Nothing beyond this class should try to
            // interpret the value other than to know that higher is better, zero is not a match, and -1 means we quit early.

            // It's a 64-bit value so we'll assign bits to the different characteristics.  Higher order bits obviously result in higher
            // numeric values so the characteristics are ordered by priority.

            // Format:
            // 0LCETPPP PPPPPPPP PPPPPPPP PSSSSSSS SSSIIIII IBFFFFFF Rbbbbbbb brrrrrr1

            // 0 - The first bit is zero to make sure the number is positive.

            // L - Whether the topic matches the link's language.
            // C - Whether the topic and link's capitalization match if it matters to the language.
            // E - Whether the text is an exact match with no plural or possessive conversions applied.
            // T - Whether the link parameters exactly match the topic title parameters.
            // P - How well the parameters match.
            // S - How high on the scope list the symbol match is.
            // I - How high on the interpretation list (named/plural/possessive) the match is.
            // B - Whether the topic has a body
            // F - How high on the list of topics that define the same symbol in the same file this is.
            // R - Whether the topic has a prototype.
            // b - The length of the body divided by 16.
            // r - The length of the prototype divided by 16.

            // 1 - The final bit is one to make sure a match will never be zero.


            // For type and class parent links, the comment type MUST have the relevant attribute set to be possible.

            var commentType = EngineInstance.CommentTypes.FromID(topic.CommentTypeID);
            var language    = EngineInstance.Languages.FromID(topic.LanguageID);

            if ((link.Type == LinkType.ClassParent && commentType.InClassHierarchy == false) ||
                (link.Type == LinkType.Type && commentType.IsVariableType == false))
            {
                return(0);
            }


            // 0------- -------- -------- -------- -------- -------- -------- -------1
            // Our baseline.

            long score = 0x0000000000000001;


            // =L------ -------- -------- -------- -------- -------- -------- -------=
            // L - Whether the topic's language matches the link's language.  For type and class parent links this is mandatory.  For
            // Natural Docs links this is the highest priority criteria as links should favor any kind of match within their own language
            // over matches from another.

            if (link.LanguageID == topic.LanguageID)
            {
                score |= 0x4000000000000000;
            }
            else if (link.Type == LinkType.ClassParent || link.Type == LinkType.Type)
            {
                return(0);
            }
            else if (minimumScore > 0x3FFFFFFFFFFFFFFF)
            {
                return(-1);
            }


            // ==CE---- -------- -------- -SSSSSSS SSSIIIII I------- -------- -------=
            // Now we have to go through the interpretations to figure out the fields that could change based on them.
            // C and S will be handled by ScoreInterpretation().  E and I will be handled here.

            // C - Whether the topic and link's capitalization match if it matters to the language.  This depends on the
            //		 interpretation because it can be affected by how named links are split.
            // E - Whether the text is an exact match with no plural or possessive conversions applied.  Named links are
            //		 okay.
            // S - How high on the scope list the symbol match is.
            // I - How high on the interpretation list (named/plural/possessive) the match is.

            long bestInterpretationScore = 0;
            int  bestInterpretationIndex = 0;

            if (link.Type == LinkType.NaturalDocs)
            {
                for (int i = 0; i < interpretations.Count; i++)
                {
                    long interpretationScore = ScoreInterpretation(topic, link, SymbolString.FromPlainText_NoParameters(interpretations[i].Target));

                    if (interpretationScore != 0)
                    {
                        // Add E if there were no plurals or possessives.  Named links are okay.
                        if (interpretations[i].PluralConversion == false && interpretations[i].PossessiveConversion == false)
                        {
                            interpretationScore |= 0x1000000000000000;
                        }

                        if (interpretationScore > bestInterpretationScore)
                        {
                            bestInterpretationScore = interpretationScore;
                            bestInterpretationIndex = i;
                        }
                    }
                }
            }

            else             // type or class parent link
            {
                bestInterpretationScore = ScoreInterpretation(topic, link, link.Symbol);
                bestInterpretationIndex = 0;

                // Add E if there was a match.
                if (bestInterpretationScore != 0)
                {
                    bestInterpretationScore |= 0x1000000000000000;
                }
            }

            // If none of the symbol interpretations matched the topic, we're done.
            if (bestInterpretationScore == 0)
            {
                return(0);
            }

            // Combine C, E, and S into the main score.
            score |= bestInterpretationScore;

            // Calculate I so that lower indexes are higher scores.  Since these are the lowest order bits it's okay to leave
            // this for the end instead of calculating it for every interpretation.
            if (bestInterpretationIndex > 63)
            {
                bestInterpretationIndex = 63;
            }

            long bestInterpretationBits = 63 - bestInterpretationIndex;

            bestInterpretationBits <<= 23;

            score |= bestInterpretationBits;

            if ((score | 0x0FFFFF80007FFFFF) < minimumScore)
            {
                return(-1);
            }


            // ====TPPP PPPPPPPP PPPPPPPP P======= ======== =------- -------- -------=
            // T - Whether the link parameters exactly match the topic title parameters.
            // P - How well the parameters match.

            // Both of these only apply to Natural Docs links that have parameters.
            if (link.Type == LinkType.NaturalDocs)
            {
                int parametersIndex = ParameterString.GetParametersIndex(link.Text);

                if (parametersIndex != -1)
                {
                    string          linkParametersString = link.Text.Substring(parametersIndex);
                    ParameterString linkParameters       = ParameterString.FromPlainText(linkParametersString);

                    // If the topic title has parameters as well, the link parameters must match them exactly.  We
                    // don't do fuzzy matching with topic title parameters.
                    if (topic.HasTitleParameters && string.Compare(linkParameters, topic.TitleParameters, !language.CaseSensitive) == 0)
                    {
                        score |= 0x0800000000000000;
                        // We can skip the prototype match since this outweighs it.  Also, we don't want two link targets
                        // where the topic title parameters are matched to be distinguished by the prototype parameters.
                        // We'll let it fall through to lower properties in the score.
                    }
                    else
                    {
                        // Score the first nine parameters.
                        for (int i = 0; i < 9; i++)
                        {
                            long paramScore = ScoreParameter(topic.ParsedPrototype, linkParameters, i, !language.CaseSensitive);

                            if (paramScore == -1)
                            {
                                return(0);
                            }

                            paramScore <<= 39 + ((9 - i) * 2);
                            score       |= paramScore;
                        }

                        // The tenth is special.  It's possible that functions may have more than ten parameters, so we go
                        // through the rest of them and use the lowest score we get.

                        long lastParamScore = ScoreParameter(topic.ParsedPrototype, linkParameters, 9, !language.CaseSensitive);
                        int  maxParameters  = linkParameters.NumberOfParameters;

                        if (topic.ParsedPrototype != null && topic.ParsedPrototype.NumberOfParameters > maxParameters)
                        {
                            maxParameters = topic.ParsedPrototype.NumberOfParameters;
                        }

                        for (int i = 10; i < maxParameters; i++)
                        {
                            long paramScore = ScoreParameter(topic.ParsedPrototype, linkParameters, i, !language.CaseSensitive);

                            if (paramScore < lastParamScore)
                            {
                                lastParamScore = paramScore;
                            }
                        }

                        if (lastParamScore == -1)
                        {
                            return(0);
                        }

                        lastParamScore <<= 39;
                        score           |= lastParamScore;
                    }
                }
            }


            // ======== ======== ======== ======== ======== =BFFFFFF Rbbbbbbb brrrrrr=
            // Finish off the score with the topic properties.

            // B - Whether the topic has a body
            // F - How high on the list of topics that define the same symbol in the same file this is.
            // R - Whether the topic has a prototype.
            // b - The length of the body divided by 16.
            // r - The length of the prototype divided by 16.

            score |= ScoreTopic(topic);

            return(score);
        }
Exemplo n.º 7
0
        // Group: Functions
        // __________________________________________________________________________


        /* Function: Merge
         *
         * Takes a list of <Topics> that come from the same class but multiple source files and rearranges them into a
         * single coherent list.  Some topics may be removed or merged with others.  The original topic list will be changed.
         *
         * Each file's topics should appear consecutively in the list and ideally in source order.  The order of the files is not
         * important but should ideally be consistent from one run to the next.
         *
         * It's possible for this function to reduce the number of topics to zero.  For example, if defining classes with a list
         * topic, the list topic itself will be removed.  You should be able to handle this and treat it as if the topic list had
         * no content.
         */
        public static void Merge(ref List <Topic> topics, Engine.Instance engineInstance)
        {
            try
            {
                var files        = engineInstance.Files;
                var commentTypes = engineInstance.CommentTypes;


                // Filter out any list topics that define members of a hierarchy.  If someone documents classes as part of a list,
                // we only want pages for the individual members, not the list topic.

                for (int i = 0; i < topics.Count; /* no auto-increment */)
                {
                    bool remove = false;

                    if (topics[i].IsList)
                    {
                        var commentType = commentTypes.FromID(topics[i].CommentTypeID);

                        if (commentType.InClassHierarchy || commentType.InDatabaseHierarchy)
                        {
                            remove = true;
                        }
                    }

                    if (remove)
                    {
                        topics.RemoveAt(i);
                    }
                    else
                    {
                        i++;
                    }
                }

                if (topics.Count == 0)
                {
                    return;
                }


                // Validate that they're all from the same class and that all of a file's topics are consecutive.

                                #if DEBUG
                int         classID     = topics[0].ClassID;
                ClassString classString = topics[0].ClassString;

                if (classID == 0)
                {
                    throw new Exception("All topics passed to Merge() must have a class ID set.");
                }

                int currentFileID = topics[0].FileID;
                IDObjects.NumberSet previousFileIDs = new IDObjects.NumberSet();

                for (int i = 1; i < topics.Count; i++)
                {
                    if (topics[i].ClassID != classID || topics[i].ClassString != classString)
                    {
                        throw new Exception("All topics passed to Merge() must have the same class string and ID.");
                    }

                    if (topics[i].FileID != currentFileID)
                    {
                        if (previousFileIDs.Contains(topics[i].FileID))
                        {
                            throw new Exception("Merge() requires all topics that share a file ID be consecutive.");
                        }

                        previousFileIDs.Add(currentFileID);
                        currentFileID = topics[i].FileID;
                    }
                }
                                #endif


                // See if there's multiple source files by comparing the first and last topics' file IDs.  If there's only one source file we'll be
                // able to skip some steps.

                bool multipleSourceFiles = (topics[0].FileID != topics[topics.Count - 1].FileID);


                List <Topic> remainingTopics = null;

                if (multipleSourceFiles)
                {
                    // First we have to sort the topic list by file name.  This ensures that the merge occurs consistently no matter
                    // what order the files in the list are in or how the file IDs were assigned.

                    List <Topic> sortedTopics = new List <Topic>(topics.Count);

                    do
                    {
                        var lowestFile      = files.FromID(topics[0].FileID);
                        var lowestFileIndex = 0;
                        var lastCheckedID   = lowestFile.ID;

                        for (int i = 1; i < topics.Count; i++)
                        {
                            if (topics[i].FileID != lastCheckedID)
                            {
                                var file = files.FromID(topics[i].FileID);

                                if (Path.Compare(file.FileName, lowestFile.FileName) < 0)
                                {
                                    lowestFile      = file;
                                    lowestFileIndex = i;
                                }

                                lastCheckedID = file.ID;
                            }
                        }

                        int count = 0;
                        for (int i = lowestFileIndex; i < topics.Count && topics[i].FileID == lowestFile.ID; i++)
                        {
                            count++;
                        }

                        sortedTopics.AddRange(topics.GetRange(lowestFileIndex, count));
                        topics.RemoveRange(lowestFileIndex, count);
                    }while (topics.Count > 0);


                    // The topics are all in sortedTopics now, and "topics" is empty.  For clarity going forward, let's rename sortedTopics
                    // to remainingTopics, since we have to move them back into topics now.

                    remainingTopics = sortedTopics;
                    sortedTopics    = null;                   // for safety


                    // Find the best topic to serve as the class definition.

                    Topic bestDefinition      = remainingTopics[0];
                    int   bestDefinitionIndex = 0;

                    for (int i = 1; i < remainingTopics.Count; i++)
                    {
                        Topic topic = remainingTopics[i];

                        if (topic.DefinesClass && engineInstance.Links.IsBetterClassDefinition(bestDefinition, topic))
                        {
                            bestDefinition      = topic;
                            bestDefinitionIndex = i;
                        }
                    }


                    // Copy the best definition in and everything that follows it in the file.  That will serve as the base for merging.

                    int bestDefinitionTopicCount = 1;

                    for (int i = bestDefinitionIndex + 1; i < remainingTopics.Count && remainingTopics[i].FileID == bestDefinition.FileID; i++)
                    {
                        bestDefinitionTopicCount++;
                    }

                    topics.AddRange(remainingTopics.GetRange(bestDefinitionIndex, bestDefinitionTopicCount));
                    remainingTopics.RemoveRange(bestDefinitionIndex, bestDefinitionTopicCount);
                }                         // if multipleSourceFiles


                // Make sure the first topic isn't embedded so that classes documented in lists still appear correctly.

                if (topics[0].IsEmbedded)
                {
                    topics[0]            = topics[0].Duplicate();
                    topics[0].IsEmbedded = false;
                }


                // Delete all the other topics that define the class.  We don't need them anymore.

                for (int i = 1; i < topics.Count; /* don't auto increment */)
                {
                    if (topics[i].DefinesClass)
                    {
                        topics.RemoveAt(i);
                    }
                    else
                    {
                        i++;
                    }
                }

                if (multipleSourceFiles)
                {
                    for (int i = 0; i < remainingTopics.Count; /* don't auto increment */)
                    {
                        if (remainingTopics[i].DefinesClass)
                        {
                            remainingTopics.RemoveAt(i);
                        }
                        else
                        {
                            i++;
                        }
                    }


                    // Now merge the remaining topics into the main list.

                    // We loop through this process one file at a time in case some topics have to be merged that aren't present in the
                    // base we chose.  For example, File A has FunctionA but not FunctionZ.  File B and File C both have FunctionZ and
                    // they need to be merged with each other.  If we only did one pass comparing all the remaining topics to the base
                    // we wouldn't see that.

                    while (remainingTopics.Count > 0)
                    {
                        int fileID = remainingTopics[0].FileID;


                        // First pick out and merge duplicates.  This is used for things like combining header and source definitions in C++.

                        for (int remainingTopicIndex = 0;
                             remainingTopicIndex < remainingTopics.Count && remainingTopics[remainingTopicIndex].FileID == fileID;
                             /* no auto-increment */)
                        {
                            var remainingTopic = remainingTopics[remainingTopicIndex];

                            // We're ignoring group topics for now.  They stay in remainingTopics.
                            if (remainingTopic.IsGroup)
                            {
                                remainingTopicIndex++;
                                continue;
                            }

                            int embeddedTopicCount = CountEmbeddedTopics(remainingTopics, remainingTopicIndex);


                            // If we're merging enums, the one with the most embedded topics (documented values) wins.  In practice one
                            // should be documented and one shouldn't be, so this should usually be any number versus zero.

                            if (remainingTopic.IsEnum)
                            {
                                int duplicateIndex = FindDuplicateTopic(remainingTopic, topics, engineInstance);

                                if (duplicateIndex == -1)
                                {
                                    remainingTopicIndex += 1 + embeddedTopicCount;
                                }
                                else
                                {
                                    int duplicateEmbeddedTopicCount = CountEmbeddedTopics(topics, duplicateIndex);

                                    if (embeddedTopicCount > duplicateEmbeddedTopicCount ||
                                        (embeddedTopicCount == duplicateEmbeddedTopicCount &&
                                         engineInstance.Links.IsBetterTopicDefinition(remainingTopic, topics[duplicateIndex]) == false))
                                    {
                                        topics.RemoveRange(duplicateIndex, 1 + duplicateEmbeddedTopicCount);
                                        topics.InsertRange(duplicateIndex, remainingTopics.GetRange(remainingTopicIndex, 1 + embeddedTopicCount));
                                    }

                                    remainingTopics.RemoveRange(remainingTopicIndex, 1 + embeddedTopicCount);
                                }
                            }


                            // If it's not an enum and it's a standalone topic, the one with the best score wins.

                            else if (embeddedTopicCount == 0)
                            {
                                int duplicateIndex = FindDuplicateTopic(remainingTopic, topics, engineInstance);

                                if (duplicateIndex == -1)
                                {
                                    remainingTopicIndex++;
                                }
                                else if (engineInstance.Links.IsBetterTopicDefinition(remainingTopic, topics[duplicateIndex]) == false)
                                {
                                    if (topics[duplicateIndex].IsEmbedded)
                                    {
                                        // Just leave them both in
                                        remainingTopicIndex++;
                                    }
                                    else
                                    {
                                        topics[duplicateIndex] = remainingTopic;
                                        remainingTopics.RemoveAt(remainingTopicIndex);
                                    }
                                }
                                else
                                {
                                    remainingTopics.RemoveAt(remainingTopicIndex);
                                }
                            }


                            // If it's not an enum and we're at a list topic, leave it for now.  We only want to remove it if EVERY member has
                            // a better definition, and those definitions can be in different files, so wait until the list is fully combined.

                            else
                            {
                                remainingTopicIndex += 1 + embeddedTopicCount;
                            }
                        }


                        // Generate groups from the topic lists.

                        // Start at 1 to skip the class topic.
                        var topicGroups = GetTopicGroups(topics, startingIndex: 1);

                        var remainingTopicGroups = GetTopicGroups(remainingTopics, limitToFileID: fileID);


                        // Now merge groups.

                        int remainingGroupIndex = 0;
                        while (remainingGroupIndex < remainingTopicGroups.Groups.Count)
                        {
                            var  remainingGroup = remainingTopicGroups.Groups[remainingGroupIndex];
                            bool merged         = false;

                            // If the group is empty because all its members were merged as duplicates, just delete it.
                            if (remainingGroup.IsEmpty)
                            {
                                remainingTopicGroups.RemoveGroupAndTopics(remainingGroupIndex);
                                merged = true;
                            }

                            // If it matches the title of an existing group, move its members to the end of the existing group.
                            else if (remainingGroup.Title != null)
                            {
                                for (int groupIndex = 0; groupIndex < topicGroups.Groups.Count; groupIndex++)
                                {
                                    if (topicGroups.Groups[groupIndex].Title == remainingGroup.Title)
                                    {
                                        remainingTopicGroups.MergeGroupInto(remainingGroupIndex, topicGroups, groupIndex);
                                        merged = true;
                                        break;
                                    }
                                }

                                // If the group had a title but didn't match one on the other list, insert it after the last group of the same
                                // dominant type so function groups stay with other function groups, variable groups stay with other variable
                                // groups, etc.
                                if (merged == false)
                                {
                                    int bestMatchIndex = -1;

                                    // Walk the list backwards because we want it to be after the last group of the type, not the first.
                                    for (int i = topicGroups.Groups.Count - 1; i >= 0; i--)
                                    {
                                        if (topicGroups.Groups[i].DominantTypeID == remainingGroup.DominantTypeID)
                                        {
                                            bestMatchIndex = i;
                                            break;
                                        }
                                    }

                                    if (bestMatchIndex == -1)
                                    {
                                        // Just add the group to the end if nothing matches.
                                        remainingTopicGroups.MoveGroupTo(remainingGroupIndex, topicGroups);
                                    }
                                    else
                                    {
                                        remainingTopicGroups.MoveGroupTo(remainingGroupIndex, topicGroups, bestMatchIndex + 1);
                                    }

                                    merged = true;
                                }
                            }

                            if (!merged)
                            {
                                remainingGroupIndex++;
                            }
                        }


                        // Now we're left with topics that are not in titled groups, meaning the file itself had no group topics or there were
                        // topics that appeared before the first one.  See if the base contains any titled groups.

                        bool hasGroupsWithTitles = false;

                        foreach (var group in topicGroups.Groups)
                        {
                            if (group.Title != null)
                            {
                                hasGroupsWithTitles = true;
                                break;
                            }
                        }


                        // If there's no titles we can just append the remaining topics as is.

                        if (hasGroupsWithTitles == false)
                        {
                            int fileIDLimit = 0;

                            while (fileIDLimit < remainingTopics.Count && remainingTopics[fileIDLimit].FileID == fileID)
                            {
                                fileIDLimit++;
                            }

                            if (fileIDLimit > 0)
                            {
                                topics.AddRange(remainingTopics.GetRange(0, fileIDLimit));
                                remainingTopics.RemoveRange(0, fileIDLimit);
                            }
                        }


                        // If there are titled groups, see if we can add them to the end of existing groups.  However, only do
                        // this if TitleMatchesType is set.  It's okay to put random functions into the group "Functions" but
                        // not into something more specific.  If there aren't appropriate groups to do this with, create new ones.

                        else
                        {
                            while (remainingTopics.Count > 0 && remainingTopics[0].FileID == fileID)
                            {
                                int type = remainingTopics[0].CommentTypeID;
                                int matchingGroupIndex = -1;

                                for (int i = topicGroups.Groups.Count - 1; i >= 0; i--)
                                {
                                    if (topicGroups.Groups[i].DominantTypeID == type &&
                                        topicGroups.Groups[i].TitleMatchesType)
                                    {
                                        matchingGroupIndex = i;
                                        break;
                                    }
                                }

                                // Create a new group if there's no existing one we can use.
                                if (matchingGroupIndex == -1)
                                {
                                    Topic generatedTopic = new Topic(engineInstance.CommentTypes);
                                    generatedTopic.TopicID       = 0;
                                    generatedTopic.Title         = engineInstance.CommentTypes.FromID(type).PluralDisplayName;
                                    generatedTopic.Symbol        = SymbolString.FromPlainText_NoParameters(generatedTopic.Title);
                                    generatedTopic.ClassString   = topics[0].ClassString;
                                    generatedTopic.ClassID       = topics[0].ClassID;
                                    generatedTopic.CommentTypeID = engineInstance.CommentTypes.IDFromKeyword("group", topics[0].LanguageID);
                                    generatedTopic.FileID        = topics[0].FileID;
                                    generatedTopic.LanguageID    = topics[0].LanguageID;

                                    // In case there's nothing that defines the "group" keyword.
                                    if (generatedTopic.CommentTypeID != 0)
                                    {
                                        topicGroups.Topics.Add(generatedTopic);
                                        topicGroups.CreateGroup(topicGroups.Topics.Count - 1, 1);
                                    }

                                    matchingGroupIndex = topicGroups.Groups.Count - 1;
                                }

                                do
                                {
                                    int topicsToMove = 1 + CountEmbeddedTopics(remainingTopics, 0);

                                    while (topicsToMove > 0)
                                    {
                                        topicGroups.AppendToGroup(matchingGroupIndex, remainingTopics[0]);
                                        remainingTopics.RemoveAt(0);
                                        topicsToMove--;
                                    }
                                }while (remainingTopics.Count > 0 && remainingTopics[0].CommentTypeID == type);
                            }
                        }
                    }


                    // Now that everything's merged into one list, make another pass to merge list topics.

                    for (int topicIndex = 0; topicIndex < topics.Count; /* no auto-increment */)
                    {
                        var topic = topics[topicIndex];

                        // Ignore group topics
                        if (topic.IsGroup)
                        {
                            topicIndex++;
                            continue;
                        }

                        int embeddedTopicCount = CountEmbeddedTopics(topics, topicIndex);

                        // Ignore single topics and enums.  Enums have embedded topics but we already handled them earlier.
                        if (embeddedTopicCount == 0 || topic.IsEnum)
                        {
                            topicIndex += 1 + embeddedTopicCount;
                            continue;
                        }


                        // If we're here we're at a list topic.  Compare its members with every other member in the list.  Remove standalone
                        // topics if the list contains a better definition, but only remove the list if EVERY member has a better definition
                        // somewhere else.  If only some do we'll leave in the whole thing and have duplicates instead of trying to pluck out
                        // individual embedded topics.

                        bool embeddedContainsBetterDefinitions = false;
                        bool embeddedContainsNonDuplicates     = false;

                        for (int embeddedTopicIndex = topicIndex + 1;
                             embeddedTopicIndex < topicIndex + 1 + embeddedTopicCount;
                             embeddedTopicIndex++)
                        {
                            var embeddedTopic         = topics[embeddedTopicIndex];
                            var embeddedTopicLanguage = engineInstance.Languages.FromID(embeddedTopic.LanguageID);
                            var foundDuplicate        = false;

                            for (int potentialDuplicateTopicIndex = 0; potentialDuplicateTopicIndex < topics.Count; /* no auto-increment */)
                            {
                                /* Skip ones in the list topic */
                                if (potentialDuplicateTopicIndex == topicIndex)
                                {
                                    potentialDuplicateTopicIndex += 1 + embeddedTopicCount;
                                    continue;
                                }

                                var potentialDuplicateTopic = topics[potentialDuplicateTopicIndex];

                                if (embeddedTopicLanguage.Parser.IsSameCodeElement(embeddedTopic, potentialDuplicateTopic))
                                {
                                    foundDuplicate = true;

                                    // If the current embedded topic is the better definition
                                    if (engineInstance.Links.IsBetterTopicDefinition(potentialDuplicateTopic, embeddedTopic))
                                    {
                                        embeddedContainsBetterDefinitions = true;

                                        // If the duplicate is also embedded, leave it alone.  Either the duplicate is going to be allowed to exist
                                        // because neither list can be completely removed, or it will be removed later when its own list is checked
                                        // for duplicates.
                                        if (potentialDuplicateTopic.IsEmbedded)
                                        {
                                            potentialDuplicateTopicIndex++;
                                        }

                                        // If the duplicate is not embedded we can remove it.
                                        else
                                        {
                                            topics.RemoveAt(potentialDuplicateTopicIndex);

                                            if (potentialDuplicateTopicIndex < topicIndex)
                                            {
                                                topicIndex--;
                                                embeddedTopicIndex--;
                                            }
                                        }
                                    }

                                    // If the potential duplicate is the better definition.  We don't need to do anything here because we're just
                                    // looking to see if all of them have better definitions elsewhere, which can be determined by whether this
                                    // group contains any better definitions or non-duplicates.
                                    else
                                    {
                                        potentialDuplicateTopicIndex++;
                                    }
                                }

                                // Not the same code element
                                else
                                {
                                    potentialDuplicateTopicIndex++;
                                }
                            }

                            if (!foundDuplicate)
                            {
                                embeddedContainsNonDuplicates = true;
                            }
                        }


                        // Now that we've checked every embedded topic against every other topic, remove the entire list only if EVERY
                        // member has a better definition somewhere else, which is the same as saying it doesn't contain any better
                        // topic definitions or non-duplicates.

                        if (embeddedContainsBetterDefinitions == false && embeddedContainsNonDuplicates == false)
                        {
                            topics.RemoveRange(topicIndex, 1 + embeddedTopicCount);
                        }
                        else
                        {
                            topicIndex += 1 + embeddedTopicCount;
                        }
                    }
                }                          // if multipleSourceFiles


                // Now that everything's merged, delete any empty groups.  We do this on the main group list for consistency,
                // since we were doing it on the remaining group list during merging.  Also, there may be new empty groups after
                // merging the list topics.

                // Start at 1 to skip the class topic.
                var groupedTopics = GetTopicGroups(topics, startingIndex: 1);

                for (int i = 0; i < groupedTopics.Groups.Count; /* don't auto increment */)
                {
                    if (groupedTopics.Groups[i].IsEmpty)
                    {
                        groupedTopics.RemoveGroupAndTopics(i);
                    }
                    else
                    {
                        i++;
                    }
                }
            }

            catch (Exception e)
            {
                // Build a message to show the class we crashed on
                if (topics != null && topics.Count >= 1 && topics[0].ClassString != null)
                {
                    var           topic = topics[0];
                    StringBuilder task  = new StringBuilder("Building class view for");

                    // Hierarchy
                    if (topic.ClassString.Hierarchy == Hierarchy.Database)
                    {
                        task.Append(" database");
                    }
                    else
                    {
                        // Language name
                        var language = (topics[0].LanguageID > 0 ? engineInstance.Languages.FromID(topics[0].LanguageID) : null);

                        if (language == null)
                        {
                            task.Append(" language ID " + topics[0].LanguageID + " class");
                        }
                        else
                        {
                            task.Append(" " + language.Name + " class");
                        }
                    }

                    // Class name
                    task.Append(" " + topic.ClassString.Symbol.FormatWithSeparator('.'));

                    e.AddNaturalDocsTask(task.ToString());
                }

                throw;
            }
        }
Exemplo n.º 8
0
        // Group: Functions
        // __________________________________________________________________________


        /* Function: MergeTopics
         * Takes a list of <Topics> that come from the same class but multiple source files and combines them into a
         * single coherent list.  It assumes all topics from a single file will be consecutive, but otherwise the groups of
         * topics can be in any order.
         */
        public static void MergeTopics(List <Topic> topics, Builder builder)
        {
            if (topics.Count == 0)
            {
                return;
            }

                        #if DEBUG
            // Validate that they're all from the same class and that all of a file's topics are consecutive.

            int         classID     = topics[0].ClassID;
            ClassString classString = topics[0].ClassString;

            if (classID == 0)
            {
                throw new Exception("All topics passed to MergeTopics() must have a class ID set.");
            }

            int currentFileID = topics[0].FileID;
            IDObjects.NumberSet previousFileIDs = new IDObjects.NumberSet();

            for (int i = 1; i < topics.Count; i++)
            {
                if (topics[i].ClassID != classID || topics[i].ClassString != classString)
                {
                    throw new Exception("All topics passed to MergeTopics() must have the same class string and ID.");
                }

                if (topics[i].FileID != currentFileID)
                {
                    if (previousFileIDs.Contains(topics[i].FileID))
                    {
                        throw new Exception("MergeTopics() requires all topics that share a file ID be consecutive.");
                    }

                    previousFileIDs.Add(currentFileID);
                    currentFileID = topics[i].FileID;
                }
            }
                        #endif

            // If the first and last topic have the same file ID, that means the entire list does and we can return it as is.
            if (topics[0].FileID == topics[topics.Count - 1].FileID)
            {
                // We do still have to make sure the first topic isn't embedded though so that classes documented in lists will
                // appear correctly.
                if (topics[0].IsEmbedded)
                {
                    topics[0]            = topics[0].Duplicate();
                    topics[0].IsEmbedded = false;
                }

                return;
            }

            var engineInstance = builder.EngineInstance;
            var files          = engineInstance.Files;
            var commentTypes   = engineInstance.CommentTypes;


            // First we have to sort the topic list by file name.  This ensures that the merge occurs consistently no matter
            // what order the files in the list are in or how the file IDs were assigned.

            List <Topic> sortedTopics = new List <Topic>(topics.Count);

            do
            {
                var lowestFile      = files.FromID(topics[0].FileID);
                var lowestFileIndex = 0;
                var lastCheckedID   = lowestFile.ID;

                for (int i = 1; i < topics.Count; i++)
                {
                    if (topics[i].FileID != lastCheckedID)
                    {
                        var file = files.FromID(topics[i].FileID);

                        if (Path.Compare(file.FileName, lowestFile.FileName) < 0)
                        {
                            lowestFile      = file;
                            lowestFileIndex = i;
                        }

                        lastCheckedID = file.ID;
                    }
                }

                int count = 0;
                for (int i = lowestFileIndex; i < topics.Count && topics[i].FileID == lowestFile.ID; i++)
                {
                    count++;
                }

                sortedTopics.AddRange(topics.GetRange(lowestFileIndex, count));
                topics.RemoveRange(lowestFileIndex, count);
            }while (topics.Count > 0);


            // The topics are all in sortedTopics now, and "topics" is empty.  For clarity going forward, let's rename sortedTopics
            // to remainingTopics, since we have to move them back into topics now.

            List <Topic> remainingTopics = sortedTopics;
            sortedTopics = null;              // for safety


            // Find the best topic to serve as the class definition.

            Topic bestDefinition      = remainingTopics[0];
            int   bestDefinitionIndex = 0;

            for (int i = 1; i < remainingTopics.Count; i++)
            {
                Topic topic = remainingTopics[i];

                if (topic.DefinesClass &&
                    builder.EngineInstance.Links.IsBetterClassDefinition(bestDefinition, topic))
                {
                    bestDefinition      = topic;
                    bestDefinitionIndex = i;
                }
            }


            // Copy the best definition in and everything that follows it in the file.  That will serve as the base for merging.

            int bestDefinitionTopicCount = 1;

            for (int i = bestDefinitionIndex + 1; i < remainingTopics.Count && remainingTopics[i].FileID == bestDefinition.FileID; i++)
            {
                bestDefinitionTopicCount++;
            }

            topics.AddRange(remainingTopics.GetRange(bestDefinitionIndex, bestDefinitionTopicCount));
            remainingTopics.RemoveRange(bestDefinitionIndex, bestDefinitionTopicCount);


            // Make sure the first topic isn't embedded so that classes documented in lists still appear correctly.

            if (topics[0].IsEmbedded)
            {
                topics[0]            = topics[0].Duplicate();
                topics[0].IsEmbedded = false;
            }


            // Delete all the other topics that define the class.  We don't need them anymore.

            for (int i = 0; i < remainingTopics.Count; /* don't auto increment */)
            {
                if (remainingTopics[i].DefinesClass)
                {
                    remainingTopics.RemoveAt(i);
                }
                else
                {
                    i++;
                }
            }


            // Merge any duplicate topics into the list.  This is used for things like header vs. source definitions in C++.

            // First we go through the primary topic list to handle removing list topics and merging individual topics into list
            // topics in the remaining topic list.  Everything else will be handled when iterating through the remaining topic list.

            int topicIndex = 0;
            while (topicIndex < topics.Count)
            {
                var topic = topics[topicIndex];

                // Ignore group topics
                if (topic.IsGroup)
                {
                    topicIndex++;
                    continue;
                }

                int embeddedTopicCount = CountEmbeddedTopics(topics, topicIndex);


                // We don't need to worry about enums until we do remaining topics.

                if (topic.IsEnum)
                {
                    topicIndex += 1 + embeddedTopicCount;
                }


                // If it's not an enum and it's a standalone topic see if it will merge with an embedded topic in the remaining topic
                // list.  We don't have to worry about merging with standalone topics until we do the remaining topic list.

                else if (embeddedTopicCount == 0)
                {
                    int duplicateIndex = FindDuplicateTopic(topic, remainingTopics, builder);

                    if (duplicateIndex == -1)
                    {
                        topicIndex++;
                    }
                    else if (remainingTopics[duplicateIndex].IsEmbedded &&
                             engineInstance.Links.IsBetterTopicDefinition(topic, remainingTopics[duplicateIndex]))
                    {
                        topics.RemoveAt(topicIndex);
                    }
                    else
                    {
                        topicIndex++;
                    }
                }


                // If it's not an enum and we're at a list topic, only remove it if EVERY member has a better definition in the other
                // list.  We can't pluck them out individually.  If even one is documented here that isn't documented elsewhere we
                // keep the entire thing in even if that leads to some duplicates.

                else
                {
                    bool allHaveBetterMatches = true;

                    for (int i = 0; i < embeddedTopicCount; i++)
                    {
                        Topic embeddedTopic  = topics[topicIndex + 1 + i];
                        int   duplicateIndex = FindDuplicateTopic(embeddedTopic, remainingTopics, builder);

                        if (duplicateIndex == -1 ||
                            engineInstance.Links.IsBetterTopicDefinition(embeddedTopic, remainingTopics[duplicateIndex]) == false)
                        {
                            allHaveBetterMatches = false;
                            break;
                        }
                    }

                    if (allHaveBetterMatches)
                    {
                        topics.RemoveRange(topicIndex, 1 + embeddedTopicCount);
                    }
                    else
                    {
                        topicIndex += 1 + embeddedTopicCount;
                    }
                }
            }


            // Now do a more comprehensive merge of the remaining topics into the primary topic list.

            int remainingTopicIndex = 0;
            while (remainingTopicIndex < remainingTopics.Count)
            {
                var remainingTopic = remainingTopics[remainingTopicIndex];

                // Ignore group topics
                if (remainingTopic.IsGroup)
                {
                    remainingTopicIndex++;
                    continue;
                }

                int embeddedTopicCount = CountEmbeddedTopics(remainingTopics, remainingTopicIndex);


                // If we're merging enums, the one with the most embedded topics (documented values) wins.  In practice one
                // should be documented and one shouldn't be, so this will be any number versus zero.

                if (remainingTopic.IsEnum)
                {
                    int duplicateIndex = FindDuplicateTopic(remainingTopic, topics, builder);

                    if (duplicateIndex == -1)
                    {
                        remainingTopicIndex += 1 + embeddedTopicCount;
                    }
                    else
                    {
                        int duplicateEmbeddedTopicCount = CountEmbeddedTopics(topics, duplicateIndex);

                        if (embeddedTopicCount > duplicateEmbeddedTopicCount ||
                            (embeddedTopicCount == duplicateEmbeddedTopicCount &&
                             engineInstance.Links.IsBetterTopicDefinition(remainingTopic, topics[duplicateIndex]) == false))
                        {
                            topics.RemoveRange(duplicateIndex, 1 + duplicateEmbeddedTopicCount);
                            topics.InsertRange(duplicateIndex, remainingTopics.GetRange(remainingTopicIndex, 1 + embeddedTopicCount));
                        }

                        remainingTopics.RemoveRange(remainingTopicIndex, 1 + embeddedTopicCount);
                    }
                }


                // If it's not an enum and it's a standalone topic the one with the best score wins.

                else if (embeddedTopicCount == 0)
                {
                    int duplicateIndex = FindDuplicateTopic(remainingTopic, topics, builder);

                    if (duplicateIndex == -1)
                    {
                        remainingTopicIndex++;
                    }
                    else if (engineInstance.Links.IsBetterTopicDefinition(remainingTopic, topics[duplicateIndex]) == false)
                    {
                        if (topics[duplicateIndex].IsEmbedded)
                        {
                            // Just leave them both in
                            remainingTopicIndex++;
                        }
                        else
                        {
                            topics[duplicateIndex] = remainingTopic;
                            remainingTopics.RemoveAt(remainingTopicIndex);
                        }
                    }
                    else
                    {
                        remainingTopics.RemoveAt(remainingTopicIndex);
                    }
                }


                // If it's not an enum and we're at a list topic, only remove it if EVERY member has a better definition in the other
                // list.  We can't pluck them out individually.  If even one is documented here that isn't documented elsewhere we
                // keep the entire thing in even if that leads to some duplicates.

                else
                {
                    bool allHaveBetterMatches = true;

                    for (int i = 0; i < embeddedTopicCount; i++)
                    {
                        Topic embeddedTopic  = remainingTopics[remainingTopicIndex + 1 + i];
                        int   duplicateIndex = FindDuplicateTopic(embeddedTopic, topics, builder);

                        if (duplicateIndex == -1 ||
                            engineInstance.Links.IsBetterTopicDefinition(embeddedTopic, topics[duplicateIndex]) == false)
                        {
                            allHaveBetterMatches = false;
                            break;
                        }
                    }

                    if (allHaveBetterMatches)
                    {
                        remainingTopics.RemoveRange(remainingTopicIndex, 1 + embeddedTopicCount);
                    }
                    else
                    {
                        remainingTopicIndex += 1 + embeddedTopicCount;
                    }
                }
            }


            // Generate groups from the topic lists.

            // Start at 1 to skip the class topic.
            // Don't group by file ID because topics from other files may have been combined into the list.
            var groupedTopics = GetTopicGroups(topics, 1, false);

            var groupedRemainingTopics = GetTopicGroups(remainingTopics);


            // Delete any empty groups.  We do this on the main group list too for consistency.

            for (int i = 0; i < groupedTopics.Groups.Count; /* don't auto increment */)
            {
                if (groupedTopics.Groups[i].IsEmpty)
                {
                    groupedTopics.RemoveGroupAndTopics(i);
                }
                else
                {
                    i++;
                }
            }

            for (int i = 0; i < groupedRemainingTopics.Groups.Count; /* don't auto increment */)
            {
                if (groupedRemainingTopics.Groups[i].IsEmpty)
                {
                    groupedRemainingTopics.RemoveGroupAndTopics(i);
                }
                else
                {
                    i++;
                }
            }


            // Now merge groups.  If any remaining groups match the title of an existing group, move its members to
            // the end of the existing group.

            int remainingGroupIndex = 0;
            while (remainingGroupIndex < groupedRemainingTopics.Groups.Count)
            {
                var  remainingGroup = groupedRemainingTopics.Groups[remainingGroupIndex];
                bool merged         = false;

                if (remainingGroup.Title != null)
                {
                    for (int groupIndex = 0; groupIndex < groupedTopics.Groups.Count; groupIndex++)
                    {
                        if (groupedTopics.Groups[groupIndex].Title == remainingGroup.Title)
                        {
                            groupedRemainingTopics.MergeGroupInto(remainingGroupIndex, groupedTopics, groupIndex);
                            merged = true;
                            break;
                        }
                    }
                }

                if (merged == false)
                {
                    remainingGroupIndex++;
                }
            }


            // Move any groups with titles that didn't match to the other list.  We insert it after the last group
            // of the same dominant type so function groups stay with other function groups, variable groups stay
            // with other variable groups, etc.

            remainingGroupIndex = 0;
            while (remainingGroupIndex < groupedRemainingTopics.Groups.Count)
            {
                var remainingGroup = groupedRemainingTopics.Groups[remainingGroupIndex];

                if (remainingGroup.Title != null)
                {
                    int bestMatchIndex = -1;

                    // Walk the list backwards because we want it to be after the last group of the type, not the first.
                    for (int i = groupedTopics.Groups.Count - 1; i >= 0; i--)
                    {
                        if (groupedTopics.Groups[i].DominantTypeID == remainingGroup.DominantTypeID)
                        {
                            bestMatchIndex = i;
                            break;
                        }
                    }

                    if (bestMatchIndex == -1)
                    {
                        // Just add them to the end if nothing matches.
                        groupedRemainingTopics.MoveGroupTo(remainingGroupIndex, groupedTopics);
                    }
                    else
                    {
                        groupedRemainingTopics.MoveGroupTo(remainingGroupIndex, groupedTopics, bestMatchIndex + 1);
                    }
                }
                else
                {
                    remainingGroupIndex++;
                }
            }


            // Now we're left with topics that are not in groups.  See if the list contains any titled groups at all.

            bool groupsWithTitles = false;

            foreach (var group in groupedTopics.Groups)
            {
                if (group.Title != null)
                {
                    groupsWithTitles = true;
                    break;
                }
            }


            // If there's no titles we can just append the remaining topics as is.

            if (groupsWithTitles == false)
            {
                groupedTopics.Topics.AddRange(groupedRemainingTopics.Topics);
            }


            // If there are titled groups, see if we can add them to the end of existing groups.  However, only do
            // this if TitleMatchesType is set.  It's okay to put random functions into the group "Functions" but
            // not into something more specific.  If there aren't appropriate groups to do this with, create new ones.

            else
            {
                // We don't care about the remaining groups anymore so we can just work directly on the topics.
                remainingTopics        = groupedRemainingTopics.Topics;
                groupedRemainingTopics = null;                  // for safety

                while (remainingTopics.Count > 0)
                {
                    int type = remainingTopics[0].CommentTypeID;
                    int matchingGroupIndex = -1;

                    for (int i = groupedTopics.Groups.Count - 1; i >= 0; i--)
                    {
                        if (groupedTopics.Groups[i].DominantTypeID == type &&
                            groupedTopics.Groups[i].TitleMatchesType)
                        {
                            matchingGroupIndex = i;
                            break;
                        }
                    }

                    // Create a new group if there's no existing one we can use.
                    if (matchingGroupIndex == -1)
                    {
                        Topic generatedTopic = new Topic(builder.EngineInstance.CommentTypes);
                        generatedTopic.TopicID       = 0;
                        generatedTopic.Title         = builder.EngineInstance.CommentTypes.FromID(type).PluralDisplayName;
                        generatedTopic.Symbol        = SymbolString.FromPlainText_NoParameters(generatedTopic.Title);
                        generatedTopic.ClassString   = topics[0].ClassString;
                        generatedTopic.ClassID       = topics[0].ClassID;
                        generatedTopic.CommentTypeID = builder.EngineInstance.CommentTypes.IDFromKeyword("group");
                        generatedTopic.FileID        = topics[0].FileID;
                        generatedTopic.LanguageID    = topics[0].LanguageID;

                        // In case there's nothing that defines the "group" keyword.
                        if (generatedTopic.CommentTypeID != 0)
                        {
                            groupedTopics.Topics.Add(generatedTopic);
                            groupedTopics.CreateGroup(groupedTopics.Topics.Count - 1, 1);
                        }

                        matchingGroupIndex = groupedTopics.Groups.Count - 1;
                    }

                    for (int i = 0; i < remainingTopics.Count; /* don't auto increment */)
                    {
                        var remainingTopic = remainingTopics[i];

                        // Need to check IsEmbedded because enums values will not have the same type as their parent.
                        if (remainingTopic.CommentTypeID == type || remainingTopic.IsEmbedded)
                        {
                            groupedTopics.AppendToGroup(matchingGroupIndex, remainingTopic);
                            remainingTopics.RemoveAt(i);
                        }
                        else
                        {
                            i++;
                        }
                    }
                }
            }
        }
Exemplo n.º 9
0
        public override string OutputOf(IList <string> commands)
        {
            StringBuilder output           = new StringBuilder();
            bool          lastWasLineBreak = true;

            Topic topic = new Topic(EngineInstance.CommentTypes);

            topic.LanguageID    = EngineInstance.Languages.FromName("C#").ID;
            topic.CommentTypeID = EngineInstance.CommentTypes.FromKeyword("Function").ID;

            Link link = new Engine.Links.Link();

            link.LanguageID = EngineInstance.Languages.FromName("C#").ID;
            link.Type       = Engine.Links.LinkType.NaturalDocs;
            bool linkIsPlainText = true;

            string show = "all";

            for (int i = 0; i < commands.Count; i++)
            {
                string command = commands[i];
                if (command.EndsWith(";"))
                {
                    command = command.Substring(0, command.Length - 1).TrimEnd();
                }

                Match  match = GetPropertyRegex.Match(command);
                string target, property, value, valueString;

                Match addPrefixMatch     = null;
                Match replacePrefixMatch = null;

                if (match.Success)
                {
                    target   = match.Groups[1].ToString().ToLower();
                    property = match.Groups[2].ToString().ToLower();
                    value    = match.Groups[3].ToString();

                    if (value.Length >= 2 && value[0] == '"' && value[value.Length - 1] == '"')
                    {
                        valueString = value.Substring(1, value.Length - 2).Trim();
                    }
                    else
                    {
                        value       = value.ToLower();
                        valueString = null;
                    }
                }
                else
                {
                    addPrefixMatch     = AddPrefixRegex.Match(command);
                    replacePrefixMatch = ReplacePrefixRegex.Match(command);

                    if (addPrefixMatch.Success || replacePrefixMatch.Success)
                    {
                        target      = "link";
                        property    = "using";
                        value       = null;
                        valueString = null;
                    }
                    else
                    {
                        target      = null;
                        property    = null;
                        value       = null;
                        valueString = null;
                    }
                }

                var lcCommand = command.ToLower();

                try
                {
                    if (command == "")
                    {
                        if (!lastWasLineBreak)
                        {
                            output.AppendLine();
                            lastWasLineBreak = true;
                        }
                    }
                    else if (command.StartsWith("//"))
                    {
                        output.AppendLine(command);
                        lastWasLineBreak = false;
                    }


                    // Topic properties

                    else if (target == "topic")
                    {
                        if (property == "languagename")
                        {
                            if (valueString == null)
                            {
                                throw new Exception("Topic.LanguageName must be set to a string value.");
                            }

                            var language = EngineInstance.Languages.FromName(valueString);

                            if (language == null)
                            {
                                throw new Exception("\"" + valueString + "\" is not recognized as a language.");
                            }

                            topic.LanguageID = language.ID;
                        }
                        else if (property == "keyword")
                        {
                            if (valueString == null)
                            {
                                throw new Exception("Topic.Keyword must be set to a string value.");
                            }

                            var commentType = EngineInstance.CommentTypes.FromKeyword(valueString);

                            if (commentType == null)
                            {
                                throw new Exception("\"" + valueString + "\" is not recognized as a keyword.");
                            }

                            topic.CommentTypeID = commentType.ID;
                        }
                        else if (property == "title")
                        {
                            if (valueString == null)
                            {
                                throw new Exception("Topic.Title must be set to a string value.");
                            }
                            if (valueString == "")
                            {
                                throw new Exception("Topic.Title cannot be set to an empty string.");
                            }

                            topic.Title = valueString;
                        }
                        else if (property == "body")
                        {
                            if (value == "null")
                            {
                                topic.Body = null;
                            }
                            else if (valueString == null)
                            {
                                throw new Exception("Topic.Body must be set to null or a string value.");
                            }
                            else
                            {
                                topic.Body = valueString;
                            }
                        }
                        else if (property == "prototype")
                        {
                            if (value == "null")
                            {
                                topic.Prototype = null;
                            }
                            else if (valueString == null)
                            {
                                throw new Exception("Topic.Prototype must be set to null or a string value.");
                            }
                            else
                            {
                                topic.Prototype = valueString;
                            }
                        }
                        else if (property == "scope")
                        {
                            if (value == "null")
                            {
                                ContextString temp = topic.PrototypeContext;
                                temp.Scope             = new SymbolString();
                                topic.PrototypeContext = temp;
                            }
                            else if (valueString == null)
                            {
                                throw new Exception("Topic.Scope must be set to null or a string value.");
                            }
                            else
                            {
                                ContextString temp = topic.PrototypeContext;
                                temp.Scope             = SymbolString.FromPlainText_NoParameters(valueString);
                                topic.PrototypeContext = temp;
                            }
                        }
                        else
                        {
                            throw new Exception("\"" + property + "\" is not a recognized Topic property.");
                        }

                        // Leave lastWasLineBreak alone since we're not generating output.
                    }


                    // Link properties

                    else if (target == "link")
                    {
                        if (property == "languagename")
                        {
                            if (valueString == null)
                            {
                                throw new Exception("Link.LanguageName must be set to a string value.");
                            }

                            var language = EngineInstance.Languages.FromName(valueString);

                            if (language == null)
                            {
                                throw new Exception("\"" + valueString + "\" is not recognized as a language.");
                            }

                            link.LanguageID = language.ID;
                        }
                        else if (property == "type")
                        {
                            if (valueString == null)
                            {
                                throw new Exception("Link.Type must be set to a string value.");
                            }

                            string lcValueString = valueString.ToLower();

                            if (lcValueString == "naturaldocs" || lcValueString == "natural docs")
                            {
                                link.Type = LinkType.NaturalDocs;
                            }
                            else if (lcValueString == "type")
                            {
                                link.Type = LinkType.Type;
                            }
                            else if (lcValueString == "classparent" || lcValueString == "class parent")
                            {
                                link.Type = LinkType.ClassParent;
                            }
                            else
                            {
                                throw new Exception("\"" + valueString + "\" is not recognized as a link type.");
                            }
                        }
                        else if (property == "text")
                        {
                            if (valueString == null)
                            {
                                throw new Exception("Link.Text must be set to a string value");
                            }
                            if (valueString == "")
                            {
                                throw new Exception("Link.Text cannot be set to an empty string.");
                            }

                            link.TextOrSymbol = valueString;
                            linkIsPlainText   = true;
                        }
                        else if (property == "scope")
                        {
                            if (value == "null")
                            {
                                ContextString temp = link.Context;
                                temp.Scope   = new SymbolString();
                                link.Context = temp;
                            }
                            else if (valueString == null)
                            {
                                throw new Exception("Link.Scope must be set to null or a string value.");
                            }
                            else if (valueString == "")
                            {
                                throw new Exception("Link.Scope cannot be set to an empty string.");
                            }
                            else
                            {
                                ContextString temp = link.Context;
                                temp.Scope   = SymbolString.FromPlainText_NoParameters(valueString);
                                link.Context = temp;
                            }
                        }
                        else if (property == "using")
                        {
                            if (value == "null")
                            {
                                ContextString temp = link.Context;
                                temp.ClearUsingStatements();
                                link.Context = temp;
                            }
                            else if (addPrefixMatch != null && addPrefixMatch.Success)
                            {
                                string op  = addPrefixMatch.Groups[1].ToString();
                                string add = addPrefixMatch.Groups[2].ToString();

                                ContextString temp = link.Context;

                                if (op == "=")
                                {
                                    temp.ClearUsingStatements();
                                }

                                temp.AddUsingStatement(
                                    UsingString.FromParameters(
                                        UsingString.UsingType.AddPrefix,
                                        SymbolString.FromPlainText_NoParameters(add)
                                        )
                                    );

                                link.Context = temp;
                            }
                            else if (replacePrefixMatch != null && replacePrefixMatch.Success)
                            {
                                string op     = replacePrefixMatch.Groups[1].ToString();
                                string remove = replacePrefixMatch.Groups[2].ToString();
                                string add    = replacePrefixMatch.Groups[3].ToString();

                                ContextString temp = link.Context;

                                if (op == "=")
                                {
                                    temp.ClearUsingStatements();
                                }

                                temp.AddUsingStatement(
                                    UsingString.FromParameters(
                                        UsingString.UsingType.ReplacePrefix,
                                        SymbolString.FromPlainText_NoParameters(add),
                                        SymbolString.FromPlainText_NoParameters(remove)
                                        )
                                    );

                                link.Context = temp;
                            }
                            else
                            {
                                throw new Exception("\"" + command + "\" is not a recognized Link.Using statement.");
                            }
                        }
                        else
                        {
                            throw new Exception("\"" + property + "\" is not recognized as a link property.");
                        }
                        // Leave lastWasLineBreak alone since we're not generating output.
                    }


                    // Show

                    else if (lcCommand.StartsWith("show"))
                    {
                        show = lcCommand.Substring(4);
                    }


                    // Score

                    else if (lcCommand == "score")
                    {
                        // Validate fields

                        if (topic.Title == null)
                        {
                            throw new Exception("You didn't set Topic.Title.");
                        }
                        if (link.TextOrSymbol == null)
                        {
                            throw new Exception("You didn't set Link.Text.");
                        }


                        // Calculate fields

                        string       parametersString;
                        SymbolString topicSymbol = SymbolString.FromPlainText(topic.Title, out parametersString);

                        var commentType = EngineInstance.CommentTypes.FromID(topic.CommentTypeID);

                        if (commentType.Scope == Engine.CommentTypes.CommentType.ScopeValue.Normal &&
                            topic.PrototypeContext.ScopeIsGlobal == false)
                        {
                            topicSymbol = topic.PrototypeContext.Scope + topicSymbol;
                        }

                        topic.Symbol = topicSymbol;

                        if (link.Type == LinkType.Type || link.Type == LinkType.ClassParent)
                        {
                            if (linkIsPlainText)
                            {
                                SymbolString linkSymbol = SymbolString.FromPlainText_NoParameters(link.TextOrSymbol);
                                link.TextOrSymbol = linkSymbol.ToString();
                                link.EndingSymbol = linkSymbol.EndingSymbol;
                                linkIsPlainText   = false;
                            }
                        }
                        else
                        {
                            string       ignore;
                            SymbolString linkSymbol = SymbolString.FromPlainText(link.TextOrSymbol, out ignore);
                            link.EndingSymbol = linkSymbol.EndingSymbol;
                        }


                        // Show topic

                        if (!lastWasLineBreak)
                        {
                            output.AppendLine();
                        }

                        var topicLanguage = EngineInstance.Languages.FromID(topic.LanguageID);
                        commentType = EngineInstance.CommentTypes.FromID(topic.CommentTypeID);

                        output.AppendLine(topicLanguage.Name + " " + commentType.Name + " Topic: " + topic.Title);
                        output.AppendLine("   Symbol: " + topic.Symbol.FormatWithSeparator('.'));

                        if (topic.TitleParameters != null)
                        {
                            output.AppendLine("   Title Parameters: " + topic.TitleParameters.ToString().Replace(Engine.Symbols.ParameterString.SeparatorChar, ','));
                        }
                        if (topic.PrototypeParameters != null)
                        {
                            output.AppendLine("   Prototype Parameters: " + topic.PrototypeParameters.ToString().Replace(Engine.Symbols.ParameterString.SeparatorChar, ','));
                        }
                        if (topic.Prototype != null)
                        {
                            output.AppendLine("   Prototype: " + topic.Prototype);
                        }
                        if (topic.Body != null)
                        {
                            output.AppendLine("   Body: " + topic.Body);
                        }

                        output.AppendLine();


                        // Show link

                        var linkLanguage = EngineInstance.Languages.FromID(link.LanguageID);

                        output.AppendLine(linkLanguage.Name + " " + link.Type + " Link: " + link.TextOrSymbol.Replace(Engine.Symbols.SymbolString.SeparatorChar, '*'));

                        if (link.Context.ScopeIsGlobal)
                        {
                            output.AppendLine("   Scope: Global");
                        }
                        else
                        {
                            output.AppendLine("   Scope: " + link.Context.Scope.FormatWithSeparator('.'));
                        }

                        var usingStatements = link.Context.GetUsingStatements();

                        if (usingStatements != null)
                        {
                            foreach (var usingStatement in usingStatements)
                            {
                                if (usingStatement.Type == UsingString.UsingType.AddPrefix)
                                {
                                    output.AppendLine("   Using: Add Prefix " + usingStatement.PrefixToAdd.FormatWithSeparator('.'));
                                }
                                else if (usingStatement.Type == UsingString.UsingType.ReplacePrefix)
                                {
                                    output.AppendLine("   Using: Replace Prefix " + usingStatement.PrefixToRemove.FormatWithSeparator('.') +
                                                      " with " + usingStatement.PrefixToAdd.FormatWithSeparator('.'));
                                }
                                else
                                {
                                    throw new NotImplementedException("Unexpected using type " + usingStatement.Type);
                                }
                            }
                        }

                        output.AppendLine();


                        // Show score

                        List <Engine.Links.LinkInterpretation> interpretations = null;

                        if (link.Type == LinkType.NaturalDocs)
                        {
                            string ignore;
                            interpretations = EngineInstance.Comments.NaturalDocsParser.LinkInterpretations(link.TextOrSymbol,
                                                                                                            Engine.Comments.Parsers.NaturalDocs.LinkInterpretationFlags.AllowNamedLinks |
                                                                                                            Engine.Comments.Parsers.NaturalDocs.LinkInterpretationFlags.AllowPluralsAndPossessives,
                                                                                                            out ignore);
                        }

                        long score = EngineInstance.CodeDB.ScoreLink(link, topic, 0, interpretations);

                        if (score <= 0)
                        {
                            output.AppendLine("☓☓☓ No Match ☓☓☓");
                        }
                        else
                        {
                            output.Append("Match score:");

                            if (show.Contains("all") || show.Contains("rawscore"))
                            {
                                string scoreString = score.ToString("X16");
                                output.Append(" " + scoreString.Substring(0, 4) + " " + scoreString.Substring(4, 4) +
                                              " " + scoreString.Substring(8, 4) + " " + scoreString.Substring(12, 4));
                            }

                            output.AppendLine();

                            // DEPENDENCY: This code depends on the format generated by Engine.CodeDB.ScoreLink().

                            // Format:
                            // 0LCETPPP PPPPPPPP PPPPPPPP PSSSSSSS SSSIIIII IBFFFFFF Rbbbbbbb brrrrrr1

                            // L - Whether the topic matches the link's language.
                            // C - Whether the topic and link's capitalization match if it matters to the language.
                            // E - Whether the text is an exact match with no plural or possessive conversions applied.
                            // T - Whether the link parameters exactly match the topic title parameters.
                            // P - How well the parameters match.
                            // S - How high on the scope list the symbol match is.
                            // I - How high on the interpretation list (named/plural/possessive) the match is.
                            // B - Whether the topic has a body
                            // F - How high on the list of topics that define the same symbol in the same file this is.
                            // R - Whether the topic has a prototype.
                            // b - The length of the body divided by 16.
                            // r - The length of the prototype divided by 16.

                            long LValue = (score & 0x4000000000000000) >> 62;
                            long CValue = (score & 0x2000000000000000) >> 61;
                            long EValue = (score & 0x1000000000000000) >> 60;
                            long TValue = (score & 0x0800000000000000) >> 59;
                            long PValue = (score & 0x07FFFF8000000000) >> 39;
                            long SValue = (score & 0x0000007FE0000000) >> 29;
                            long IValue = (score & 0x000000001F800000) >> 23;
                            long BValue = (score & 0x0000000000400000) >> 22;
                            long FValue = (score & 0x00000000003F0000) >> 16;
                            long RValue = (score & 0x0000000000008000) >> 15;
                            long bValue = (score & 0x0000000000007F80) >> 7;
                            long rValue = (score & 0x000000000000007E) >> 1;

                            if (show.Contains("all") || show.Contains("language"))
                            {
                                output.AppendLine("   " + (LValue == 1 ? "☒" : "☐") + " - Language");
                            }
                            if (show.Contains("all") || show.Contains("capitalization"))
                            {
                                output.AppendLine("   " + (CValue == 1 ? "☒" : "☐") + " - Capitalization");
                            }
                            if (show.Contains("all") || show.Contains("interpretation"))
                            {
                                output.AppendLine("   " + (EValue == 1 ? "☒" : "☐") + " - Exact text");
                            }

                            if (show.Contains("all") || show.Contains("parameters"))
                            {
                                output.AppendLine("   " + (TValue == 1 ? "☒" : "☐") + " - Topic title parameters");

                                output.Append("   ");
                                for (int shift = 18; shift >= 0; shift -= 2)
                                {
                                    long individualPValue = PValue >> shift;
                                    individualPValue &= 0x0000000000000003;

                                    switch (individualPValue)
                                    {
                                    case 3:
                                        output.Append("☒");
                                        break;

                                    case 2:
                                        output.Append("↑");
                                        break;

                                    case 1:
                                        output.Append("↓");
                                        break;

                                    case 0:
                                        output.Append("☐");
                                        break;
                                    }
                                }
                                output.AppendLine(" - Parameters");
                            }

                            if (show.Contains("all") || show.Contains("scope"))
                            {
                                output.AppendLine("   " + (1023 - SValue) + " - Scope index");
                                output.AppendLine("      (" + SValue + " score)");
                            }
                            if (show.Contains("all") || show.Contains("interpretation"))
                            {
                                output.AppendLine("   " + (63 - IValue) + " - Interpretation index");
                                output.AppendLine("      (" + IValue + " score)");
                            }

                            if (show.Contains("all") || show.Contains("body"))
                            {
                                output.AppendLine("   " + (BValue == 1 ? "☒" : "☐") + " - Body");
                                if (BValue == 1)
                                {
                                    output.Append("      (" + bValue + " score, " + (bValue * 16));

                                    if (bValue == 0xFF)
                                    {
                                        output.Append('+');
                                    }
                                    else
                                    {
                                        output.Append("-" + ((bValue * 16) + 15));
                                    }

                                    output.AppendLine(" characters)");
                                }
                            }

                            if (show.Contains("all") || show.Contains("prototype"))
                            {
                                output.AppendLine("   " + (RValue == 1 ? "☒" : "☐") + " - Prototype");
                                if (RValue == 1)
                                {
                                    output.Append("      (" + rValue + " score, " + (rValue * 16));

                                    if (rValue == 0x3F)
                                    {
                                        output.Append('+');
                                    }
                                    else
                                    {
                                        output.Append("-" + ((rValue * 16) + 15));
                                    }

                                    output.AppendLine(" characters)");
                                }
                            }

                            if (show.Contains("all") || show.Contains("samesymbol"))
                            {
                                output.AppendLine("   " + (63 - FValue) + " - Same symbol in same file index");
                                output.AppendLine("      (" + FValue + " score)");
                            }
                        }

                        output.AppendLine();
                        lastWasLineBreak = true;
                    }

                    else
                    {
                        throw new Exception("Unknown command " + command);
                    }
                }
                catch (Exception e)
                {
                    output.AppendLine("Command: " + command);
                    output.AppendLine("Exception: " + e.Message);
                    output.AppendLine("(" + e.GetType().ToString() + ")");
                    lastWasLineBreak = false;
                }
            }

            return(output.ToString());
        }