/// <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)); }
public static void ReadTerms(string origin, string str, int len, int line, ref int i, RantDictionaryTable table, RantDictionaryEntry activeTemplate, Dictionary <string, RantDictionaryEntry> templates, out RantDictionaryEntry result) { SkipSpace(str, len, ref i); int t = 0; var terms = new RantDictionaryTerm[table.TermsPerEntry]; int split = -1; char c = '\0'; var buffer = new StringBuilder(); var white = new StringBuilder(); while (i < len) { switch (c = str[i++]) { // Inline comment case '#': goto done; // Phrasal split operator case '+': if (split > -1) { throw new RantTableLoadException(origin, line, i, "err-table-multiple-splits"); } white.Length = 0; split = buffer.Length; SkipSpace(str, len, ref i); break; // Term reference case '[': { SkipSpace(str, len, ref i); if (i >= len) { throw new RantTableLoadException(origin, line, i, "err-table-incomplete-term-reference"); } int start = i; if (white.Length > 0) { buffer.Append(white); white.Length = 0; } switch (str[i++]) { // Current term from active template case ']': if (t == -1) { throw new RantTableLoadException(origin, line, start + 1, "err-table-no-template"); } buffer.Append(activeTemplate[t].Value); break; // Custom term from active template case '.': { if (activeTemplate == null) { throw new RantTableLoadException(origin, line, start + 1, "err-table-no-template"); } while (i < len && IsValidSubtypeChar(str[i])) { i++; // Read subtype name } if (str[i] != ']') { throw new RantTableLoadException(origin, line, i, "err-table-incomplete-term-reference"); } string subName = str.Substring(start + 1, i - start - 1); if (subName.Length == 0) { throw new RantTableLoadException(origin, line, start + 1, "err-table-empty-subtype-reference"); } int templateSubIndex = table.GetSubtypeIndex(subName); if (templateSubIndex == -1) { throw new RantTableLoadException(origin, line, start + 1, "err-table-nonexistent-subtype", subName); } // Add term value to buffer buffer.Append(activeTemplate[templateSubIndex].Value); i++; // Skip past closing bracket break; } // It is probably a reference to another entry, let's see. default: { while (i < len && IsValidSubtypeChar(str[i]) || str[i] == '.') { i++; } if (str[i] != ']') { throw new RantTableLoadException(origin, line, i, "err-table-incomplete-term-reference"); } var id = str.Substring(start, i - start).Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries); switch (id.Length) { // It's just a template ID. case 1: { if (!templates.TryGetValue(id[0], out RantDictionaryEntry entry)) { throw new RantTableLoadException(origin, line, start + 1, "err-table-entry-not-found"); } // Append term value to buffer buffer.Append(entry[t].Value); break; } // Template ID and custom subtype case 2: { if (!templates.TryGetValue(id[0], out RantDictionaryEntry entry)) { throw new RantTableLoadException(origin, line, start + 1, "err-table-entry-not-found"); } int templateSubIndex = table.GetSubtypeIndex(id[1]); if (templateSubIndex == -1 || templateSubIndex >= table.TermsPerEntry) { throw new RantTableLoadException(origin, line, start + 1, "err-table-nonexistent-subtype", id[1]); } buffer.Append(entry[templateSubIndex].Value); break; } // ??? default: throw new RantTableLoadException(origin, line, start + 1, "err-table-invalid-term-reference"); } i++; // Skip past closing bracket break; } } break; } case '\\': { if (white.Length > 0) { buffer.Append(white); white.Length = 0; } switch (c = str[i++]) { case 'n': buffer.Append('\n'); continue; case 'r': buffer.Append('\r'); continue; case 't': buffer.Append('\t'); continue; case 'v': buffer.Append('\v'); continue; case 'f': buffer.Append('\f'); continue; case 'b': buffer.Append('\b'); continue; case 's': buffer.Append(' '); continue; case 'u': { if (i + 4 > len) { throw new RantTableLoadException(origin, line, i + 1, "err-table-incomplete-escape"); } if (!ushort.TryParse(str.Substring(i, 4), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out ushort codePoint)) { throw new RantTableLoadException(origin, line, i + 1, "err-table-unrecognized-codepoint"); } buffer.Append((char)codePoint); i += 4; continue; } case 'U': { if (i + 8 > len) { throw new RantTableLoadException(origin, line, i + 1, "err-table-incomplete-escape"); } if (!Util.TryParseSurrogatePair(str.Substring(i, 8), out char high, out char low)) { throw new RantTableLoadException(origin, line, i + 1, "err-table-unrecognized-codepoint"); } buffer.Append(high).Append(low); i += 8; continue; } default: buffer.Append(c); continue; } continue; } case ',': if (t >= terms.Length) { throw new RantTableLoadException(origin, line, i, "err-table-too-many-terms", terms.Length, t); } terms[t++] = new RantDictionaryTerm(buffer.ToString(), split); buffer.Length = 0; white.Length = 0; split = -1; SkipSpace(str, len, ref i); break; default: if (char.IsWhiteSpace(c)) { white.Append(c); } else { if (white.Length > 0) { buffer.Append(white); white.Length = 0; } buffer.Append(c); } continue; } } done: if (t != terms.Length - 1) { throw new RantTableLoadException(origin, line, i, "err-table-too-few-terms", terms.Length, t + 1); } terms[t] = new RantDictionaryTerm(buffer.ToString()); result = new RantDictionaryEntry(terms); // Add classes from template if (activeTemplate != null) { foreach (string cl in activeTemplate.GetRequiredClasses()) { result.AddClass(cl, false); } foreach (string cl in activeTemplate.GetOptionalClasses()) { result.AddClass(cl, true); } } }
/// <summary> /// Loads a table from the specified stream. /// </summary> /// <param name="origin">The origin of the stream. This will typically be a file path or package name.</param> /// <param name="stream">The stream to load the table from.</param> /// <returns></returns> public static RantDictionaryTable FromStream(string origin, Stream stream) { string name = null; // Stores the table name before final table construction int termsPerEntry = 0; // Stores the term count var subtypes = new Dictionary <string, int>(); // Stores subtypes before final table construction var hidden = new HashSet <string>(StringComparer.InvariantCultureIgnoreCase); RantDictionaryTable table = null; // The table object, constructed when first entry is found string l; // Current line string int line = 0; // Current line number int len, i; // Length and character index of current line bool dummy = false; // Determines if the next entry is a dummy entry string tId = null; // Template ID RantDictionaryEntry activeTemplate = null; // Current template var templates = new Dictionary <string, RantDictionaryEntry>(); RantDictionaryEntry currentEntry = null; var autoClasses = new HashSet <string>(); var autoClassStack = new Stack <List <string> >(); using (var reader = new StreamReader(stream)) { while (!reader.EndOfStream) { line++; // Skip blank lines if (Util.IsNullOrWhiteSpace(l = reader.ReadLine())) { continue; } // Update line info len = l.Length; i = 0; // Skip whitespace at the start of the line while (i < len && char.IsWhiteSpace(l[i])) { i++; } switch (l[i++]) { // Comments case '#': continue; // Directive case '@': { // Read directive name int dPos = i; if (!Tools.ReadDirectiveName(l, len, ref i, out string directiveName)) { throw new RantTableLoadException(origin, line, dPos + 1, "err-table-missing-directive-name"); } // Read arguments var args = new List <Argument>(); while (Tools.ReadArg(origin, l, len, line, ref i, out Argument arg)) { args.Add(arg); } switch (directiveName.ToLowerInvariant()) { // Table name definition case "name": { // Do not allow this to appear anywhere except at the top of the file if (table != null) { throw new RantTableLoadException(origin, line, dPos + 1, "err-table-misplaced-header-directive"); } // Do not allow multiple @name directives if (name != null) { throw new RantTableLoadException(origin, line, dPos + 1, "err-table-multiple-names"); } // One argument required if (args.Count != 1) { throw new RantTableLoadException(origin, line, dPos + 1, "err-table-name-args"); } // Must meet standard identifier requirements if (!Util.ValidateName(args[0].Value)) { throw new RantTableLoadException(origin, line, args[0].CharIndex + 1, "err-table-invalid-name", args[0].Value); } name = args[0].Value; break; } // Subtype definition case "sub": { // Do not allow this to appear anywhere except at the top of the file if (table != null) { throw new RantTableLoadException(origin, line, dPos + 1, "err-table-misplaced-header-directive"); } // @sub requires at least one argument if (args.Count == 0) { throw new RantTableLoadException(origin, line, dPos + 1, "err-table-subtype-args"); } // If the first argument is a number, use it as the subtype index. if (Util.ParseInt(args[0].Value, out int termIndex)) { // Disallow negative term indices if (termIndex < 0) { throw new RantTableLoadException(origin, line, dPos + 1, "err-table-sub-index-negative", termIndex); } // Requires at least one name if (args.Count < 2) { throw new RantTableLoadException(origin, line, dPos + 1, "err-table-sub-missing-name"); } // If the index is outside the current term index range, increase the number. if (termIndex >= termsPerEntry) { termsPerEntry = termIndex + 1; } // Assign all following names to the index for (int j = 1; j < args.Count; j++) { // Validate subtype name if (!Util.ValidateName(args[j].Value)) { throw new RantTableLoadException(origin, line, args[j].CharIndex + 1, "err-table-bad-subtype", args[j].Value); } subtypes[args[j].Value] = termIndex; } } else { // Add to last index termIndex = termsPerEntry++; // Assign all following names to the index foreach (var a in args) { // Validate subtype name if (!Util.ValidateName(a.Value)) { throw new RantTableLoadException(origin, line, a.CharIndex + 1, "err-table-bad-subtype", a.Value); } subtypes[a.Value] = termIndex; } } break; } case "hide": if (args.Count == 0) { break; } foreach (var a in args) { if (!Util.ValidateName(a.Value)) { throw new RantTableLoadException(origin, line, i, "err-table-invalid-class", a.Value); } hidden.Add(String.Intern(a.Value)); } break; case "dummy": if (args.Count != 0) { throw new RantTableLoadException(origin, line, i, "err-table-argc-mismatch", directiveName, 0, args.Count); } dummy = true; break; case "id": if (args.Count != 1) { throw new RantTableLoadException(origin, line, i, "err-table-argc-mismatch", directiveName, 1, args.Count); } if (!Util.ValidateName(args[0].Value)) { throw new RantTableLoadException(origin, line, args[0].CharIndex + 1, "err-table-bad-template-id", args[0].Value); } tId = args[0].Value; break; case "using": if (args.Count != 1) { throw new RantTableLoadException(origin, line, i, "err-table-argc-mismatch", directiveName, 1, args.Count); } if (!Util.ValidateName(args[0].Value)) { throw new RantTableLoadException(origin, line, args[0].CharIndex + 1, "err-table-bad-template-id", args[0].Value); } if (!templates.TryGetValue(args[0].Value, out activeTemplate)) { throw new RantTableLoadException(origin, line, args[0].CharIndex + 1, "err-table-template-not-found", args[0].Value); } break; case "class": { var cList = new List <string>(); if (args.Count == 0) { throw new RantTableLoadException(origin, line, i, "err-table-args-expected", directiveName); } foreach (var cArg in args) { if (!Tools.ValidateClassName(cArg.Value)) { throw new RantTableLoadException(origin, line, cArg.CharIndex + 1, "err-table-invalid-class", cArg.Value); } cList.Add(cArg.Value); autoClasses.Add(cArg.Value); } autoClassStack.Push(cList); break; } case "endclass": { if (args.Count == 0) { if (autoClassStack.Count > 0) { foreach (string cName in autoClassStack.Pop()) { autoClasses.Remove(cName); } } } break; } } break; } // Entry case '>': Tools.ConstructTable(origin, name, subtypes, ref termsPerEntry, ref table); Tools.ReadTerms(origin, l, len, line, ref i, table, activeTemplate, templates, out currentEntry); if (!dummy) { table.AddEntry(currentEntry); } foreach (string autoClass in autoClasses) { currentEntry.AddClass(autoClass); } if (tId != null) { templates[tId] = currentEntry; tId = null; } dummy = false; activeTemplate = null; break; // Property case '-': { Tools.ConstructTable(origin, name, subtypes, ref termsPerEntry, ref table); Tools.SkipSpace(l, len, ref i); // Read property name int dPos = i; if (!Tools.ReadDirectiveName(l, len, ref i, out string propName)) { throw new RantTableLoadException(origin, line, dPos + 1, "err-table-missing-property-name"); } // Read arguments var args = new List <Argument>(); while (Tools.ReadArg(origin, l, len, line, ref i, out Argument arg)) { args.Add(arg); } // No args? Skip it. if (args.Count == 0) { continue; } switch (propName.ToLowerInvariant()) { case "class": foreach (var cArg in args) { if (!Tools.ValidateClassName(cArg.Value)) { throw new RantTableLoadException(origin, line, cArg.CharIndex + 1, "err-table-invalid-class", cArg.Value); } currentEntry.AddClass(cArg.Value); } break; case "weight": { if (!float.TryParse(args[0].Value, out float weight) || weight <= 0) { throw new RantTableLoadException(origin, line, args[0].CharIndex + 1, "err-table-invalid-weight", args[0].Value); } currentEntry.Weight = weight; table.EnableWeighting = true; break; } case "pron": if (args.Count != table.TermsPerEntry) { continue; } for (int j = 0; j < currentEntry.TermCount; j++) { currentEntry[j].Pronunciation = args[j].Value; } break; default: if (args.Count == 1) { currentEntry.SetMetadata(propName, args[0].Value); } else { currentEntry.SetMetadata(propName, args.Select(a => a.Value).ToArray()); } break; } break; } } } } // Add hidden classes foreach (string hc in hidden) { table.HideClass(hc); } table.RebuildCache(); return(table); }
internal override void DeserializeData(EasyReader reader) { this.Name = reader.ReadString(); this.Language = reader.ReadString(); this.TermsPerEntry = reader.ReadInt32(); for (int i = 0; i < TermsPerEntry; i++) { foreach (var subtype in reader.ReadStringArray()) { AddSubtype(subtype, i); } } _hidden.AddRange(reader.ReadStringArray()); int numEntries = reader.ReadInt32(); for (int i = 0; i < numEntries; i++) { var terms = new RantDictionaryTerm[TermsPerEntry]; for (int j = 0; j < TermsPerEntry; j++) { var value = reader.ReadString(); var pron = reader.ReadString(); int valueSplit = reader.ReadInt32(); int pronSplit = reader.ReadInt32(); terms[j] = new RantDictionaryTerm(value, pron, valueSplit, pronSplit); } float weight = reader.ReadSingle(); var entry = new RantDictionaryEntry(terms) { Weight = weight }; foreach (var reqClass in reader.ReadStringArray()) { entry.AddClass(reqClass, false); } foreach (var optClass in reader.ReadStringArray()) { entry.AddClass(optClass, true); } int metaCount = reader.ReadInt32(); for (int j = 0; j < metaCount; j++) { bool isArray = reader.ReadBoolean(); var key = reader.ReadString(); if (isArray) { entry.SetMetadata(key, reader.ReadStringArray()); } else { entry.SetMetadata(key, reader.ReadString()); } } AddEntry(entry); } }
/// <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)); }
/// <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); }