// ----------------------------------------------------------------------------- // file="RegexAstNode.AdditionalPayload" // Project: StgSharp // Copyright (c) Nitload. // SPDX-License-Identifier: MIT // ----------------------------------------------------------------------------- using StgSharp.RegularAnalysis.Abstraction; using System; using System.Collections.Generic; using System.Globalization; using System.Text; namespace StgSharp.RegularAnalysis.Text { public abstract record RegexAstPayload( string Source, int Row, int Column, RegexElementLabel Flag ) : ISyntaxPayload; public sealed record RegexEmptyPayload() : RegexAstPayload(string.Empty, 0, 0, RegexElementLabel.NONE); /// /// A deterministic literal sequence. Escapes represented as literals are consumed here. /// public sealed record RegexLiteralPayload( string Text, string Source, int Row, int Column, RegexElementLabel Flag ) : RegexAstPayload(Source, Row, Column, Flag); public enum RegexCharSetType { None, Single, Range, Set, Any } public sealed record RegexCharSet( string Value, RegexCharSetType Type, bool Accept ); /// /// A character-set atom expanded into the rules formerly produced by FigureCharSetType. /// public sealed record RegexCharSetPayload( IReadOnlyList Set, bool Accept, string Source, int Row, int Column ) : RegexAstPayload(Source, Row, Column, RegexElementLabel.UNIT_SET) { public bool IsAny => Set.Count == 1 && Set[0].Type == RegexCharSetType.Any; } public sealed record RegexCountPayload( int Min, int Max, bool IsGreedy, string Source, int Row, int Column ) : RegexAstPayload(Source, Row, Column, RegexElementLabel.COUNT) { public bool IsStar => Min == 0 && Max < 0; public bool IsPlus => Min == 1 && Max < 0; } public sealed record RegexGroupPayload( string Name, bool IsCapturing, string Source, int Row, int Column ) : RegexAstPayload(Source, Row, Column, RegexElementLabel.GROUP_BEGIN); public sealed record RegexCommonPayload( string Source, int Row, int Column, RegexElementLabel Flag ) : RegexAstPayload(Source, Row, Column, Flag) { public static RegexAstPayload FromToken( Token token ) { return token.Flag switch { RegexElementLabel.UNIT or RegexElementLabel.UNIT_SPAN => new RegexLiteralPayload(DecodeLiteral(token.Value), token.Value, token.Line, token.Column, token.Flag), RegexElementLabel.UNIT_SET => ParseCharSet(token), RegexElementLabel.COUNT => ParseCount(token), RegexElementLabel.GROUP_BEGIN => ParseGroup(token), _ => new RegexCommonPayload(token.Value, token.Line, token.Column, token.Flag) }; } private static void AddEscape( char escaped, List rules ) { bool ruleAccept = char.IsAsciiLetterLower(escaped); char kind = char.ToLower(escaped, CultureInfo.InvariantCulture); if (!IsNamedSet(escaped)) { throw new InvalidCastException($"Unknown character set escape: \\{escaped}"); } rules.Add(new(kind.ToString(), RegexCharSetType.Set, ruleAccept)); } private static char DecodeEscapedChar( char value ) { return value switch { 'n' or 'r' => throw new NotSupportedException( "Multi-line regular expressions are not supported yet."), 't' => '\t', '0' => '\0', _ => value }; } private static string DecodeLiteral( string source ) { if (source.Length != 2 || source[0] != '\\') { return source; } return source[1] switch { 'n' => "\n", 'r' => "\r", 't' => "\t", '0' => "\0", _ => source[1].ToString() }; } private static bool IsNamedSet( char value ) { return char.ToLower(value, CultureInfo.InvariantCulture) is 's' or 'w' or 'd'; } private static void ParseBracketContent( ReadOnlySpan content, List rules ) { HashSet singles = []; int index = 0; while (index < content.Length) { if (content[index] is '\r' or '\n') { throw new NotSupportedException( "Multi-line regular expressions are not supported yet."); } if (content[index] == '\\') { if (++index >= content.Length) { throw new InvalidCastException("Unclosed escape in character set."); } char escaped = content[index++]; if (IsNamedSet(escaped)) { AddEscape(escaped, rules); } else { _ = singles.Add(DecodeEscapedChar(escaped)); } continue; } char first = content[index++]; if (index < content.Length - 0 && content[index] == '-' && index + 1 < content.Length) { index++; char last; if (content[index] == '\\') { if (++index >= content.Length) { throw new InvalidCastException("Unclosed range in character set."); } last = DecodeEscapedChar(content[index++]); } else { last = content[index++]; } if (last < first) { throw new InvalidCastException($"Invalid character range: {first}-{last}"); } rules.Add(new(content[first..last].ToString(), RegexCharSetType.Range, true)); } else { _ = singles.Add(first); } } if (singles.Count > 0) { StringBuilder text = new(singles.Count); foreach (char value in singles) { _ = text.Append(value); } rules.Add(new(text.ToString(), RegexCharSetType.Single, true)); } } private static RegexCharSetPayload ParseCharSet( Token token ) { string source = token.Value; List rules = []; bool accept = true; if (source == ".") { rules.Add(new(string.Empty, RegexCharSetType.Any, true)); } else if (source.Length >= 2 && source[0] == '[' && source[^1] == ']') { int start = source.Length > 2 && source[1] == '^' ? 2 : 1; accept = start == 1; ParseBracketContent(source.AsSpan(start, source.Length - start - 1), rules); } else if (source.Length == 2 && source[0] == '\\') { AddEscape(source[1], rules); } else { throw new InvalidCastException($"Unknown char set: {source}"); } return new RegexCharSetPayload(rules.ToArray(), accept, source, token.Line, token.Column); } private static RegexCountPayload ParseCount( Token token ) { string source = token.Value; bool greedy = source.Length == 1 || source[^1] != '?'; ReadOnlySpan count = greedy ? source.AsSpan() : source.AsSpan(0, source.Length - 1); return count[0] switch { '*' => new RegexCountPayload(0, -1, greedy, source, token.Line, token.Column), '+' => new RegexCountPayload(1, -1, greedy, source, token.Line, token.Column), '?' => new RegexCountPayload(0, 1, greedy, source, token.Line, token.Column), '{' => ParseRange(count, greedy, token), _ => throw new InvalidOperationException($"Invalid count format: {source}") }; } private static RegexGroupPayload ParseGroup( Token token ) { string source = token.Value; bool named = source.StartsWith("(?<", StringComparison.Ordinal); string name = named ? source[3..^1] : string.Empty; return new RegexGroupPayload(name, source != "(?:", source, token.Line, token.Column); } private static RegexCountPayload ParseRange( ReadOnlySpan count, bool greedy, Token token ) { if (count.Length < 3 || count[^1] != '}') { throw new InvalidOperationException($"Invalid count format: {token.Value}"); } ReadOnlySpan body = count[1..^1]; int comma = body.IndexOf(','); if (comma < 0) { if (!int.TryParse(body, out int exact)) { throw new InvalidOperationException($"Invalid count format: {token.Value}"); } return new RegexCountPayload(exact, exact, greedy, token.Value, token.Line, token.Column); } if (!int.TryParse(body[..comma], out int min)) { throw new InvalidOperationException($"Invalid count format: {token.Value}"); } ReadOnlySpan maxText = body[(comma + 1)..]; int max = maxText.IsEmpty ? -1 : int.TryParse(maxText, out int parsed) ? parsed : throw new InvalidOperationException($"Invalid count format: {token.Value}"); if (max >= 0 && max < min) { throw new InvalidOperationException($"Invalid count range: {token.Value}"); } return new RegexCountPayload(min, max, greedy, token.Value, token.Line, token.Column); } } }