/* Function: MergeCommentTypes * * Merges two <ConfigFiles.TextFiles> into a new one, putting all the comment types into one list and applying any alter * entries. This does NOT cover keywords, ignored keywords, or tags; those will be blank in the result. Returns the new * list and whether it was successful. * * Any errors will be added to errorList, such as defining a duplicate entry that doesn't use alter, or an alter entry for a * non-existent comment type. All alter entries will be applied, including any appearing in the base config, so there will * only be non-alter entries in the returned list. */ protected bool MergeCommentTypes(ConfigFiles.TextFile baseConfig, ConfigFiles.TextFile overridingConfig, out ConfigFiles.TextFile combinedConfig, Errors.ErrorList errorList) { combinedConfig = new ConfigFiles.TextFile(); // We merge the base config into the empty config instead of just copying it so any alter entries it has are applied if (!MergeCommentTypesInto(ref combinedConfig, baseConfig, errorList) || !MergeCommentTypesInto(ref combinedConfig, overridingConfig, errorList)) { combinedConfig = null; return(false); } return(true); }
/* Function: TouchUp_Stage2 * Applies some minor improvements to the <ConfigFiles.TextFile>, such as making sure the capitalization of Alter Topic Type * and [Language] Keywords match the original definition. Assumes everything is valid, meaning all Alter Topic Type entries * have corresponding entries in finalConfig and all [Language] Keyword entries have corresponding languages in * <Languages.Manager>. */ protected void TouchUp_Stage2(ref ConfigFiles.TextFile textConfig, Config finalConfig) { if (textConfig.HasCommentTypes) { foreach (var commentType in textConfig.CommentTypes) { // Fix "Alter Comment Type: [name]" capitalization if (commentType.AlterType) { var originalType = finalConfig.CommentTypeFromName(commentType.Name); commentType.FixNameCapitalization(originalType.Name); // We don't also check to see if the comment type we're altering exists in the same file and merge their // definitions into one. Why? Consider this: // // Comment Type: Comment Type A // Keyword: Keyword A // // Comment Type: Comment Type B // Keyword: Keyword B // // Alter Comment Type: Comment Type A // Keyword: Keyword B // // Keyword B should be part of Comment Type A. However, if we merged the definitions it would appear // first and be overridden by Comment Type B. So we just leave the two comment type entries for A instead. } if (commentType.HasKeywordGroups) { foreach (var keywordGroup in commentType.KeywordGroups) { // Fix "[Language] Keywords" capitalization if (keywordGroup.IsLanguageSpecific) { var originalLanguage = EngineInstance.Languages.FromName(keywordGroup.LanguageName); keywordGroup.LanguageName = originalLanguage.Name; } } } } } }
/* Function: MergeIgnoredKeywordsInto * Merges the ignored keywords from the <ConfigFiles.TextFile> into a <StringSet>. */ protected void MergeIgnoredKeywordsInto(ref StringSet ignoredKeywords, ConfigFiles.TextFile textConfig) { if (textConfig.HasIgnoredKeywords) { foreach (var ignoredKeywordGroup in textConfig.IgnoredKeywordGroups) { foreach (var ignoredKeywordDefinition in ignoredKeywordGroup.KeywordDefinitions) { ignoredKeywords.Add(ignoredKeywordDefinition.Keyword); if (ignoredKeywordDefinition.HasPlural) { ignoredKeywords.Add(ignoredKeywordDefinition.Plural); } } } } }
// Group: Initialization Support Functions // __________________________________________________________________________ // // These functions are only used by <Start_Stage1()> and <Start_Stage2()>. // /* Function: ValidateCommentTypes * Validates all the comment type settings in a <ConfigFiles.TextFile>. Returns whether it is valid, and adds any errors it * finds to errorList. */ protected bool ValidateCommentTypes(ConfigFiles.TextFile configFile, Errors.ErrorList errorList) { bool success = true; if (configFile.HasCommentTypes) { foreach (var commentType in configFile.CommentTypes) { if (!ValidateCommentType(commentType, errorList)) { success = false; // Continue anyway so we can report the errors in all of them. } } } return(success); }
/* Function: MergeCommentTypesInto * Merges the comment types of the second <ConfigFiles.TextFile> into the first, adding new types and applying any * alter entries. This does NOT merge keywords, ignored keywords, or tags. The base config will be changed, even if * there are errors. Returns false if there were any errors and adds them to errorList. */ protected bool MergeCommentTypesInto(ref ConfigFiles.TextFile baseConfig, ConfigFiles.TextFile overridingConfig, Errors.ErrorList errorList) { bool success = true; if (overridingConfig.HasCommentTypes) { foreach (var overridingCommentType in overridingConfig.CommentTypes) { var matchingCommentType = baseConfig.FindCommentType(overridingCommentType.Name); if (matchingCommentType != null) { if (overridingCommentType.AlterType == false) { errorList.Add(Locale.Get("NaturalDocs.Engine", "Comments.txt.CommentTypeAlreadyExists(name)", overridingCommentType.Name), overridingCommentType.PropertyLocation); success = false; } else { MergeCommentTypeInto(ref matchingCommentType, overridingCommentType); } } else // no match { if (overridingCommentType.AlterType == true) { errorList.Add(Locale.Get("NaturalDocs.Engine", "Comments.txt.AlteredCommentTypeDoesntExist(name)", overridingCommentType.Name), overridingCommentType.NamePropertyLocation); success = false; } else { baseConfig.AddCommentType(overridingCommentType.Duplicate()); } } } } return(success); }
/* Function: MergeKeywordsInto_Stage2 * Merges the keywords from the <ConfigFiles.TextFile> into the <Config>, returning whether it was successful. It * assumes all <ConfigFiles.TextCommentTypes> in textConfig have corresponding <CommentTypes> in outputConfig. * Any errors will be added to errorList, such as having a language-specific keyword that doesn't match a name in * <Languages.Manager>. */ protected bool MergeKeywordsInto_Stage2(ref Config outputConfig, ConfigFiles.TextFile textConfig, StringSet ignoredKeywords, Errors.ErrorList errorList) { bool success = true; if (textConfig.HasCommentTypes) { foreach (var commentType in textConfig.CommentTypes) { int commentTypeID = outputConfig.CommentTypeFromName(commentType.Name).ID; #if DEBUG if (commentTypeID == 0) { throw new InvalidOperationException(); } #endif if (commentType.HasKeywordGroups) { foreach (var keywordGroup in commentType.KeywordGroups) { int languageID = 0; if (keywordGroup.IsLanguageSpecific) { var language = EngineInstance.Languages.FromName(keywordGroup.LanguageName); if (language == null) { errorList.Add( Locale.Get("NaturalDocs.Engine", "Comments.txt.UnrecognizedKeywordLanguage(name)", keywordGroup.LanguageName), keywordGroup.PropertyLocation ); success = false; } else { languageID = language.ID; } } foreach (var keywordDefinition in keywordGroup.KeywordDefinitions) { if (!ignoredKeywords.Contains(keywordDefinition.Keyword)) { var outputKeywordDefinition = new KeywordDefinition(keywordDefinition.Keyword); outputKeywordDefinition.CommentTypeID = commentTypeID; if (languageID != 0) { outputKeywordDefinition.LanguageID = languageID; } // AddKeywordDefinition will handle overwriting definitions with the same keyword and language outputConfig.AddKeywordDefinition(outputKeywordDefinition); } if (keywordDefinition.HasPlural && !ignoredKeywords.Contains(keywordDefinition.Plural)) { var outputKeywordDefinition = new KeywordDefinition(keywordDefinition.Plural); outputKeywordDefinition.CommentTypeID = commentTypeID; outputKeywordDefinition.Plural = true; if (languageID != 0) { outputKeywordDefinition.LanguageID = languageID; } outputConfig.AddKeywordDefinition(outputKeywordDefinition); } } } } } } return(success); }
// Group: Initialization Functions // __________________________________________________________________________ /* Function: Start_Stage1 * * Loads and combines the two versions of <Comments.txt>, returning whether it was successful. If there were any * errors they will be added to errorList. * * Only the settings which don't depend on <Languages.txt> will be loaded. Call <Start_Stage2()> after * <Languages.Manager.Start_Stage1()> has been called to complete the process. * * Dependencies: * * - <Config.Manager> must be started before this class can start. */ public bool Start_Stage1(Errors.ErrorList errorList) { StartupIssues newStartupIssues = StartupIssues.None; bool success = true; // // Comments.txt // Path systemTextConfigPath = EngineInstance.Config.SystemConfigFolder + "/Comments.txt"; Path projectTextConfigPath = EngineInstance.Config.ProjectConfigFolder + "/Comments.txt"; Path oldTopicsFilePath = EngineInstance.Config.ProjectConfigFolder + "/Topics.txt"; ConfigFiles.TextFileParser textConfigFileParser = new ConfigFiles.TextFileParser(); // Load the system Comments.txt. if (!textConfigFileParser.Load(systemTextConfigPath, PropertySource.SystemCommentsFile, errorList, out systemTextConfig)) { success = false; } // Load the project Comments.txt. We want to do this even if the system Comments.txt failed so we get the error messages // from both. if (System.IO.File.Exists(projectTextConfigPath)) { if (!textConfigFileParser.Load(projectTextConfigPath, PropertySource.ProjectCommentsFile, errorList, out projectTextConfig)) { success = false; } } // If the project Comments.txt doesn't exist, try loading Topics.txt, which is what the file was called prior to 2.0. else if (System.IO.File.Exists(oldTopicsFilePath)) { if (!textConfigFileParser.Load(oldTopicsFilePath, PropertySource.ProjectCommentsFile, errorList, out projectTextConfig)) { success = false; } } // If neither file exists just create a blank config. The project Comments.txt not existing is not an error. else { projectTextConfig = new ConfigFiles.TextFile(); } if (!success) { return(false); } if (!ValidateCommentTypes(systemTextConfig, errorList)) { success = false; } if (!ValidateCommentTypes(projectTextConfig, errorList)) { success = false; } if (!success) { return(false); } // Merge them into one combined config. Note that this doesn't do keywords, ignored keywords, or tags. Only the comment // types and their non-keyword properties will exist in the merged config. if (!MergeCommentTypes(systemTextConfig, projectTextConfig, out mergedTextConfig, errorList)) { return(false); } if (!ValidateCommentTypes(mergedTextConfig, errorList)) { return(false); } // // Comments.nd // Path lastRunConfigPath = EngineInstance.Config.WorkingDataFolder + "/Comments.nd"; ConfigFiles.BinaryFileParser binaryConfigFileParser = new ConfigFiles.BinaryFileParser(); // If we need to start fresh anyway we can skip loading the file and create a blank config if (EngineInstance.HasIssues(StartupIssues.NeedToStartFresh | StartupIssues.CommentIDsInvalidated | StartupIssues.CodeIDsInvalidated) || // though if that wasn't the case but we failed at loading the file, the result is the same !binaryConfigFileParser.Load(lastRunConfigPath, out lastRunConfig)) { lastRunConfig = new Config(); newStartupIssues |= StartupIssues.NeedToReparseAllFiles | StartupIssues.CommentIDsInvalidated; } // // Create the final config // config = new Config(); // We go through the comment types present in the merged text config files and convert them into the final config. We // need the contents of Comments.nd so we can keep the comment type IDs consistent from one run to the next if possible. IDObjects.NumberSet usedCommentTypeIDs = new IDObjects.NumberSet(); IDObjects.NumberSet lastRunUsedCommentTypeIDs = lastRunConfig.UsedCommentTypeIDs(); if (mergedTextConfig.HasCommentTypes) { foreach (var textCommentType in mergedTextConfig.CommentTypes) { var finalCommentType = FinalizeCommentType(textCommentType); // We still need to set the ID. See if a comment type of the same name existed in the previous run. var lastRunCommentType = lastRunConfig.CommentTypeFromName(textCommentType.Name); // If there wasn't one we can assign a new ID, but pick one that isn't used in this run or the last run so there's no // conflicts. if (lastRunCommentType == null) { int id = lastRunUsedCommentTypeIDs.LowestAvailable; if (usedCommentTypeIDs.Contains(id)) { id = Math.Max(usedCommentTypeIDs.Highest + 1, lastRunUsedCommentTypeIDs.Highest + 1); } finalCommentType.ID = id; config.AddCommentType(finalCommentType); usedCommentTypeIDs.Add(finalCommentType.ID); lastRunUsedCommentTypeIDs.Add(finalCommentType.ID); } // If the type did exist but we haven't used its ID yet, we can keep it. else if (!usedCommentTypeIDs.Contains(lastRunCommentType.ID)) { finalCommentType.ID = lastRunCommentType.ID; config.AddCommentType(finalCommentType); usedCommentTypeIDs.Add(finalCommentType.ID); } // However, if the type did exist and we assigned that ID already, then we have a conflict and have to tell the engine // that the IDs from the last run were invalidated. We can just assign anything unused at this point since it no longer // matters. else { finalCommentType.ID = usedCommentTypeIDs.LowestAvailable; config.AddCommentType(finalCommentType); usedCommentTypeIDs.Add(finalCommentType.ID); newStartupIssues |= StartupIssues.CommentIDsInvalidated; } // Now that we have a final ID, set the simple identifier for any type that still needs it. Some types may have it null if // it wasn't manually defined and the name didn't contain A-Z characters. if (finalCommentType.SimpleIdentifier == null) { finalCommentType.SimpleIdentifier = "CommentTypeID" + finalCommentType.ID; } } } // Now do the same thing for tags. List <string> mergedTagList = null; int mergedTagCount = (systemTextConfig.HasTags ? systemTextConfig.Tags.Count : 0) + (projectTextConfig.HasTags ? projectTextConfig.Tags.Count : 0); if (mergedTagCount > 0) { mergedTagList = new List <string>(mergedTagCount); if (systemTextConfig.HasTags) { mergedTagList.AddRange(systemTextConfig.Tags); } if (projectTextConfig.HasTags) { mergedTagList.AddRange(projectTextConfig.Tags); } } IDObjects.NumberSet usedTagIDs = new IDObjects.NumberSet(); IDObjects.NumberSet lastRunUsedTagIDs = lastRunConfig.UsedTagIDs(); if (mergedTagList != null) { foreach (var tagString in mergedTagList) { // Just skip it if it already exists if (config.TagFromName(tagString) != null) { continue; } var tag = new Tag(tagString); // We still need to set the ID. See if a tag of the same name existed in the previous run. var lastRunTag = lastRunConfig.TagFromName(tagString); // If there wasn't one we can assign a new ID, but pick one that isn't used in this run or the last run so there's no // conflicts. if (lastRunTag == null) { int id = lastRunUsedTagIDs.LowestAvailable; if (usedTagIDs.Contains(id)) { id = Math.Max(usedTagIDs.Highest + 1, lastRunUsedTagIDs.Highest + 1); } tag.ID = id; config.AddTag(tag); usedTagIDs.Add(tag.ID); lastRunUsedTagIDs.Add(tag.ID); } // If the tag did exist but we haven't used its ID yet, we can keep it. else if (!usedTagIDs.Contains(lastRunTag.ID)) { tag.ID = lastRunTag.ID; config.AddTag(tag); usedTagIDs.Add(tag.ID); } // However, if the tag did exist and we assigned that ID already, then we have a conflict and have to tell the engine // that the IDs from the last run were invalidated. We can just assign anything unused at this point since it no longer // matters. else { tag.ID = usedTagIDs.LowestAvailable; config.AddTag(tag); usedTagIDs.Add(tag.ID); newStartupIssues |= StartupIssues.CommentIDsInvalidated; } } } // That's it for stage one. Everything else is in stage 2. if (newStartupIssues != StartupIssues.None) { EngineInstance.AddStartupIssues(newStartupIssues); } return(true); }