Refactored parsing services into separate project

This commit is contained in:
Ben Wallis
2016-01-31 11:56:55 +00:00
parent 86dc03f4ff
commit 8bfbe7cc66
36 changed files with 842 additions and 147 deletions

View File

@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Filtration.ObjectModel;
using Filtration.Parser.Interface.Services;
namespace Filtration.Parser.Services
{
internal class BlockGroupHierarchyBuilder : IBlockGroupHierarchyBuilder
{
private ItemFilterBlockGroup _rootBlockGroup;
public void Initialise(ItemFilterBlockGroup rootBlockGroup)
{
_rootBlockGroup = rootBlockGroup;
}
public void Cleanup()
{
_rootBlockGroup = null;
}
public ItemFilterBlockGroup IntegrateStringListIntoBlockGroupHierarchy(IEnumerable<string> groupStrings)
{
if (_rootBlockGroup == null)
{
throw new Exception("BlockGroupHierarchyBuilder must be initialised with root BlockGroup before use");
}
return IntegrateStringListIntoBlockGroupHierarchy(groupStrings, _rootBlockGroup);
}
public ItemFilterBlockGroup IntegrateStringListIntoBlockGroupHierarchy(IEnumerable<string> groupStrings, ItemFilterBlockGroup startItemGroup)
{
var inputGroups = groupStrings.ToList();
var firstGroup = inputGroups.First().Trim();
if (firstGroup.StartsWith("~"))
{
firstGroup = firstGroup.Substring(1);
}
ItemFilterBlockGroup matchingChildItemGroup = null;
if (startItemGroup.ChildGroups.Count(g => g.GroupName == firstGroup) > 0)
{
matchingChildItemGroup = startItemGroup.ChildGroups.First(c => c.GroupName == firstGroup);
}
if (matchingChildItemGroup == null)
{
var newItemGroup = CreateBlockGroup(inputGroups.First().Trim(), startItemGroup);
startItemGroup.ChildGroups.Add(newItemGroup);
inputGroups = inputGroups.Skip(1).ToList();
return inputGroups.Count > 0 ? IntegrateStringListIntoBlockGroupHierarchy(inputGroups, newItemGroup) : newItemGroup;
}
inputGroups = inputGroups.Skip(1).ToList();
return inputGroups.Count > 0 ? IntegrateStringListIntoBlockGroupHierarchy(inputGroups, matchingChildItemGroup) : matchingChildItemGroup;
}
private ItemFilterBlockGroup CreateBlockGroup(string groupNameString, ItemFilterBlockGroup parentGroup)
{
var advanced = false;
if (groupNameString.StartsWith("~"))
{
groupNameString = groupNameString.Substring(1);
advanced = true;
}
if (parentGroup.Advanced)
{
advanced = true;
}
return new ItemFilterBlockGroup(groupNameString, parentGroup, advanced);
}
}
}

View File

@@ -0,0 +1,446 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Media;
using Filtration.Common.Utilities;
using Filtration.ObjectModel;
using Filtration.ObjectModel.BlockItemBaseTypes;
using Filtration.ObjectModel.BlockItemTypes;
using Filtration.ObjectModel.Enums;
using Filtration.ObjectModel.Extensions;
using Filtration.ObjectModel.ThemeEditor;
using Filtration.Parser.Interface.Services;
namespace Filtration.Parser.Services
{
internal class ItemFilterBlockTranslator : IItemFilterBlockTranslator
{
private readonly IBlockGroupHierarchyBuilder _blockGroupHierarchyBuilder;
private const string _indent = " ";
private readonly string _newLine = Environment.NewLine + _indent;
private readonly string _disabledNewLine = Environment.NewLine + "#" + _indent;
private ThemeComponentCollection _masterComponentCollection;
public ItemFilterBlockTranslator(IBlockGroupHierarchyBuilder blockGroupHierarchyBuilder)
{
_blockGroupHierarchyBuilder = blockGroupHierarchyBuilder;
}
// This method converts a string into a ItemFilterBlock. This is used for pasting ItemFilterBlocks
// and reading ItemFilterScripts from a file.
public IItemFilterBlock TranslateStringToItemFilterBlock(string inputString, ThemeComponentCollection masterComponentCollection)
{
_masterComponentCollection = masterComponentCollection;
var block = new ItemFilterBlock();
var showHideFound = false;
foreach (var line in new LineReader(() => new StringReader(inputString)))
{
if (line.StartsWith(@"# Section:"))
{
var section = new ItemFilterSection
{
Description = line.Substring(line.IndexOf(":", StringComparison.Ordinal) + 1).Trim()
};
return section;
}
if (line.StartsWith(@"#") && !showHideFound)
{
block.Description = line.TrimStart('#').TrimStart(' ');
continue;
}
var adjustedLine = line.Replace("#", " # ");
var trimmedLine = adjustedLine.TrimStart(' ');
var spaceOrEndOfLinePos = trimmedLine.IndexOf(" ", StringComparison.Ordinal) > 0 ? trimmedLine.IndexOf(" ", StringComparison.Ordinal) : trimmedLine.Length;
var lineOption = trimmedLine.Substring(0, spaceOrEndOfLinePos);
switch (lineOption)
{
case "Show":
showHideFound = true;
block.Action = BlockAction.Show;
block.Enabled = true;
AddBlockGroupToBlock(block, trimmedLine);
break;
case "Hide":
showHideFound = true;
block.Action = BlockAction.Hide;
block.Enabled = true;
AddBlockGroupToBlock(block, trimmedLine);
break;
case "ShowDisabled":
showHideFound = true;
block.Action = BlockAction.Show;
block.Enabled = false;
AddBlockGroupToBlock(block, trimmedLine);
break;
case "HideDisabled":
showHideFound = true;
block.Action = BlockAction.Hide;
block.Enabled = false;
AddBlockGroupToBlock(block, trimmedLine);
break;
case "ItemLevel":
{
AddNumericFilterPredicateItemToBlockItems<ItemLevelBlockItem>(block, trimmedLine);
break;
}
case "DropLevel":
{
AddNumericFilterPredicateItemToBlockItems<DropLevelBlockItem>(block, trimmedLine);
break;
}
case "Quality":
{
AddNumericFilterPredicateItemToBlockItems<QualityBlockItem>(block,trimmedLine);
break;
}
case "Rarity":
{
var blockItemValue = new RarityBlockItem();
var result = Regex.Match(trimmedLine, @"^\w+\s+([><!=]{0,2})\s*(\w+)$");
if (result.Groups.Count == 3)
{
blockItemValue.FilterPredicate.PredicateOperator =
EnumHelper.GetEnumValueFromDescription<FilterPredicateOperator>(string.IsNullOrEmpty(result.Groups[1].Value) ? "=" : result.Groups[1].Value);
blockItemValue.FilterPredicate.PredicateOperand =
(int)EnumHelper.GetEnumValueFromDescription<ItemRarity>(result.Groups[2].Value);
}
block.BlockItems.Add(blockItemValue);
break;
}
case "Class":
{
AddStringListItemToBlockItems<ClassBlockItem>(block, trimmedLine);
break;
}
case "BaseType":
{
AddStringListItemToBlockItems<BaseTypeBlockItem>(block, trimmedLine);
break;
}
case "Sockets":
{
AddNumericFilterPredicateItemToBlockItems<SocketsBlockItem>(block, trimmedLine);
break;
}
case "LinkedSockets":
{
AddNumericFilterPredicateItemToBlockItems<LinkedSocketsBlockItem>(block,trimmedLine);
break;
}
case "Width":
{
AddNumericFilterPredicateItemToBlockItems<WidthBlockItem>(block, trimmedLine);
break;
}
case "Height":
{
AddNumericFilterPredicateItemToBlockItems<HeightBlockItem>(block, trimmedLine);
break;
}
case "SocketGroup":
{
AddStringListItemToBlockItems<SocketGroupBlockItem>(block, trimmedLine);
break;
}
case "SetTextColor":
{
// Only ever use the last SetTextColor item encountered as multiples aren't valid.
RemoveExistingBlockItemsOfType<TextColorBlockItem>(block);
AddColorItemToBlockItems<TextColorBlockItem>(block, trimmedLine);
break;
}
case "SetBackgroundColor":
{
// Only ever use the last SetBackgroundColor item encountered as multiples aren't valid.
RemoveExistingBlockItemsOfType<BackgroundColorBlockItem>(block);
AddColorItemToBlockItems<BackgroundColorBlockItem>(block, trimmedLine);
break;
}
case "SetBorderColor":
{
// Only ever use the last SetBorderColor item encountered as multiples aren't valid.
RemoveExistingBlockItemsOfType<BorderColorBlockItem>(block);
AddColorItemToBlockItems<BorderColorBlockItem>(block, trimmedLine);
break;
}
case "SetFontSize":
{
// Only ever use the last SetFontSize item encountered as multiples aren't valid.
RemoveExistingBlockItemsOfType<FontSizeBlockItem>(block);
var match = Regex.Match(trimmedLine, @"\s+(\d+)");
if (match.Success)
{
var blockItemValue = new FontSizeBlockItem(Convert.ToInt16(match.Value));
block.BlockItems.Add(blockItemValue);
}
break;
}
case "PlayAlertSound":
{
// Only ever use the last PlayAlertSound item encountered as multiples aren't valid.
RemoveExistingBlockItemsOfType<SoundBlockItem>(block);
var matches = Regex.Matches(trimmedLine, @"\s+(\d+)");
switch (matches.Count)
{
case 1:
if (matches[0].Success)
{
var blockItemValue = new SoundBlockItem
{
Value = Convert.ToInt16(matches[0].Value),
SecondValue = 79
};
block.BlockItems.Add(blockItemValue);
}
break;
case 2:
if (matches[0].Success && matches[1].Success)
{
var blockItemValue = new SoundBlockItem
{
Value = Convert.ToInt16(matches[0].Value),
SecondValue = Convert.ToInt16(matches[1].Value)
};
block.BlockItems.Add(blockItemValue);
}
break;
}
break;
}
}
}
return block;
}
private static void RemoveExistingBlockItemsOfType<T>(IItemFilterBlock block)
{
var existingBlockItemCount = block.BlockItems.Count(b => b.GetType() == typeof(T));
if (existingBlockItemCount > 0)
{
var existingBlockItem = block.BlockItems.First(b => b.GetType() == typeof(T));
block.BlockItems.Remove(existingBlockItem);
}
}
private static void AddNumericFilterPredicateItemToBlockItems<T>(IItemFilterBlock block, string inputString) where T : NumericFilterPredicateBlockItem
{
var blockItem = Activator.CreateInstance<T>();
SetNumericFilterPredicateFromString(blockItem.FilterPredicate, inputString);
block.BlockItems.Add(blockItem);
}
private static void SetNumericFilterPredicateFromString(NumericFilterPredicate predicate, string inputString)
{
var result = Regex.Match(inputString, @"^\w+\s+([><=]{0,2})\s*(\d{0,3})$");
if (result.Groups.Count != 3) return;
predicate.PredicateOperator =
EnumHelper.GetEnumValueFromDescription<FilterPredicateOperator>(string.IsNullOrEmpty(result.Groups[1].Value) ? "=" : result.Groups[1].Value);
predicate.PredicateOperand = Convert.ToInt16(result.Groups[2].Value);
}
private static void AddStringListItemToBlockItems<T>(IItemFilterBlock block, string inputString) where T : StringListBlockItem
{
var blockItem = Activator.CreateInstance<T>();
PopulateListFromString(blockItem.Items, inputString.Substring(inputString.IndexOf(" ", StringComparison.Ordinal) + 1).Trim());
block.BlockItems.Add(blockItem);
}
private static void PopulateListFromString(ICollection<string> list, string inputString)
{
var result = Regex.Matches(inputString, @"[^\s""]+|""([^""]*)""");
foreach (Match match in result)
{
list.Add(match.Groups[1].Success
? match.Groups[1].Value
: match.Groups[0].Value);
}
}
private void AddColorItemToBlockItems<T>(IItemFilterBlock block, string inputString) where T : ColorBlockItem
{
block.BlockItems.Add(GetColorBlockItemFromString<T>(inputString));
}
private T GetColorBlockItemFromString<T>(string inputString) where T: ColorBlockItem
{
var blockItem = Activator.CreateInstance<T>();
var result = Regex.Matches(inputString, @"([\w\s]*)[#]?(.*)");
blockItem.Color = GetColorFromString(result[0].Groups[1].Value);
var componentName = result[0].Groups[2].Value.Trim();
if (!string.IsNullOrEmpty(componentName))
{
ThemeComponentType componentType;
if (typeof(T) == typeof(TextColorBlockItem))
{
componentType = ThemeComponentType.TextColor;
}
else if (typeof(T) == typeof(BackgroundColorBlockItem))
{
componentType = ThemeComponentType.BackgroundColor;
}
else if (typeof(T) == typeof(BorderColorBlockItem))
{
componentType = ThemeComponentType.BorderColor;
}
else
{
throw new Exception("Parsing error - unknown theme component type");
}
if (_masterComponentCollection != null)
{
blockItem.ThemeComponent = _masterComponentCollection.AddComponent(componentType, componentName,
blockItem.Color);
}
}
return blockItem;
}
public void ReplaceColorBlockItemsFromString(ObservableCollection<IItemFilterBlockItem> blockItems, string inputString)
{
// Reverse iterate to remove existing IAudioVisualBlockItems
for (var idx = blockItems.Count - 1; idx >= 0; idx--)
{
if (blockItems[idx] is IAudioVisualBlockItem)
{
blockItems.RemoveAt(idx);
}
}
foreach (var line in new LineReader(() => new StringReader(inputString)))
{
var matches = Regex.Match(line, @"(\w+)");
switch (matches.Value)
{
case "SetTextColor":
{
blockItems.Add(GetColorBlockItemFromString<TextColorBlockItem>(line));
break;
}
case "SetBackgroundColor":
{
blockItems.Add(GetColorBlockItemFromString<BackgroundColorBlockItem>(line));
break;
}
case "SetBorderColor":
{
blockItems.Add(GetColorBlockItemFromString<BorderColorBlockItem>(line));
break;
}
case "SetFontSize":
{
var match = Regex.Match(line, @"\s+(\d+)");
if (!match.Success) break;
blockItems.Add(new FontSizeBlockItem(Convert.ToInt16(match.Value)));
break;
}
}
}
}
private void AddBlockGroupToBlock(IItemFilterBlock block, string inputString)
{
var blockGroupStart = inputString.IndexOf("#", StringComparison.Ordinal);
if (blockGroupStart <= 0) return;
var blockGroupText = inputString.Substring(blockGroupStart + 1);
var blockGroups = blockGroupText.Split('-').ToList();
if (blockGroups.Count(b => !string.IsNullOrEmpty(b.Trim())) > 0)
{
block.BlockGroup = _blockGroupHierarchyBuilder.IntegrateStringListIntoBlockGroupHierarchy(blockGroups);
block.BlockGroup.IsChecked = block.Action == BlockAction.Show;
}
}
private static Color GetColorFromString(string inputString)
{
var argbValues = Regex.Matches(inputString, @"\s+(\d+)");
switch (argbValues.Count)
{
case 3:
return new Color
{
A = byte.MaxValue,
R = Convert.ToByte(argbValues[0].Value),
G = Convert.ToByte(argbValues[1].Value),
B = Convert.ToByte(argbValues[2].Value)
};
case 4:
return new Color
{
R = Convert.ToByte(argbValues[0].Value),
G = Convert.ToByte(argbValues[1].Value),
B = Convert.ToByte(argbValues[2].Value),
A = Convert.ToByte(argbValues[3].Value)
};
}
return new Color();
}
// This method converts an ItemFilterBlock object into a string. This is used for copying a ItemFilterBlock
// to the clipboard, and when saving a ItemFilterScript.
public string TranslateItemFilterBlockToString(IItemFilterBlock block)
{
if (block.GetType() == typeof (ItemFilterSection))
{
return "# Section: " + block.Description;
}
var outputString = string.Empty;
if (!block.Enabled)
{
outputString += "#Disabled Block Start" + Environment.NewLine;
}
if (!string.IsNullOrEmpty(block.Description))
{
outputString += "# " + block.Description + Environment.NewLine;
}
outputString += (!block.Enabled ? "#" : string.Empty) + block.Action.GetAttributeDescription();
if (block.BlockGroup != null)
{
outputString += " # " + block.BlockGroup;
}
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (var blockItem in block.BlockItems.Where(b => b.GetType() != typeof(ActionBlockItem)).OrderBy(b => b.SortOrder))
{
if (blockItem.OutputText != string.Empty)
{
outputString += (!block.Enabled ? _disabledNewLine : _newLine) + blockItem.OutputText;
}
}
if (!block.Enabled)
{
outputString += Environment.NewLine + "#Disabled Block End";
}
return outputString;
}
}
}

View File

@@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Filtration.Common.Utilities;
using Filtration.ObjectModel;
using Filtration.Parser.Interface.Services;
using Filtration.Properties;
namespace Filtration.Parser.Services
{
internal class ItemFilterScriptTranslator : IItemFilterScriptTranslator
{
private readonly IItemFilterBlockTranslator _blockTranslator;
private readonly IBlockGroupHierarchyBuilder _blockGroupHierarchyBuilder;
public ItemFilterScriptTranslator(IItemFilterBlockTranslator blockTranslator,
IBlockGroupHierarchyBuilder blockGroupHierarchyBuilder)
{
_blockTranslator = blockTranslator;
_blockGroupHierarchyBuilder = blockGroupHierarchyBuilder;
}
public string PreprocessDisabledBlocks(string inputString)
{
bool inDisabledBlock = false;
var showHideFound = false;
var lines = Regex.Split(inputString, "\r\n|\r|\n").ToList();
var linesToRemove = new List<int>();
for (var i = 0; i < lines.Count; i++)
{
if (lines[i].StartsWith("#Disabled Block Start"))
{
inDisabledBlock = true;
linesToRemove.Add(i);
continue;
}
if (inDisabledBlock)
{
if (lines[i].StartsWith("#Disabled Block End"))
{
inDisabledBlock = false;
showHideFound = false;
linesToRemove.Add(i);
continue;
}
lines[i] = lines[i].TrimStart('#');
lines[i] = lines[i].Replace("#", " # ");
var spaceOrEndOfLinePos = lines[i].IndexOf(" ", StringComparison.Ordinal) > 0 ? lines[i].IndexOf(" ", StringComparison.Ordinal) : lines[i].Length;
var lineOption = lines[i].Substring(0, spaceOrEndOfLinePos);
// If we haven't found a Show or Hide line yet, then this is probably the block comment.
// Put its # back on and skip to the next line.
if (lineOption != "Show" && lineOption != "Hide" && showHideFound == false)
{
lines[i] = "#" + lines[i];
continue;
}
if (lineOption == "Show")
{
lines[i] = lines[i].Replace("Show", "ShowDisabled");
showHideFound = true;
}
else if (lineOption == "Hide")
{
lines[i] = lines[i].Replace("Hide", "HideDisabled");
showHideFound = true;
}
}
}
for (var i = linesToRemove.Count - 1; i >= 0; i--)
{
lines.RemoveAt(linesToRemove[i]);
}
return lines.Aggregate((c, n) => c + Environment.NewLine + n);
}
public ItemFilterScript TranslateStringToItemFilterScript(string inputString)
{
var script = new ItemFilterScript();
_blockGroupHierarchyBuilder.Initialise(script.ItemFilterBlockGroups.First());
inputString = inputString.Replace("\t", "");
if (inputString.Contains("#Disabled Block Start"))
{
inputString = PreprocessDisabledBlocks(inputString);
}
var conditionBoundaries = IdentifyBlockBoundaries(inputString);
var lines = Regex.Split(inputString, "\r\n|\r|\n");
// Process the script header
for (var i = 0; i < conditionBoundaries.First.Value; i++)
{
if (lines[i].StartsWith("#"))
{
script.Description += lines[i].Substring(1).Trim(' ') + Environment.NewLine;
}
}
if (!string.IsNullOrEmpty(script.Description))
{
script.Description = script.Description.TrimEnd('\n').TrimEnd('\r');
}
// Extract each block from between boundaries and translate it into a ItemFilterBlock object
// and add that object to the ItemFilterBlocks list
for (var boundary = conditionBoundaries.First; boundary != null; boundary = boundary.Next)
{
var begin = boundary.Value;
var end = boundary.Next?.Value ?? lines.Length;
var block = new string[end - begin];
Array.Copy(lines, begin, block, 0, end - begin);
var blockString = string.Join("\r\n", block);
script.ItemFilterBlocks.Add(_blockTranslator.TranslateStringToItemFilterBlock(blockString, script.ThemeComponents));
}
_blockGroupHierarchyBuilder.Cleanup();
return script;
}
private static LinkedList<int> IdentifyBlockBoundaries(string inputString)
{
var blockBoundaries = new LinkedList<int>();
var previousLine = string.Empty;
var currentLine = 0;
foreach (var line in new LineReader(() => new StringReader(inputString)))
{
currentLine++;
var trimmedLine = line.TrimStart(' ').TrimEnd(' ');
if (trimmedLine.StartsWith("Show") || trimmedLine.StartsWith("Hide") ||
trimmedLine.StartsWith("# Section:"))
{
// If the line previous to the Show or Hide line is a comment then we should include that in the block
// as it represents the block description.
// currentLine > 2 caters for an edge case where the script description is a single line and the first
// block has no description. This prevents the script description from being assigned to the first block's description.
blockBoundaries.AddLast(previousLine.StartsWith("#") && !previousLine.StartsWith("# Section:") &&
currentLine > 2
? currentLine - 2
: currentLine - 1);
}
previousLine = line;
}
return blockBoundaries;
}
public string TranslateItemFilterScriptToString(ItemFilterScript script)
{
var outputString = string.Empty;
outputString += "# Script edited with Filtration - https://github.com/ben-wallis/Filtration" +
Environment.NewLine;
if (!string.IsNullOrEmpty(script.Description))
{
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (var line in new LineReader(() => new StringReader(script.Description)))
{
if (!line.Contains("Script edited with Filtration"))
{
outputString += "# " + line + Environment.NewLine;
}
}
outputString += Environment.NewLine;
}
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (var block in script.ItemFilterBlocks)
{
outputString += _blockTranslator.TranslateItemFilterBlockToString(block) + Environment.NewLine;
if (Settings.Default.ExtraLineBetweenBlocks)
{
outputString += Environment.NewLine;
}
}
return outputString;
}
}
}