/* 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(); } } }
/* 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); }
// 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); } }
/* 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); } } }
/* 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(); } } }
// 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); }
// 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; } }
// 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++; } } } } }
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()); }