/// <summary> /// Determines if the specified dictionary entry passes the filter. /// </summary> /// <param name="entry">The entry to test.</param> /// <param name="exclusive">Specifies whether the search is exclusive.</param> /// <returns></returns> public bool Test(RantDictionaryEntry entry, bool exclusive = false) { return exclusive ? _items.Any() == entry.GetClasses().Any() && entry.GetClasses().All(c => _items.Any(item => item.Any(rule => rule.ShouldMatch && rule.Class == c))) : !_items.Any() || _items.All(set => set.Any(rule => entry.ContainsClass(rule.Class) == rule.ShouldMatch)); }
/// <summary> /// Determines if the specified dictionary entry passes the filter. /// </summary> /// <param name="entry">The entry to test.</param> /// <param name="exclusive">Specifies whether the search is exclusive.</param> /// <returns></returns> public bool Test(RantDictionaryEntry entry, bool exclusive = false) { return(exclusive ? _items.Any() == entry.GetClasses().Any() && entry.GetClasses().All(c => _items.Any(item => item.Any(rule => rule.ShouldMatch && rule.Class == c))) : !_items.Any() || _items.All(set => set.Any(rule => entry.ContainsClass(rule.Class) == rule.ShouldMatch))); }
public bool Test(RantDictionaryEntry entry) { if (!EntryTypeDefFilter.Test(Filter, entry)) { return(true); } return(entry.GetClasses().Where(IsValidValue).Count() == 1); }
/// <summary> /// Removes the specified entry from the table. /// </summary> /// <param name="entry">The entry to remove from the table.</param> /// <returns>True if successfully removed; otherwise, False.</returns> public bool RemoveEntry(RantDictionaryEntry entry) { if (entry == null) { throw new ArgumentNullException(nameof(entry)); } if (!_entriesHash.Remove(entry)) { return(false); } _entriesList.Remove(entry); _dirty = true; return(true); }
/// <summary> /// Adds the specified entry to the table. /// </summary> /// <param name="entry">The entry to add to the table.</param> /// <returns>True if successfully added; otherwise, False.</returns> public bool AddEntry(RantDictionaryEntry entry) { if (entry == null) { throw new ArgumentNullException(nameof(entry)); } if (entry.TermCount != TermsPerEntry) { return(false); } if (!_entriesHash.Add(entry)) { return(false); } _entriesList.Add(entry); _dirty = true; return(true); }
public static bool AssociatesWith(this RantDictionaryEntry a, RantDictionaryEntry b) { if (a == null || b == null) return false; bool aNoneRequired = !a.GetRequiredClasses().Any(); bool bNoneRequired = !b.GetRequiredClasses().Any(); if (aNoneRequired && bNoneRequired) return true; // If both have no required classes, pass. // One or both have required classes. // Remove B optionals from A required. var aRequired = a.GetRequiredClasses().Except(b.GetOptionalClasses()); // Remove A optionals from B required. var bRequired = b.GetRequiredClasses().Except(a.GetOptionalClasses()); // Both should be either empty, or have exactly the same classes. return !aRequired.Except(bRequired).Any() && aRequired.Any() == bRequired.Any(); }
public static bool RelatesWith(this RantDictionaryEntry a, RantDictionaryEntry b) { if (a == null || b == null) return false; bool aNoneRequired = !a.GetRequiredClasses().Any(); bool bNoneRequired = !b.GetRequiredClasses().Any(); if (aNoneRequired && bNoneRequired) return true; // If both have no required classes, pass. // One or both have required classes. // Remove B optionals from A required. var aRequired = a.GetRequiredClasses().Except(b.GetOptionalClasses()); // Remove A optionals from B required. var bRequired = b.GetRequiredClasses().Except(a.GetOptionalClasses()); // Both should share at least one class. return aRequired.Intersect(bRequired).Any(); }
public static bool AssociatesWith(this RantDictionaryEntry a, RantDictionaryEntry b) { if (a == null || b == null) { return(false); } bool aNoneRequired = !a.GetRequiredClasses().Any(); bool bNoneRequired = !b.GetRequiredClasses().Any(); if (aNoneRequired && bNoneRequired) { return(true); // If both have no required classes, pass. } // One or both have required classes. // Remove B optionals from A required. var aRequired = a.GetRequiredClasses().Except(b.GetOptionalClasses()); // Remove A optionals from B required. var bRequired = b.GetRequiredClasses().Except(a.GetOptionalClasses()); // Both should be either empty, or have exactly the same classes. return(!aRequired.Except(bRequired).Any() && aRequired.Any() == bRequired.Any()); }
public static bool DivergesFrom(this RantDictionaryEntry a, RantDictionaryEntry b) { if (a == null || b == null) { return(false); } bool aNoneRequired = !a.GetRequiredClasses().Any(); bool bNoneRequired = !b.GetRequiredClasses().Any(); if (aNoneRequired && bNoneRequired) { return(true); // If both have no required classes, pass. } // One or both have required classes. // Remove B optionals from A required. var aRequired = a.GetRequiredClasses().Except(b.GetOptionalClasses()); // Remove A optionals from B required. var bRequired = b.GetRequiredClasses().Except(a.GetOptionalClasses()); // Both should be either empty, or differ by at least one class. return(aRequired.Except(bRequired).Any() || bRequired.Except(aRequired).Any()); }
public static bool RelatesWith(this RantDictionaryEntry a, RantDictionaryEntry b) { if (a == null || b == null) { return(false); } bool aNoneRequired = !a.GetRequiredClasses().Any(); bool bNoneRequired = !b.GetRequiredClasses().Any(); if (aNoneRequired && bNoneRequired) { return(true); // If both have no required classes, pass. } // One or both have required classes. // Remove B optionals from A required. var aRequired = a.GetRequiredClasses().Except(b.GetOptionalClasses()); // Remove A optionals from B required. var bRequired = b.GetRequiredClasses().Except(a.GetOptionalClasses()); // Both should share at least one class. return(aRequired.Intersect(bRequired).Any()); }
/// <summary> /// Determines whether a type should apply to the specifed entry according to the specified filter. /// </summary> /// <param name="filter">The filter to test with.</param> /// <param name="entry">The entry to test.</param> /// <returns></returns> public static bool Test(EntryTypeDefFilter filter, RantDictionaryEntry entry) => filter?.DoTest(entry) ?? false;
/// <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 package from the specified stream and returns it as a RantPackage object. /// </summary> /// <param name="source">The stream to load the package data from.</param> /// <returns></returns> public static RantPackage Load(Stream source) { using (var reader = new EasyReader(source)) { var magic = Encoding.ASCII.GetString(reader.ReadBytes(4)); if (magic == OLD_MAGIC) return LoadOldPackage(reader); if (magic != MAGIC) throw new InvalidDataException("File is corrupt."); var package = new RantPackage(); var version = reader.ReadUInt32(); if (version != PACKAGE_VERSION) throw new InvalidDataException("Invalid package version: " + version); var compress = reader.ReadBoolean(); var size = reader.ReadInt32(); var data = reader.ReadBytes(size); if (compress) data = EasyCompressor.Decompress(data); var doc = BsonDocument.Read(data); var info = doc["info"]; if (info == null) throw new InvalidDataException("Metadata is missing from package."); package.Title = info["title"]; package.ID = info["id"]; package.Version = RantPackageVersion.Parse(info["version"]); package.Description = info["description"]; package.Authors = (string[])info["authors"]; package.Tags = (string[])info["tags"]; var deps = info["dependencies"]; if (deps != null && deps.IsArray) { for(int i = 0; i < deps.Count; i++) { var dep = deps[i]; var depId = dep["id"].Value; var depVersion = dep["version"].Value; bool depAllowNewer = (bool)dep["allow-newer"].Value; package.AddDependency(new RantPackageDependency(depId.ToString(), depVersion.ToString()) { AllowNewer = depAllowNewer }); } } var patterns = doc["patterns"]; if(patterns != null) { var names = patterns.Keys; foreach (string name in names) package.AddPattern(new RantPattern(name, RantPatternOrigin.File, patterns[name])); } var tables = doc["tables"]; if(tables != null) { var names = tables.Keys; foreach(string name in names) { var table = tables[name]; string tableName = table["name"]; string[] tableSubs = (string[])table["subs"]; string[] hiddenClasses = (string[])table["hidden"]; var entries = new List<RantDictionaryEntry>(); var entryList = table["entries"]; for(var i = 0; i < entryList.Count; i++) { var loadedEntry = entryList[i]; int weight = 1; if (loadedEntry.HasKey("weight")) weight = (int)loadedEntry["weight"].Value; string[] requiredClasses = (string[])loadedEntry["classes"]; string[] optionalClasses = (string[])loadedEntry["optional_classes"]; var terms = new List<RantDictionaryTerm>(); var termList = loadedEntry["terms"]; for(var j = 0; j < termList.Count; j++) { var t = new RantDictionaryTerm(termList[j]["value"], termList[j]["pron"]); terms.Add(t); } var entry = new RantDictionaryEntry( terms.ToArray(), requiredClasses.Concat(optionalClasses.Select(x => x + "?")), weight ); entries.Add(entry); } var rantTable = new RantDictionaryTable( tableName, tableSubs, entries, hiddenClasses ); package.AddTable(rantTable); } } return package; } }
private bool DoTest(RantDictionaryEntry entry) { return(_filterParts == null || _filterParts.All(f => entry.ContainsClass(f.Item1) == f.Item2)); }
// Returns the classes of an object, but optional classes are postfixed with ? private static IEnumerable <string> GetClassesForExport(RantDictionaryEntry entry) { return(entry.GetRequiredClasses().Concat(entry.GetOptionalClasses().Select(x => x + "?"))); }
internal RantDictionaryEntry GetEntry(Carrier carrier, int subtypeIndex, IEnumerable <RantDictionaryEntry> pool, RNG rng) { if (carrier == null) { return(pool.PickEntry(rng)); } RantDictionaryEntry result = null; // Handle match carriers foreach (var match in carrier.GetCarriers(CarrierComponent.Match)) { if (_matchTable.TryGetValue(match, out result)) { return(result); } } // Handle associative carriers foreach (var assoc in carrier.GetCarriers(CarrierComponent.Associative)) { if (_assocTable.TryGetValue(assoc, out result)) { pool = pool.Where(e => e.AssociatesWith(result)); } break; } // Handle match-associative carriers foreach (var massoc in carrier.GetCarriers(CarrierComponent.MatchAssociative)) { if (_matchTable.TryGetValue(massoc, out result)) { pool = pool.Where(e => e.AssociatesWith(result)); } break; } // Handle unique carriers foreach (var unique in carrier.GetCarriers(CarrierComponent.Unique)) { HashSet <RantDictionaryEntry> usedSet; if (!_uniqueTable.TryGetValue(unique, out usedSet)) { usedSet = _uniqueTable[unique] = new HashSet <RantDictionaryEntry>(); } pool = pool.Except(usedSet); } // Handle match-unique carriers foreach (var munique in carrier.GetCarriers(CarrierComponent.Unique)) { if (_matchTable.TryGetValue(munique, out result)) { pool = pool.Where(e => e != result); } } // Handle relational carriers foreach (var relate in carrier.GetCarriers(CarrierComponent.Relational)) { if (_assocTable.TryGetValue(relate, out result)) { pool = pool.Where(e => e.RelatesWith(result)); } } // Handle match-relational carriers foreach (var relate in carrier.GetCarriers(CarrierComponent.MatchRelational)) { if (_matchTable.TryGetValue(relate, out result)) { pool = pool.Where(e => e.RelatesWith(result)); } } // Handle dissociative carriers foreach (var relate in carrier.GetCarriers(CarrierComponent.Dissociative)) { if (_assocTable.TryGetValue(relate, out result)) { pool = pool.Where(e => !e.RelatesWith(result)); } } // Handle match-dissociative carriers foreach (var relate in carrier.GetCarriers(CarrierComponent.MatchDissociative)) { if (_matchTable.TryGetValue(relate, out result)) { pool = pool.Where(e => !e.RelatesWith(result)); } } // Handle divergent carriers foreach (var diverge in carrier.GetCarriers(CarrierComponent.Divergent)) { if (_assocTable.TryGetValue(diverge, out result)) { pool = pool.Where(e => e.DivergesFrom(result)); } } // Handle match-divergent carriers foreach (var diverge in carrier.GetCarriers(CarrierComponent.MatchDivergent)) { if (_matchTable.TryGetValue(diverge, out result)) { pool = pool.Where(e => e.DivergesFrom(result)); } } result = pool.PickEntry(rng); // Handle rhyme carrier foreach (var rhyme in carrier.GetCarriers(CarrierComponent.Rhyme)) { _ <RantDictionaryTerm, HashSet <RantDictionaryEntry> > rhymeState; if (!_rhymeTable.TryGetValue(rhyme, out rhymeState)) { result = pool .Where(e => !Util.IsNullOrWhiteSpace(e.Terms[subtypeIndex].Pronunciation)) .PickEntry(rng); if (result == null) { return(null); } _rhymeTable[rhyme] = _.Create(result.Terms[subtypeIndex], new HashSet <RantDictionaryEntry>(new[] { result })); break; } result = pool.Except(rhymeState.Item2) .Where(e => !Util.IsNullOrWhiteSpace(e.Terms[subtypeIndex].Pronunciation) && _rhymer.Rhyme(rhymeState.Item1, e.Terms[subtypeIndex])) .PickEntry(rng); if (result != null) { rhymeState.Item2.Add(result); } break; // Ignore any extra rhyme carriers } if (result == null) { return(null); } foreach (var a in carrier.GetCarriers(CarrierComponent.Associative)) { if (!_assocTable.ContainsKey(a)) { _assocTable[a] = result; } } foreach (var a in carrier.GetCarriers(CarrierComponent.Dissociative)) { if (!_assocTable.ContainsKey(a)) { _assocTable[a] = result; } } foreach (var a in carrier.GetCarriers(CarrierComponent.Divergent)) { if (!_assocTable.ContainsKey(a)) { _assocTable[a] = result; } } foreach (var a in carrier.GetCarriers(CarrierComponent.Relational)) { if (!_assocTable.ContainsKey(a)) { _assocTable[a] = result; } } foreach (var unique in carrier.GetCarriers(CarrierComponent.Unique)) { _uniqueTable[unique].Add(result); } foreach (var match in carrier.GetCarriers(CarrierComponent.Match)) { _matchTable[match] = result; break; } return(result); }
/// <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); }
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> /// Checks if the table contains the specified entry. /// </summary> /// <param name="entry">The entry to search for.</param> /// <returns>True if found, False if not.</returns> public bool ContainsEntry(RantDictionaryEntry entry) { return(_entriesHash.Contains(entry ?? throw new ArgumentNullException(nameof(entry)))); }
public static bool DivergesFrom(this RantDictionaryEntry a, RantDictionaryEntry b) { if (a == null || b == null) return false; bool aNoneRequired = !a.GetRequiredClasses().Any(); bool bNoneRequired = !b.GetRequiredClasses().Any(); if (aNoneRequired && bNoneRequired) return true; // If both have no required classes, pass. // One or both have required classes. // Remove B optionals from A required. var aRequired = a.GetRequiredClasses().Except(b.GetOptionalClasses()); // Remove A optionals from B required. var bRequired = b.GetRequiredClasses().Except(a.GetOptionalClasses()); // Both should be either empty, or differ by at least one class. return aRequired.Except(bRequired).Any() || bRequired.Except(aRequired).Any(); }
private bool DoTest(RantDictionaryEntry entry) { return _filterParts == null || _filterParts.All(f => entry.ContainsClass(f.Item1) == f.Item2); }
public bool Test(RantDictionaryEntry entry) { if (!EntryTypeDefFilter.Test(Filter, entry)) return true; return entry.GetClasses().Where(IsValidValue).Count() == 1; }
/// <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 package from the specified stream and returns it as a RantPackage object. /// </summary> /// <param name="source">The stream to load the package data from.</param> /// <returns></returns> public static RantPackage Load(Stream source) { using (var reader = new EasyReader(source)) { if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != Magic) throw new InvalidDataException("File is corrupt."); int numPatterns = reader.ReadInt32(); int numTables = reader.ReadInt32(); if (numPatterns < 0 || numTables < 0) throw new InvalidDataException("File is corrupt."); var pkg = new RantPackage(); // Patterns for (int i = 0; i < numPatterns; i++) { var name = reader.ReadString(); var code = reader.ReadString(); pkg.AddPattern(new RantPattern(name, RantPatternSource.String, code)); } // Tables for (int i = 0; i < numTables; i++) { var name = reader.ReadString(); var subs = reader.ReadStringArray(); int numEntries = reader.ReadInt32(); var hiddenClasses = reader.ReadStringArray(); var entries = new RantDictionaryEntry[numEntries]; for (int j = 0; j < numEntries; j++) { int weight = reader.ReadInt32(); bool flags = reader.ReadBoolean(); // unused int numTerms = reader.ReadInt32(); var terms = new RantDictionaryTerm[numTerms]; for (int k = 0; k < numTerms; k++) { var value = reader.ReadString(); var pron = reader.ReadString(); terms[k] = new RantDictionaryTerm(value, pron); } var classes = reader.ReadStringArray(); entries[j] = new RantDictionaryEntry(terms, classes, weight); } pkg.AddTable(new RantDictionaryTable(name, subs, entries, hiddenClasses)); } return pkg; } }
/// <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); }
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); } } }
// Returns the classes of an object, but optional classes are postfixed with ? private static IEnumerable<string> GetClassesForExport(RantDictionaryEntry entry) { return entry.GetRequiredClasses().Concat(entry.GetOptionalClasses().Select(x => x + "?")); }