namespace Shigure;
///
/// 轻量 Lua 表字面量解析:足够读取 Fuyutsui ClassBlocks 声明。
///
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 _map = new();
private readonly Dictionary _trailingComments = 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, string? trailingComment = null)
{
if (key is null)
{
_entries.Add((null, value));
return;
}
_map[key] = value;
_entries.Add((key, value));
if (!string.IsNullOrWhiteSpace(trailingComment))
{
_trailingComments[key] = trailingComment.Trim();
}
}
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;
/// 表项同行 `--` 行注释(不含 `--` 前缀),无则 null。
public string? GetTrailingComment(object key)
=> _trailingComments.TryGetValue(key, out var comment) ? comment : null;
public IEnumerable 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)
=> TryExtractAssignedTable(source, assignmentName, out var table, out _, out _) ? table : null;
///
/// 提取赋值表字面量,并返回表起止下标(含首尾大括号,endExclusive)。
///
public static bool TryExtractAssignedTable(
string source,
string assignmentName,
out TableValue table,
out int tableStart,
out int tableEndExclusive)
{
table = null!;
tableStart = -1;
tableEndExclusive = -1;
var index = source.IndexOf(assignmentName, StringComparison.Ordinal);
if (index < 0)
{
return false;
}
var eq = source.IndexOf('=', index + assignmentName.Length);
if (eq < 0)
{
return false;
}
var cursor = eq + 1;
while (cursor < source.Length && char.IsWhiteSpace(source[cursor]))
{
cursor++;
}
if (cursor >= source.Length || source[cursor] != '{')
{
return false;
}
var parser = new Parser(source, cursor);
if (parser.ParseValue() is not TableValue parsed)
{
return false;
}
table = parsed;
tableStart = cursor;
tableEndExclusive = parser.Position;
return true;
}
private sealed class Parser
{
private readonly string _text;
private int _pos;
public int Position => _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;
}
}
// 支持 `[37] = "…", -- 银月城生命药水` / `[37] = "…" -- 名称`
var trailingComment = CaptureEntryTrailingComment();
table.Set(key, value, trailingComment);
}
return table;
}
///
/// 读取表项值后的分隔符与同行 `--` 注释。
/// 常见写法:`value, -- 名称` 或 `value -- 名称`(逗号可在下一行)。
///
private string? CaptureEntryTrailingComment()
{
SkipSpacesAndTabs();
string? comment = null;
if (TryReadLineComment(out var beforeSeparator))
{
comment = beforeSeparator;
}
// 注释后可能换行再写逗号;也跳过值与逗号之间的空白。
SkipTrivia();
if (_pos < _text.Length && _text[_pos] is ',' or ';')
{
_pos++;
SkipSpacesAndTabs();
if (comment is null && TryReadLineComment(out var afterSeparator))
{
comment = afterSeparator;
}
}
return string.IsNullOrWhiteSpace(comment) ? null : comment.Trim();
}
private void SkipSpacesAndTabs()
{
while (_pos < _text.Length && _text[_pos] is ' ' or '\t')
{
_pos++;
}
}
/// 若当前位置是行注释 `--`(非 `--[[`),消费并返回注释正文。
private bool TryReadLineComment(out string comment)
{
comment = string.Empty;
if (_pos + 1 >= _text.Length || _text[_pos] != '-' || _text[_pos + 1] != '-')
{
return false;
}
// 块注释交给 SkipTrivia,不当作名称来源。
if (_pos + 3 < _text.Length && _text[_pos + 2] == '[' && _text[_pos + 3] == '[')
{
return false;
}
_pos += 2;
var start = _pos;
while (_pos < _text.Length && _text[_pos] is not ('\r' or '\n'))
{
_pos++;
}
comment = _text[start.._pos];
return true;
}
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();
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;
}
}
}
}