diff --git a/src/War3Net.IO.Slk/SylkParser.cs b/src/War3Net.IO.Slk/SylkParser.cs index 4487e66f..82a42287 100644 --- a/src/War3Net.IO.Slk/SylkParser.cs +++ b/src/War3Net.IO.Slk/SylkParser.cs @@ -6,138 +6,197 @@ // ------------------------------------------------------------------------------ using System; +using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Text; namespace War3Net.IO.Slk { public sealed class SylkParser { - private SylkTable _table; - private int? _lastY; - - public SylkParser() + public SylkTable Parse(Stream input, bool leaveOpen = false) { - _lastY = null; + var lines = new List(); + using var reader = new StreamReader(input, Encoding.UTF8, true, 1024, leaveOpen); + { + string line; + while ((line = reader.ReadLine()) != null) + { + lines.Add(line); + } + } + + return Parse(lines); } - public SylkTable Parse(Stream input, bool leaveOpen = false) + public SylkTable Parse(List lines) { - using var reader = new StreamReader(input, Encoding.UTF8, true, 1024, leaveOpen); + int? maxX = null; + int? maxY = null; - var isOnFirstLine = true; - while (true) + var bLine = lines.FirstOrDefault(x => x.StartsWith("B;", StringComparison.InvariantCultureIgnoreCase)); + if (!string.IsNullOrWhiteSpace(bLine)) { - var line = reader.ReadLine(); - var fields = line.Split(';'); - var recordType = fields[0]; - - string GetField(string fieldName, bool mandatory) + var parts = bLine.Split(';', StringSplitOptions.TrimEntries); + foreach (var part in parts) { - foreach (var field in fields) + if (part.StartsWith("X", StringComparison.InvariantCultureIgnoreCase)) { - if (field.StartsWith(fieldName)) + if (int.TryParse(part.Substring(1), out int x)) { - return field.Substring(fieldName.Length); + maxX = x; } } - - if (mandatory) + else if (part.StartsWith("Y", StringComparison.InvariantCultureIgnoreCase)) { - throw new InvalidDataException($"Record does not contain mandatory field of type '{fieldName}'."); + if (int.TryParse(part.Substring(1), out int y)) + { + maxY = y; + } + } + else if (part.StartsWith("D", StringComparison.InvariantCultureIgnoreCase)) + { + var dContent = part.Substring(1).Trim(); + var dParts = dContent.Split(' '); + if (dParts.Length == 2) + { + if (int.TryParse(dParts[0], out int y)) + { + maxY ??= y; + } + if (int.TryParse(dParts[1], out int x)) + { + maxX ??= x; + } + } + else if (dParts.Length == 4) + { + int.TryParse(dParts[0], out int startY); + int.TryParse(dParts[1], out int startX); + if (int.TryParse(dParts[2], out int y)) + { + maxY ??= (y - startY); + } + if (int.TryParse(dParts[3], out int x)) + { + maxX ??= (x - startX); + } + } } - - return null; } + } - if (isOnFirstLine) + var _table = new SylkTable(maxX ?? 0, maxY ?? 0); + + + int nextX = 0; + int nextY = 0; + + foreach (var line in lines) + { + var isCell = line.StartsWith("C;", StringComparison.InvariantCultureIgnoreCase); + var isFormatting = line.StartsWith("F;", StringComparison.InvariantCultureIgnoreCase); + if (isCell || isFormatting) { - isOnFirstLine = false; - if (recordType != "ID") + int? x = null; + int? y = null; + object value = null; + + var parts = line.Split(";", StringSplitOptions.TrimEntries); + foreach (var part in parts) { - throw new InvalidDataException("SYLK file must start with 'ID'."); + if (part.StartsWith("X", StringComparison.InvariantCultureIgnoreCase)) + { + if (int.TryParse(part.Substring(1), out int parsedX)) + { + x = parsedX - 1; + } + } + else if (part.StartsWith("Y", StringComparison.InvariantCultureIgnoreCase)) + { + if (int.TryParse(part.Substring(1), out int parsedY)) + { + y = parsedY - 1; + } + } + else if (part.StartsWith("K", StringComparison.InvariantCultureIgnoreCase)) + { + value = ParseValueString(part.Substring(1)); + } } - GetField("P", true); - } - else - { - switch (recordType) + if (isFormatting && x == null && y == null) { - case "ID": - throw new InvalidDataException("Record type 'ID' can only occur on the first line."); + continue; + } - case "B": - if (_table != null) - { - throw new InvalidDataException("Only one record of type 'B' may be present."); - } + x ??= nextX; + y ??= nextY; - _table = new SylkTable(int.Parse(GetField("X", true)), int.Parse(GetField("Y", true))); - break; + nextX = x.Value; + nextY = y.Value; - case "C": - if (_table == null) - { - throw new InvalidDataException("Unable to parse record of type 'C' before encountering a record of type 'B'."); - } + if (isCell) + { + nextX++; + } - SetCellContent(GetField("X", true), GetField("Y", false), GetField("K", false)); - break; + if (maxX.HasValue && nextX >= maxX) + { + nextX = 0; + nextY++; + } - case "E": - return _table; + if (value != null) + { + if (x.Value >= _table.Width || y.Value >= _table.Height) + { + _table.Resize(Math.Max(x.Value+1, _table.Width), Math.Max(y.Value+1, _table.Height)); + } - default: - throw new NotSupportedException($"Support for record type '{recordType}' is not implemented. Only records of type 'ID', 'B', 'C', and 'E' are supported."); + _table[x.Value, y.Value] = value; } } } + + return _table; } - /// The cell's 1-indexed X position. - /// The cell's 1-indexed Y position. - private void SetCellContent(string x, string? y, string value) + private object ParseValueString(string value) { - if (y == null && _lastY == null) + if (value.StartsWith('"')) { - throw new InvalidDataException("Row for cell is not defined."); - } - - var xi = int.Parse(x, NumberStyles.Integer, CultureInfo.InvariantCulture) - 1; - var yi = y == null ? _lastY.Value : (int.Parse(y, NumberStyles.Integer, CultureInfo.InvariantCulture) - 1); + if (!value.EndsWith('"')) + { + value = value + "\""; + } - if (value.StartsWith('"') && value.EndsWith('"')) - { - _table[xi, yi] = value[1..^1]; + return value.Substring(1, value.Length - 2); } - else if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var @int)) + else if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) { - _table[xi, yi] = @int; + return intValue; } - else if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var @float)) + else if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var floatValue)) { - _table[xi, yi] = @float; + return floatValue; } else if (string.Equals(value, bool.TrueString, StringComparison.OrdinalIgnoreCase)) { - _table[xi, yi] = true; + return true; } else if (string.Equals(value, bool.FalseString, StringComparison.OrdinalIgnoreCase)) { - _table[xi, yi] = false; - } - else if (string.Equals(value, "#VALUE!", StringComparison.Ordinal) || string.Equals(value, "#REF!", StringComparison.Ordinal)) - { - _table[xi, yi] = 0; + return false; } - else + else if (string.Equals(value, "#VALUE!", StringComparison.Ordinal) || string.Equals(value, "#REF!", StringComparison.Ordinal) || string.IsNullOrEmpty(value)) { - throw new NotSupportedException($"Unable to parse value '{value}'. Can only parse strings, integers, floats, and booleans."); + return 0; } - _lastY = yi; + return null; } } } \ No newline at end of file diff --git a/src/War3Net.IO.Slk/SylkTable.cs b/src/War3Net.IO.Slk/SylkTable.cs index 8aae9208..75a160e7 100644 --- a/src/War3Net.IO.Slk/SylkTable.cs +++ b/src/War3Net.IO.Slk/SylkTable.cs @@ -14,9 +14,9 @@ namespace War3Net.IO.Slk { public sealed class SylkTable : IEnumerable { - private readonly int _width; - private readonly int _height; - private readonly object[,] _values; + private int _width; + private int _height; + private object[,] _values; private int _rows; private int _columns; @@ -28,6 +28,28 @@ public SylkTable(int width, int height) _values = new object[_width, _height]; } + public void Resize(int width, int height) + { + var oldWidth = _width; + var oldHeight = _height; + var oldValues = _values; + + _width = width; + _height = height; + _values = new object[_width, _height]; + + for (int column = 0; column < Math.Min(_width, oldWidth); column++) + { + for (int row = 0; row < Math.Min(height, oldHeight); row++) + { + _values[column, row] = oldValues[column, row]; + } + } + + _rows = Math.Min(_rows, _height-1); + _columns = Math.Min(_columns, _width-1); + } + /// /// Gets the maximum amount of columns that can fit in the . ///