/* Function: MergeLanguages * * Merges two <ConfigFiles.TextFiles> into a new one, putting all the languages into one list and applying any alter entries. * This does NOT cover file extensions, aliases, or shebang strings; 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 language. 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 MergeLanguages(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 (!MergeLanguagesInto(ref combinedConfig, baseConfig, errorList) || !MergeLanguagesInto(ref combinedConfig, overridingConfig, errorList)) { combinedConfig = null; return(false); } return(true); }
// Group: Initialization Support Functions // __________________________________________________________________________ // // These functions are only used by <Start_Stage1()> and <Start_Stage2()>. // /* Function: ValidateLanguages * Validates all the language settings in a <ConfigFiles.TextFile>. Returns whether it is valid, and adds any errors it * finds to errorList. */ protected bool ValidateLanguages(ConfigFiles.TextFile configFile, Errors.ErrorList errorList) { bool success = true; if (configFile.HasLanguages) { foreach (var language in configFile.Languages) { if (!ValidateLanguage(language, errorList)) { success = false; // Continue anyway so we can report the errors in all of them. } } } return(success); }
/* Function: MergePrototypeEndersInto_Stage2 * Merges the prototype enders found in the <ConfigFiles.TextFile> into the <Languages> of the <Config>, returning whether * it was successful. If not it will add any errors into the error list. This must be done in <Start_Stage2()> because we need * <CommentTypes.Manager> to have loaded all the comment type names. */ protected bool MergePrototypeEndersInto_Stage2(ref Config config, ConfigFiles.TextFile textFileConfig, Errors.ErrorList errorList) { bool success = true; if (textFileConfig.HasLanguages) { foreach (var textFileLanguage in textFileConfig.Languages) { if (textFileLanguage.HasPrototypeEnders) { var matchingLanguage = config.LanguageFromName(textFileLanguage.Name); #if DEBUG if (matchingLanguage == null) { throw new InvalidOperationException(); } #endif foreach (var textFilePrototypeEnders in textFileLanguage.PrototypeEnders) { var matchingCommentType = EngineInstance.CommentTypes.FromName(textFilePrototypeEnders.CommentType); if (matchingCommentType == null) { errorList.Add(Locale.Get("NaturalDocs.Engine", "Languages.txt.PrototypeEnderCommentTypeDoesntExist(name)", textFilePrototypeEnders.CommentType), textFilePrototypeEnders.PropertyLocation); success = false; } else { matchingLanguage.AddPrototypeEnders( new PrototypeEnders(matchingCommentType.ID, textFilePrototypeEnders.EnderStrings) ); } } } } } return(success); }
/* Function: TouchUp_Stage2 * Applies some minor improvements to the <ConfigFiles.TextFile>, such as making sure the capitalization of Alter Language * and [Comment Type] Prototype Enders match the original definition. Assumes everything is valid, meaning all Alter * Language entries have corresponding entries in finalConfig and all [Comment Type] Prototype Enders entries have * corresponding languages in <CommentTypes.Manager>. */ protected void TouchUp_Stage2(ref ConfigFiles.TextFile textConfig, Config finalConfig) { if (textConfig.HasLanguages) { foreach (var textFileLanguage in textConfig.Languages) { // Fix "Alter Language: [name]" capitalization if (textFileLanguage.AlterLanguage) { var originalLanguage = finalConfig.LanguageFromName(textFileLanguage.Name); textFileLanguage.FixNameCapitalization(originalLanguage.Name); // We don't also check to see if the language we're altering exists in the same file and merge their definitions // into one. Why? Consider this: // // Language: Language A // Extensions: langA // // Language: Language B // Extensions: langB // // Alter Language: Language A // Add Extensions: langB // // File extensions B should be part of Language A. However, if we merged the definitions it would appear first // and be overridden by Language B. So we just leave the two language entries for A instead. } if (textFileLanguage.HasPrototypeEnders) { foreach (var textFilePrototypeEnders in textFileLanguage.PrototypeEnders) { // Fix "[Comment Type] Prototype Enders" capitalization var originalCommentType = EngineInstance.CommentTypes.FromName(textFilePrototypeEnders.CommentType); textFilePrototypeEnders.CommentType = originalCommentType.Name; } } } } }
/* Function: MergeLanguagesInto * Merges the languages of the second <ConfigFiles.TextFile> into the first, adding new types and applying any alter entries. * This does NOT merge file extensions, aliases, or shebang strings. 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 MergeLanguagesInto(ref ConfigFiles.TextFile baseConfig, ConfigFiles.TextFile overridingConfig, Errors.ErrorList errorList) { bool success = true; if (overridingConfig.HasLanguages) { foreach (var overridingLanguage in overridingConfig.Languages) { var matchingLanguage = baseConfig.FindLanguage(overridingLanguage.Name); if (matchingLanguage != null) { if (overridingLanguage.AlterLanguage == false) { errorList.Add(Locale.Get("NaturalDocs.Engine", "Languages.txt.LanguageAlreadyExists(name)", overridingLanguage.Name), overridingLanguage.PropertyLocation); success = false; } else { MergeLanguageInto(ref matchingLanguage, overridingLanguage); } } else // no match { if (overridingLanguage.AlterLanguage == true) { errorList.Add(Locale.Get("NaturalDocs.Engine", "Languages.txt.AlteredLanguageDoesntExist(name)", overridingLanguage.Name), overridingLanguage.NamePropertyLocation); success = false; } else { baseConfig.AddLanguage(overridingLanguage.Duplicate()); } } } } return(success); }
/* Function: MergeLanguageIdentifiersInto * Merges the file extensions, aliases, and shebang strings from the <ConfigFiles.TextFiles> into the <Config>. It assumes all * <ConfigFiles.TextFileLanguages> in textConfig have corresponding <Language> in outputConfig. */ protected void MergeLanguageIdentifiersInto(ref Config outputConfig, ConfigFiles.TextFile systemTextConfig, ConfigFiles.TextFile projectTextConfig) { // First collect our ignored extensions StringSet ignoredFileExtensions = new StringSet(Config.KeySettingsForFileExtensions); if (systemTextConfig.HasIgnoredFileExtensions) { foreach (var ignoredFileExtension in systemTextConfig.IgnoredFileExtensions) { ignoredFileExtensions.Add(ignoredFileExtension); } } if (projectTextConfig.HasIgnoredFileExtensions) { foreach (var ignoredFileExtension in projectTextConfig.IgnoredFileExtensions) { ignoredFileExtensions.Add(ignoredFileExtension); } } // Now turn our language lists into one big combined one, but not in a way that merges any of its entries like // mergedTextConfig did. Just put them all one after the other. int languageEntryCount = (systemTextConfig.HasLanguages ? systemTextConfig.Languages.Count : 0) + (projectTextConfig.HasLanguages ? projectTextConfig.Languages.Count : 0); List <ConfigFiles.TextFileLanguage> languages = new List <ConfigFiles.TextFileLanguage>(languageEntryCount); if (systemTextConfig.HasLanguages) { languages.AddRange(systemTextConfig.Languages); } if (projectTextConfig.HasLanguages) { languages.AddRange(projectTextConfig.Languages); } // Now apply file extensions, aliases, and shebang strings. We do it from this list instead of mergedTextConfig so // so everything happens in the proper order. For example: // // Language: LanguageA // Extensions: langA // // Language: LanguageB // Extensions: langB // // Alter Language: LanguageA // Replace Extensions: langB // // In this case langB should actually map to LanguageA. Not only that, langA should not be applied at all because // we used Replace instead of Add. for (int i = 0; i < languageEntryCount; i++) { // We don't need to check whether they're defined for the first time, added, or replaced here. In all cases we would // apply them unless there's a future entry that says Replace. bool applyFileExtensions = languages[i].HasFileExtensions; bool applyAliases = languages[i].HasAliases; bool applyShebangStrings = languages[i].HasShebangStrings; // Check for future Replace entries. string normalizedLanguageName = languages[i].Name.NormalizeKey(Config.KeySettingsForLanguageName); for (int j = i + 1; j < languageEntryCount; j++) { if (!applyFileExtensions && !applyAliases && !applyShebangStrings) { break; } if (languages[j].Name.NormalizeKey(Config.KeySettingsForLanguageName) == normalizedLanguageName) { if (languages[j].HasFileExtensions && languages[j].FileExtensionsPropertyChange == ConfigFiles.TextFileLanguage.PropertyChange.Replace) { applyFileExtensions = false; } if (languages[j].HasAliases && languages[j].AliasesPropertyChange == ConfigFiles.TextFileLanguage.PropertyChange.Replace) { applyAliases = false; } if (languages[j].HasShebangStrings && languages[j].ShebangStringsPropertyChange == ConfigFiles.TextFileLanguage.PropertyChange.Replace) { applyShebangStrings = false; } } } // Apply what's left. int languageID = outputConfig.LanguageFromName(languages[i].Name).ID; #if DEBUG if (languageID == 0) { throw new InvalidOperationException(); } #endif if (applyFileExtensions) { foreach (var fileExtension in languages[i].FileExtensions) { if (!ignoredFileExtensions.Contains(fileExtension)) { outputConfig.AddFileExtension(fileExtension, languageID); } } } if (applyAliases) { foreach (var alias in languages[i].Aliases) { outputConfig.AddAlias(alias, languageID); } } if (applyShebangStrings) { foreach (var shebangString in languages[i].ShebangStrings) { outputConfig.AddShebangString(shebangString, languageID); } } } }
// Group: Initialization Functions // __________________________________________________________________________ /* Function: Start_Stage1 * * Loads and combines the two versions of <Languages.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 <Comments.txt> will be loaded. Call <Start_Stage2()> after * <CommentTypes.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; // // Languages.txt // Path systemTextConfigPath = EngineInstance.Config.SystemConfigFolder + "/Languages.txt"; Path projectTextConfigPath = EngineInstance.Config.ProjectConfigFolder + "/Languages.txt"; ConfigFiles.TextFileParser textConfigFileParser = new ConfigFiles.TextFileParser(); // Load the system Languages.txt. if (!textConfigFileParser.Load(systemTextConfigPath, PropertySource.SystemLanguagesFile, errorList, out systemTextConfig)) { success = false; } // Load the project Languages.txt. We want to do this even if the system Languages.txt failed so we get the error messages // from both. if (System.IO.File.Exists(projectTextConfigPath)) { if (!textConfigFileParser.Load(projectTextConfigPath, PropertySource.ProjectLanguagesFile, errorList, out projectTextConfig)) { success = false; } } // If it doesn't exist it's not an error. Just create a blank config. else { projectTextConfig = new ConfigFiles.TextFile(); } if (!success) { return(false); } if (!ValidateLanguages(systemTextConfig, errorList)) { success = false; } if (!ValidateLanguages(projectTextConfig, errorList)) { success = false; } if (!success) { return(false); } // Merge them into one combined config. Note that this doesn't do file extensions, aliases, or shebang strings. Only the // languages and their other properties will exist in the merged config. if (!MergeLanguages(systemTextConfig, projectTextConfig, out mergedTextConfig, errorList)) { return(false); } if (!ValidateLanguages(mergedTextConfig, errorList)) { return(false); } // // Languages.nd // Path lastRunConfigPath = EngineInstance.Config.WorkingDataFolder + "/Languages.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.CodeIDsInvalidated; } // // Create the final config // config = new Config(); // We go through the languages 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 language IDs consistent from one run to the next if possible. IDObjects.NumberSet usedLanguageIDs = new IDObjects.NumberSet(); IDObjects.NumberSet lastRunUsedLanguageIDs = lastRunConfig.UsedLanguageIDs(); if (mergedTextConfig.HasLanguages) { foreach (var textLanguage in mergedTextConfig.Languages) { // First we need our base language object. If there's a predefined language matching the name we'll use that so // we get its settings and parser. If not we'll create a new object. We shouldn't have to worry about more than one // language using the same predefined language object because validation should have taken care of that. var finalLanguage = FindPredefinedLanguage(textLanguage.Name); if (finalLanguage == null) { finalLanguage = new Language(textLanguage.Name); } // Apply the properties. Keep going if there are errors since we want to find them all. if (!FinalizeLanguage(ref finalLanguage, textLanguage, errorList)) { success = false; } // We still need to set the ID. See if a language of the same name existed in the previous run. var lastRunLanguage = lastRunConfig.LanguageFromName(textLanguage.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 (lastRunLanguage == null) { int id = lastRunUsedLanguageIDs.LowestAvailable; if (usedLanguageIDs.Contains(id)) { id = Math.Max(usedLanguageIDs.Highest + 1, lastRunUsedLanguageIDs.Highest + 1); } finalLanguage.ID = id; config.AddLanguage(finalLanguage); usedLanguageIDs.Add(finalLanguage.ID); lastRunUsedLanguageIDs.Add(finalLanguage.ID); } // If the language did exist but we haven't used its ID yet, we can keep it. else if (!usedLanguageIDs.Contains(lastRunLanguage.ID)) { finalLanguage.ID = lastRunLanguage.ID; config.AddLanguage(finalLanguage); usedLanguageIDs.Add(finalLanguage.ID); } // However, if the language 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 { finalLanguage.ID = usedLanguageIDs.LowestAvailable; config.AddLanguage(finalLanguage); usedLanguageIDs.Add(finalLanguage.ID); newStartupIssues |= StartupIssues.CodeIDsInvalidated; } // 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 (finalLanguage.SimpleIdentifier == null) { finalLanguage.SimpleIdentifier = "LanguageID" + finalLanguage.ID; } // Also create a generic parser object if one wasn't inherited from a predefined language. if (!finalLanguage.HasParser) { finalLanguage.Parser = new Parser(EngineInstance, finalLanguage); } } } if (!success) { return(false); } // Apply file extensions, aliases, and shebang strings now. MergeLanguageIdentifiersInto(ref config, systemTextConfig, projectTextConfig); // That's it for stage one. Everything else is in stage 2. if (newStartupIssues != StartupIssues.None) { EngineInstance.AddStartupIssues(newStartupIssues); } return(true); }