mirror of
https://github.com/waynebian01/Shigure.git
synced 2026-09-03 07:15:24 +08:00
395 lines
11 KiB
C#
395 lines
11 KiB
C#
namespace Shigure;
|
|
|
|
/// <summary>
|
|
/// 轻量 Lua 表字面量解析:足够读取 Fuyutsui ClassBlocks 声明。
|
|
/// </summary>
|
|
internal static class LuaLiteParser
|
|
{
|
|
public abstract class Value;
|
|
|
|
public sealed class NilValue : Value
|
|
{
|
|
public static readonly NilValue Instance = new();
|
|
private NilValue() { }
|
|
}
|
|
|
|
public sealed class BoolValue(bool value) : Value
|
|
{
|
|
public bool Value { get; } = value;
|
|
}
|
|
|
|
public sealed class NumberValue(double value) : Value
|
|
{
|
|
public double Value { get; } = value;
|
|
public int AsInt() => (int)Math.Truncate(Value);
|
|
}
|
|
|
|
public sealed class StringValue(string value) : Value
|
|
{
|
|
public string Value { get; } = value;
|
|
}
|
|
|
|
public sealed class TableValue : Value
|
|
{
|
|
private readonly Dictionary<object, Value> _map = new();
|
|
private readonly List<(object? Key, Value Value)> _entries = new();
|
|
|
|
public IReadOnlyList<(object? Key, Value Value)> Entries => _entries;
|
|
|
|
public void Set(object? key, Value value)
|
|
{
|
|
if (key is null)
|
|
{
|
|
_entries.Add((null, value));
|
|
return;
|
|
}
|
|
|
|
_map[key] = value;
|
|
_entries.Add((key, value));
|
|
}
|
|
|
|
public bool TryGet(object key, out Value value) => _map.TryGetValue(key, out value!);
|
|
|
|
public Value? Get(object key) => _map.TryGetValue(key, out var value) ? value : null;
|
|
|
|
public IEnumerable<Value> IPairs()
|
|
{
|
|
for (var i = 1; ; i++)
|
|
{
|
|
if (!_map.TryGetValue((long)i, out var value) && !_map.TryGetValue(i, out value))
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
yield return value;
|
|
}
|
|
}
|
|
|
|
public string? GetString(object key)
|
|
=> Get(key) is StringValue s ? s.Value : null;
|
|
|
|
public double? GetNumber(object key)
|
|
=> Get(key) is NumberValue n ? n.Value : null;
|
|
|
|
public bool? GetBool(object key)
|
|
=> Get(key) is BoolValue b ? b.Value : null;
|
|
|
|
public TableValue? GetTable(object key)
|
|
=> Get(key) as TableValue;
|
|
}
|
|
|
|
public static TableValue? ExtractAssignedTable(string source, string assignmentName)
|
|
{
|
|
var index = source.IndexOf(assignmentName, StringComparison.Ordinal);
|
|
if (index < 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var eq = source.IndexOf('=', index + assignmentName.Length);
|
|
if (eq < 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var cursor = eq + 1;
|
|
while (cursor < source.Length && char.IsWhiteSpace(source[cursor]))
|
|
{
|
|
cursor++;
|
|
}
|
|
|
|
if (cursor >= source.Length || source[cursor] != '{')
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var parser = new Parser(source, cursor);
|
|
return parser.ParseValue() as TableValue;
|
|
}
|
|
|
|
private sealed class Parser
|
|
{
|
|
private readonly string _text;
|
|
private int _pos;
|
|
|
|
public Parser(string text, int pos)
|
|
{
|
|
_text = text;
|
|
_pos = pos;
|
|
}
|
|
|
|
public Value ParseValue()
|
|
{
|
|
SkipTrivia();
|
|
if (_pos >= _text.Length)
|
|
{
|
|
return NilValue.Instance;
|
|
}
|
|
|
|
var ch = _text[_pos];
|
|
if (ch == '{')
|
|
{
|
|
return ParseTable();
|
|
}
|
|
|
|
if (ch is '"' or '\'')
|
|
{
|
|
return new StringValue(ParseQuotedString());
|
|
}
|
|
|
|
if (ch == '-' || char.IsDigit(ch))
|
|
{
|
|
return ParseNumber();
|
|
}
|
|
|
|
if (char.IsLetter(ch) || ch == '_')
|
|
{
|
|
var ident = ParseIdentifier();
|
|
return ident switch
|
|
{
|
|
"true" => new BoolValue(true),
|
|
"false" => new BoolValue(false),
|
|
"nil" => NilValue.Instance,
|
|
_ => new StringValue(ident)
|
|
};
|
|
}
|
|
|
|
throw new InvalidDataException($"无法解析的 Lua 值,位置 {_pos}: '{ch}'");
|
|
}
|
|
|
|
private TableValue ParseTable()
|
|
{
|
|
Expect('{');
|
|
var table = new TableValue();
|
|
long nextArrayIndex = 1;
|
|
|
|
while (true)
|
|
{
|
|
SkipTrivia();
|
|
if (_pos >= _text.Length)
|
|
{
|
|
throw new InvalidDataException("Lua 表缺少结束 }");
|
|
}
|
|
|
|
if (_text[_pos] == '}')
|
|
{
|
|
_pos++;
|
|
break;
|
|
}
|
|
|
|
object? key = null;
|
|
Value value;
|
|
|
|
if (_text[_pos] == '[')
|
|
{
|
|
_pos++;
|
|
SkipTrivia();
|
|
var keyValue = ParseValue();
|
|
SkipTrivia();
|
|
Expect(']');
|
|
SkipTrivia();
|
|
Expect('=');
|
|
key = ToKey(keyValue);
|
|
value = ParseValue();
|
|
}
|
|
else
|
|
{
|
|
var start = _pos;
|
|
var peeked = ParseValue();
|
|
SkipTrivia();
|
|
if (_pos < _text.Length && _text[_pos] == '=' && peeked is StringValue name
|
|
&& IsIdentifierAt(start))
|
|
{
|
|
_pos++;
|
|
key = name.Value;
|
|
value = ParseValue();
|
|
}
|
|
else
|
|
{
|
|
key = nextArrayIndex++;
|
|
value = peeked;
|
|
}
|
|
}
|
|
|
|
table.Set(key, value);
|
|
SkipTrivia();
|
|
if (_pos < _text.Length && _text[_pos] == ',')
|
|
{
|
|
_pos++;
|
|
continue;
|
|
}
|
|
|
|
if (_pos < _text.Length && _text[_pos] == ';')
|
|
{
|
|
_pos++;
|
|
}
|
|
}
|
|
|
|
return table;
|
|
}
|
|
|
|
private bool IsIdentifierAt(int start)
|
|
{
|
|
if (start < 0 || start >= _text.Length)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var ch = _text[start];
|
|
return char.IsLetter(ch) || ch == '_';
|
|
}
|
|
|
|
private static object ToKey(Value value) => value switch
|
|
{
|
|
NumberValue n => (long)Math.Truncate(n.Value),
|
|
StringValue s => s.Value,
|
|
BoolValue b => b.Value,
|
|
_ => throw new InvalidDataException("不支持的表键类型")
|
|
};
|
|
|
|
private string ParseQuotedString()
|
|
{
|
|
var quote = _text[_pos++];
|
|
var chars = new List<char>();
|
|
while (_pos < _text.Length)
|
|
{
|
|
var ch = _text[_pos++];
|
|
if (ch == quote)
|
|
{
|
|
return new string(chars.ToArray());
|
|
}
|
|
|
|
if (ch == '\\' && _pos < _text.Length)
|
|
{
|
|
var esc = _text[_pos++];
|
|
chars.Add(esc switch
|
|
{
|
|
'n' => '\n',
|
|
'r' => '\r',
|
|
't' => '\t',
|
|
'\\' => '\\',
|
|
'\'' => '\'',
|
|
'"' => '"',
|
|
_ => esc
|
|
});
|
|
continue;
|
|
}
|
|
|
|
chars.Add(ch);
|
|
}
|
|
|
|
throw new InvalidDataException("字符串未闭合");
|
|
}
|
|
|
|
private NumberValue ParseNumber()
|
|
{
|
|
var start = _pos;
|
|
if (_text[_pos] == '-')
|
|
{
|
|
_pos++;
|
|
}
|
|
|
|
while (_pos < _text.Length && (char.IsDigit(_text[_pos]) || _text[_pos] is '.' or 'e' or 'E' or '+' or '-'))
|
|
{
|
|
// 避免把 `1,` 后的逗号吃掉;仅允许数字本体字符。
|
|
var c = _text[_pos];
|
|
if (c is '+' or '-')
|
|
{
|
|
if (_pos == start || (_text[_pos - 1] is not ('e' or 'E')))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (c is '.' or 'e' or 'E' || char.IsDigit(c) || c is '+' or '-')
|
|
{
|
|
_pos++;
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
var text = _text[start.._pos];
|
|
if (!double.TryParse(text, System.Globalization.NumberStyles.Float,
|
|
System.Globalization.CultureInfo.InvariantCulture, out var number))
|
|
{
|
|
throw new InvalidDataException($"无法解析数字: {text}");
|
|
}
|
|
|
|
return new NumberValue(number);
|
|
}
|
|
|
|
private string ParseIdentifier()
|
|
{
|
|
var start = _pos;
|
|
_pos++;
|
|
while (_pos < _text.Length)
|
|
{
|
|
var ch = _text[_pos];
|
|
if (char.IsLetterOrDigit(ch) || ch == '_')
|
|
{
|
|
_pos++;
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return _text[start.._pos];
|
|
}
|
|
|
|
private void Expect(char expected)
|
|
{
|
|
SkipTrivia();
|
|
if (_pos >= _text.Length || _text[_pos] != expected)
|
|
{
|
|
throw new InvalidDataException($"期望 '{expected}',位置 {_pos}");
|
|
}
|
|
|
|
_pos++;
|
|
}
|
|
|
|
private void SkipTrivia()
|
|
{
|
|
while (_pos < _text.Length)
|
|
{
|
|
if (char.IsWhiteSpace(_text[_pos]))
|
|
{
|
|
_pos++;
|
|
continue;
|
|
}
|
|
|
|
if (_text[_pos] == '-' && _pos + 1 < _text.Length && _text[_pos + 1] == '-')
|
|
{
|
|
_pos += 2;
|
|
if (_pos + 1 < _text.Length && _text[_pos] == '[' && _text[_pos + 1] == '[')
|
|
{
|
|
_pos += 2;
|
|
while (_pos + 1 < _text.Length && !(_text[_pos] == ']' && _text[_pos + 1] == ']'))
|
|
{
|
|
_pos++;
|
|
}
|
|
|
|
if (_pos + 1 < _text.Length)
|
|
{
|
|
_pos += 2;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
while (_pos < _text.Length && _text[_pos] is not ('\r' or '\n'))
|
|
{
|
|
_pos++;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|