using System.Text.RegularExpressions; namespace Strata.SqlTools.Rules.Rule.Expression; /// /// Parses markdown/LaTeX mathematical expressions and converts them to Expression objects. /// Supports parsing of logical operations, comparisons, properties, and literals. /// public static class Markdown { /// /// Match timeout applied to all regular expressions to guard against catastrophic /// backtracking / ReDoS denial-of-service attacks (SonarQube rule S6444). /// private static readonly TimeSpan RegexTimeout = TimeSpan.FromSeconds(1); private static readonly Dictionary> LogicalOperators = new() { { "\\land", (left, right) => new And((BoolExpr)left, (BoolExpr)right) }, { "\\lor", (left, right) => new Or((BoolExpr)left, (BoolExpr)right) }, { "\\wedge", (left, right) => new And((BoolExpr)left, (BoolExpr)right) }, { "\\vee", (left, right) => new Or((BoolExpr)left, (BoolExpr)right) }, { "AND", (left, right) => new And((BoolExpr)left, (BoolExpr)right) }, { "OR", (left, right) => new Or((BoolExpr)left, (BoolExpr)right) }, }; private static readonly Dictionary> ComparisonOperators = new() { { "=", (left, right) => new Equal(left, right) }, { "\\neq", (left, right) => new NotEqual(left, right) }, { "!=", (left, right) => new NotEqual(left, right) }, { ">", (left, right) => new GreaterThan(left, right) }, { "\\gt", (left, right) => new GreaterThan(left, right) }, }; /// /// Parses a markdown/LaTeX string into an Expression object. /// /// The markdown/LaTeX string to parse. /// The parsed Expression object. /// Thrown when the markdown cannot be parsed. public static Expression Parse(string markdown) { if (string.IsNullOrWhiteSpace(markdown)) { throw new ArgumentException("Markdown cannot be null or empty", nameof(markdown)); } // Remove common markdown delimiters markdown = markdown.Trim(); markdown = StripMarkdownDelimiters(markdown); return ParseExpression(markdown); } /// /// Attempts to parse a markdown/LaTeX string into an Expression object. /// /// The markdown/LaTeX string to parse. /// The parsed Expression object if successful. /// True if parsing was successful, false otherwise. public static bool TryParse(string markdown, out Expression? expression) { try { expression = Parse(markdown); return true; } catch { expression = null; return false; } } private static string StripMarkdownDelimiters(string text) { // Remove $...$ or $$...$$ delimiters text = Regex.Replace(text, @"^\$\$?\s*", "", RegexOptions.None, RegexTimeout); text = Regex.Replace(text, @"\s*\$\$?$", "", RegexOptions.None, RegexTimeout); // Remove ```math...``` code fence text = Regex.Replace(text, @"^```math\s*", "", RegexOptions.Multiline, RegexTimeout); text = Regex.Replace(text, @"\s*```$", "", RegexOptions.Multiline, RegexTimeout); return text.Trim(); } private static Expression ParseExpression(string text) { text = text.Trim(); // Try to parse logical operations (lowest precedence) var logicalExpr = TryParseLogicalOperation(text); if (logicalExpr is not null) { return logicalExpr; } // Try to parse comparison operations var comparisonExpr = TryParseComparison(text); if (comparisonExpr is not null) { return comparisonExpr; } // Handle parentheses var parenthesisExpr = TryParseParentheses(text); if (parenthesisExpr is not null) { return parenthesisExpr; } // Parse property, literal, or other atomic expressions return ParseAtomicExpression(text); } private static Expression? TryParseLogicalOperation(string text) { foreach (var op in LogicalOperators.Keys) { var parts = SplitByOperator(text, op); if (parts.Length == 2) { var left = ParseExpression(parts[0]); var right = ParseExpression(parts[1]); return LogicalOperators[op](left, right); } } return null; } private static Expression? TryParseComparison(string text) { foreach (var op in ComparisonOperators.Keys) { var parts = SplitByOperator(text, op); if (parts.Length == 2) { var left = ParseExpression(parts[0]); var right = ParseExpression(parts[1]); return ComparisonOperators[op](left, right); } } return null; } private static Expression? TryParseParentheses(string text) { // Handle regular parentheses if (text.StartsWith('(') && text.EndsWith(')')) { var inner = text.Substring(1, text.Length - 2); if (IsBalanced(inner)) { return ParseExpression(inner); } } // Handle LaTeX \left( and \right) if (text.StartsWith("\\left(") && text.EndsWith("\\right)")) { var inner = text.Substring(6, text.Length - 13); if (IsBalanced(inner)) { return ParseExpression(inner); } } return null; } private static Expression ParseAtomicExpression(string text) { // Try parsing as property access var propertyExpr = TryParseProperty(text); if (propertyExpr is not null) { return propertyExpr; } // Try parsing as literal var literalExpr = TryParseLiteral(text); if (literalExpr is not null) { return literalExpr; } throw new ArgumentException($"Unable to parse expression: {text}"); } private static Expression? TryParseProperty(string text) { // Parse property access (e.g., x.PropertyName or \text{x.PropertyName}) var propertyMatch = Regex.Match(text, @"^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$", RegexOptions.None, RegexTimeout); if (propertyMatch.Success) { return new Property(propertyMatch.Groups[1].Value, propertyMatch.Groups[2].Value); } // Parse \text{...} property access var textMatch = Regex.Match(text, @"^\\text\{([^}]+)\}$", RegexOptions.None, RegexTimeout); if (textMatch.Success) { var textContent = textMatch.Groups[1].Value; var propMatch = Regex.Match(textContent, @"^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$", RegexOptions.None, RegexTimeout); if (propMatch.Success) { return new Property(propMatch.Groups[1].Value, propMatch.Groups[2].Value); } // Check for boolean literals in \text{} format if (textContent.Equals("true", StringComparison.OrdinalIgnoreCase)) { return new Literal(true); } if (textContent.Equals("false", StringComparison.OrdinalIgnoreCase)) { return new Literal(false); } // Single property name if (Regex.IsMatch(textContent, @"^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.None, RegexTimeout)) { return new Property(textContent); } // String literal return new StringLiteral(textContent); } // Check for boolean literals before simple property if (text.Equals("true", StringComparison.OrdinalIgnoreCase)) { return new Literal(true); } if (text.Equals("false", StringComparison.OrdinalIgnoreCase)) { return new Literal(false); } // Parse simple property without parameter if (Regex.IsMatch(text, @"^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.None, RegexTimeout)) { return new Property(text); } return null; } private static Expression? TryParseLiteral(string text) { // Parse string literals (quoted) var stringMatch = Regex.Match(text, @"^[""'](.+?)[""']$", RegexOptions.None, RegexTimeout); if (stringMatch.Success) { return new StringLiteral(stringMatch.Groups[1].Value); } // Parse empty string literals if (text == "\"\"" || text == "''") { return new StringLiteral(string.Empty); } // Parse numeric literals if (int.TryParse(text, out var intValue)) { return new NumberLiteral(intValue); } if (decimal.TryParse(text, out var decimalValue)) { return new NumberLiteral(decimalValue); } return null; } private static string[] SplitByOperator(string text, string op) { var result = new List(); int depth = 0; int lastIndex = 0; int i = 0; while (i < text.Length) { i = ProcessParentheses(text, i, ref depth); if (i >= text.Length) { break; } // Check if we found the operator at depth 0 if (depth == 0 && i + op.Length <= text.Length && TryMatchOperator(text, i, op)) { result.Add(text.Substring(lastIndex, i - lastIndex).Trim()); lastIndex = i + op.Length; i += op.Length; continue; } i++; } if (result.Count == 0) { return new[] { text }; } result.Add(text.Substring(lastIndex).Trim()); return result.ToArray(); } private static int ProcessParentheses(string text, int index, ref int depth) { // Track parentheses depth if (text[index] == '(' || (index + 5 < text.Length && text.Substring(index, 6) == "\\left(")) { depth++; if (text[index] == '\\') { return index + 6; } else { return index + 1; } } if (text[index] == ')' || (index + 6 < text.Length && text.Substring(index, 7) == "\\right)")) { depth--; if (text[index] == '\\') { return index + 7; } else { return index + 1; } } return index; } private static bool TryMatchOperator(string text, int index, string op) { var substring = text.Substring(index, op.Length); if (substring != op) { return false; } // Make sure it's a separate operator, not part of a larger token bool validBefore = (index == 0 || char.IsWhiteSpace(text[index - 1]) || text[index] == '\\'); bool validAfter = (index + op.Length >= text.Length || char.IsWhiteSpace(text[index + op.Length])); return validBefore && validAfter; } private static bool IsBalanced(string text) { int depth = 0; int i = 0; while (i < text.Length) { if (text[i] == '(') { depth++; } else if (text[i] == ')') { depth--; if (depth < 0) { return false; } } else if (i + 5 < text.Length && text.Substring(i, 6) == "\\left(") { depth++; i += 5; } else if (i + 6 < text.Length && text.Substring(i, 7) == "\\right)") { depth--; if (depth < 0) { return false; } i += 6; } i++; } return depth == 0; } }