/// <summary> /// Loads a RantDictionary from the file at the specified path. /// </summary> /// <param name="path">The path to the file to load.</param> /// <returns></returns> public static RantDictionaryTable FromFile(string path) { var name = ""; string[] subtypes = { "default" }; bool header = true; var scopedClassSet = new HashSet <string>(); RantDictionaryEntry entry = null; var entries = new List <RantDictionaryEntry>(); var entryStringes = new List <Stringe>(); var types = new Dictionary <string, EntryTypeDef>(); var hiddenClasses = new HashSet <string> { "nsfw" }; foreach (var token in DicLexer.Tokenize(path, File.ReadAllText(path))) { switch (token.ID) { case DicTokenType.Directive: { var parts = VocabUtils.GetArgs(token.Value).ToArray(); if (!parts.Any()) { continue; } var dirName = parts.First().ToLower(); var args = parts.Skip(1).ToArray(); switch (dirName) { case "name": if (!header) { LoadError(path, token, "The #name directive may only be used in the file header."); } if (args.Length != 1) { LoadError(path, token, "#name directive expected one word:\r\n\r\n" + token.Value); } if (!Util.ValidateName(args[0])) { LoadError(path, token, $"Invalid #name value: '{args[1]}'"); } name = args[0].ToLower(); break; case "subs": if (!header) { LoadError(path, token, "The #subs directive may only be used in the file header."); } subtypes = args.Select(s => s.Trim().ToLower()).ToArray(); break; case "version": // Kept here for backwards-compatability if (!header) { LoadError(path, token, "The #version directive may only be used in the file header."); } break; case "hidden": if (!header) { LoadError(path, token, "The #hidden directive may only be used in the file header."); } if (Util.ValidateName(args[0])) { hiddenClasses.Add(args[0]); } break; // Deprecated, remove in Rant 3 case "nsfw": scopedClassSet.Add("nsfw"); break; // Deprecated, remove in Rant 3 case "sfw": scopedClassSet.Remove("nsfw"); break; case "class": { if (args.Length < 2) { LoadError(path, token, "The #class directive expects an operation and at least one value."); } switch (args[0].ToLower()) { case "add": foreach (var cl in args.Skip(1)) { scopedClassSet.Add(cl.ToLower()); } break; case "remove": foreach (var cl in args.Skip(1)) { scopedClassSet.Remove(cl.ToLower()); } break; } } break; case "type": { if (!header) { LoadError(path, token, "The #type directive may only be used in the file header."); } if (args.Length != 3) { LoadError(path, token, "#type directive requires 3 arguments."); } types.Add(args[0], new EntryTypeDef(args[0], args[1].Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries), Util.IsNullOrWhiteSpace(args[2]) ? null : new EntryTypeDefFilter(args[2]))); } break; } } break; case DicTokenType.Entry: { if (Util.IsNullOrWhiteSpace(name)) { LoadError(path, token, "Missing table name before entry list."); } if (Util.IsNullOrWhiteSpace(token.Value)) { LoadError(path, token, "Encountered empty entry."); } header = false; entry = new RantDictionaryEntry(token.Value.Split('/').Select(s => s.Trim()).ToArray(), scopedClassSet); entries.Add(entry); entryStringes.Add(token); } break; case DicTokenType.DiffEntry: { if (Util.IsNullOrWhiteSpace(name)) { LoadError(path, token, "Missing table name before entry list."); } if (Util.IsNullOrWhiteSpace(token.Value)) { LoadError(path, token, "Encountered empty entry."); } header = false; string first = null; entry = new RantDictionaryEntry(token.Value.Split('/') .Select((s, i) => { if (i > 0) { return(Diff.Mark(first, s)); } return(first = s.Trim()); }).ToArray(), scopedClassSet); entries.Add(entry); entryStringes.Add(token); } break; case DicTokenType.Property: { var parts = token.Value.Split(new[] { ' ' }, 2, StringSplitOptions.RemoveEmptyEntries); if (!parts.Any()) { LoadError(path, token, "Empty property field."); } switch (parts[0].ToLower()) { case "class": { if (parts.Length < 2) { continue; } foreach (var cl in parts[1].Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)) { bool opt = cl.EndsWith("?"); entry.AddClass(VocabUtils.GetString(opt ? cl.Substring(0, cl.Length - 1) : cl), opt); } } break; case "weight": { if (parts.Length != 2) { LoadError(path, token, "'weight' property expected a value."); } int weight; if (!Int32.TryParse(parts[1], out weight)) { LoadError(path, token, "Invalid weight value: '" + parts[1] + "'"); } entry.Weight = weight; } break; case "pron": { if (parts.Length != 2) { LoadError(path, token, "'" + parts[0] + "' property expected a value."); } var pron = parts[1].Split('/') .Select(s => s.Trim()) .ToArray(); if (subtypes.Length == pron.Length) { for (int i = 0; i < entry.Terms.Length; i++) { entry.Terms[i].Pronunciation = pron[i]; } } } break; default: { EntryTypeDef typeDef; if (!types.TryGetValue(parts[0], out typeDef)) { LoadError(path, token, $"Unknown property name '{parts[0]}'."); } // Okay, it's a type. if (parts.Length != 2) { LoadError(path, token, "Missing type value."); } entry.AddClass(VocabUtils.GetString(parts[1])); if (!typeDef.IsValidValue(parts[1])) { LoadError(path, token, $"'{parts[1]}' is not a valid value for type '{typeDef.Name}'."); } break; } } } break; } } if (types.Any()) { var eEntries = entries.GetEnumerator(); var eEntryStringes = entryStringes.GetEnumerator(); while (eEntries.MoveNext() && eEntryStringes.MoveNext()) { foreach (var type in types.Values) { if (!type.Test(eEntries.Current)) { // TODO: Find a way to output multiple non-fatal table load errors without making a gigantic exception message. LoadError(path, eEntryStringes.Current, $"Entry '{eEntries.Current}' does not satisfy type '{type.Name}'."); } } } } return(new RantDictionaryTable(name, subtypes, entries, hiddenClasses)); }
/// <summary> /// Loads a RantDictionary from the file at the specified path. /// </summary> /// <param name="path">The path to the file to load.</param> /// <param name="nsfwFilter">Specifies whether to allow or disallow NSFW entries.</param> /// <returns></returns> public static RantDictionaryTable FromFile(string path, NsfwFilter nsfwFilter = NsfwFilter.Disallow) { var name = ""; var version = Version; string[] subtypes = { "default" }; bool header = true; bool nsfw = false; var scopedClassSet = new HashSet <string>(); RantDictionaryEntry entry = null; var entries = new List <RantDictionaryEntry>(); foreach (var token in DicLexer.Tokenize(File.ReadAllText(path))) { switch (token.ID) { case DicTokenType.Directive: { var parts = token.Value.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (!parts.Any()) { continue; } switch (parts[0].ToLower()) { case "name": if (!header) { LoadError(path, token, "The #name directive may only be used in the file header."); } if (parts.Length != 2) { LoadError(path, token, "#name directive expected one word:\r\n\r\n" + token.Value); } if (!Util.ValidateName(parts[1])) { LoadError(path, token, "Invalid #name value: '\{parts[1]}'"); } name = parts[1].ToLower(); break; case "subs": if (!header) { LoadError(path, token, "The #subs directive may only be used in the file header."); } subtypes = parts.Skip(1).Select(s => s.Trim().ToLower()).ToArray(); break; case "version": if (!header) { LoadError(path, token, "The #version directive may only be used in the file header."); } if (parts.Length != 2) { LoadError(path, token, "The #version directive requires a value."); } if (!int.TryParse(parts[1], out version)) { LoadError(path, token, "Invalid version number '\{parts[1]}'"); } if (version > Version) { LoadError(path, token, "Unsupported file version '\{version}'"); } break; case "nsfw": nsfw = true; break; case "sfw": nsfw = false; break; case "class": { if (parts.Length < 3) { LoadError(path, token, "The #class directive expects an operation and at least one value."); } switch (parts[1].ToLower()) { case "add": foreach (var cl in parts.Skip(2)) { scopedClassSet.Add(cl.ToLower()); } break; case "remove": foreach (var cl in parts.Skip(2)) { scopedClassSet.Remove(cl.ToLower()); } break; } } break; } } break; case DicTokenType.Entry: { if (nsfwFilter == NsfwFilter.Disallow && nsfw) { continue; } if (Util.IsNullOrWhiteSpace(name)) { LoadError(path, token, "Missing dictionary name before entry list."); } if (Util.IsNullOrWhiteSpace(token.Value)) { LoadError(path, token, "Encountered empty dictionary entry."); } header = false; entry = new RantDictionaryEntry(token.Value.Split('/').Select(s => s.Trim()).ToArray(), scopedClassSet, nsfw); entries.Add(entry); } break; case DicTokenType.DiffEntry: { if (nsfwFilter == NsfwFilter.Disallow && nsfw) { continue; } if (Util.IsNullOrWhiteSpace(name)) { LoadError(path, token, "Missing dictionary name before entry list."); } if (Util.IsNullOrWhiteSpace(token.Value)) { LoadError(path, token, "Encountered empty dictionary entry."); } header = false; string first = null; entry = new RantDictionaryEntry(token.Value.Split('/') .Select((s, i) => { if (i > 0) { return(Diff.Mark(first, s)); } return(first = s.Trim()); }).ToArray(), scopedClassSet, nsfw); entries.Add(entry); } break; case DicTokenType.Property: { if (nsfwFilter == NsfwFilter.Disallow && nsfw) { continue; } var parts = token.Value.Split(new[] { ' ' }, 2, StringSplitOptions.RemoveEmptyEntries); if (!parts.Any()) { LoadError(path, token, "Empty property field."); } switch (parts[0].ToLower()) { case "class": { if (parts.Length < 2) { continue; } foreach (var cl in parts[1].Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)) { bool opt = cl.EndsWith("?"); entry.AddClass(VocabUtils.GetString(opt ? cl.Substring(0, cl.Length - 1) : cl), opt); } } break; case "weight": { if (parts.Length != 2) { LoadError(path, token, "'weight' property expected a value."); } int weight; if (!Int32.TryParse(parts[1], out weight)) { LoadError(path, token, "Invalid weight value: '" + parts[1] + "'"); } entry.Weight = weight; } break; case "pron": { if (parts.Length != 2) { LoadError(path, token, "'" + parts[0] + "' property expected a value."); } var pron = parts[1].Split('/') .Select(s => s.Trim()) .ToArray(); if (subtypes.Length != pron.Length) { LoadError(path, token, "Pronunciation list length must match subtype count."); } for (int i = 0; i < entry.Terms.Length; i++) { entry.Terms[i].Pronunciation = pron[i]; } } break; } } break; } } return(new RantDictionaryTable(name, subtypes, entries)); }