using System.Text; using Strata.SqlTools.SqlBreakdown.Classes; using Strata.SqlTools.SqlBreakdown.Expressions; using Strata.SqlTools.SqlBreakdown.Expressions.Arithmetic; using Strata.SqlTools.SqlBreakdown.Expressions.Conditional; using Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Comparisons; using Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Logical; using Strata.SqlTools.SqlBreakdown.Expressions.Functions; using Strata.SqlTools.SqlBreakdown.Expressions.Functions.Aggregate; using Strata.SqlTools.SqlBreakdown.Expressions.Functions.Conditional; using Strata.SqlTools.SqlBreakdown.Expressions.Literals; using Strata.SqlTools.SqlBreakdown.Interfaces.Core; namespace Strata.SqlTools.Markdown.Expressions; /// /// Generates markdown documentation for Expression trees. /// Creates human-readable documentation with expression structure, type information, and visual representations. /// public class ExpressionGenerator : IVisitor { private int _indentLevel = 0; private readonly string _indentString = " "; /// /// Generates markdown documentation from an Expression tree. /// /// The expression to document. /// Optional title for the documentation. /// A markdown formatted string documenting the expression. public string GenerateMarkdown(Expression expression, string? title = null) { var sb = new StringBuilder(); if (!string.IsNullOrWhiteSpace(title)) { sb.AppendLine($"# {title}"); sb.AppendLine(); } sb.AppendLine("## Expression Structure"); sb.AppendLine(); sb.AppendLine("```"); _indentLevel = 0; sb.AppendLine(expression.Accept(this)); sb.AppendLine("```"); sb.AppendLine(); sb.AppendLine("## Expression Type"); sb.AppendLine(); sb.AppendLine($"**Type:** `{expression.GetType().Name}`"); sb.AppendLine(); sb.AppendLine("## Mermaid Diagram"); sb.AppendLine(); sb.AppendLine(GenerateMermaidDiagram(expression)); sb.AppendLine(); sb.AppendLine("## Mathematical Expression"); sb.AppendLine(); sb.AppendLine(GenerateMathematicalExpression(expression)); return sb.ToString(); } /// /// Generates a mathematical expression using LaTeX notation for GitHub markdown. /// /// The expression to convert to mathematical notation. /// If true, generates inline math ($...$), otherwise block math ($$...$$). /// A string containing the LaTeX mathematical expression. public static string GenerateMathematicalExpression(Expression expression, bool inline = false) { var latex = ConvertToLatex(expression); return inline ? $"${latex}$" : $"$$\n{latex}\n$$"; } /// /// Generates a mathematical expression from raw LaTeX in markdown format. /// /// The LaTeX expression. /// The format to use: "dollar" for $/$$ delimiters, "math" for ```math code fence. /// If true and format is "dollar", generates inline math ($...$), otherwise block math ($$...$$). Ignored for "math" format. /// A string containing the formatted mathematical expression. public static string GenerateRawMathematicalExpression(string latex, string format = "dollar", bool inline = false) { return format.ToLower() switch { "math" => $"```math\n{latex}\n```", _ => inline ? $"${latex}$" : $"$$\n{latex}\n$$" }; } private static string ConvertToLatex(Expression expression) { return expression switch { // Arithmetic expressions ArithmeticExpression arith => ConvertArithmeticToLatex(arith), // Comparison expressions ComparisonOperatorExpression comp => ConvertComparisonToLatex(comp), // Logical expressions AndExpression and => $"({ConvertToLatex(and.ExpressionA)} \\land {ConvertToLatex(and.ExpressionB)})", OrExpression or => $"({ConvertToLatex(or.ExpressionA)} \\lor {ConvertToLatex(or.ExpressionB)})", NotExpression not => $"\\neg({ConvertToLatex(not.ExpressionA)})", // Literals NumberLiteralExpression num => num.Value.ToString() ?? "0", StringLiteralExpression str => $"\\text{{\"{EscapeLatex(str.Value)}\"}}", BooleanLiteralExpression b => b.Value ? "\\text{true}" : "\\text{false}", NullLiteralExpression => "\\text{NULL}", // Column expressions ColumnExpression col => $"\\text{{{EscapeLatex(col.ColumnName)}}}", // Parameter expressions ParameterExpression param => $"@{EscapeLatex(param.ParameterName)}", // Case expressions (before FunctionExpression since it's a subclass) CaseExpression caseExpr => ConvertCaseToLatex(caseExpr), // Functions FunctionExpression func => ConvertFunctionToLatex(func), // Between expressions BetweenExpression between => $"{ConvertToLatex(between.Expression)} \\in [{ConvertToLatex(between.LowerBound)}, {ConvertToLatex(between.UpperBound)}]", // IN expressions InExpression inExpr => $"{ConvertToLatex(inExpr.SearchExpression)} \\in \\{{{string.Join(", ", inExpr.ValuesToCompare.Select(ConvertToLatex))}\\}}", // LIKE expressions LikeExpression like => $"{ConvertToLatex(like.Subject)} \\approx \\text{{\"{EscapeLatex(ConvertExpressionToString(like.Pattern))}\"}}", _ => $"\\text{{{EscapeLatex(expression.GetType().Name)}}}" }; } private static string ConvertArithmeticToLatex(ArithmeticExpression arith) { var left = ConvertToLatex(arith.ExpressionA); var right = ConvertToLatex(arith.ExpressionB); var op = arith.ArithmeticOperator switch { "+" => "+", "-" => "-", "*" => "\\times", "/" => "\\div", "%" => "\\bmod", _ => "?" }; return $"({left} {op} {right})"; } private static string ConvertComparisonToLatex(ComparisonOperatorExpression comp) { var left = ConvertToLatex(comp.ExpressionA); var right = ConvertToLatex(comp.ExpressionB); var op = comp.Operator switch { "=" => "=", "<>" => "\\neq", "!=" => "\\neq", ">" => ">", ">=" => "\\geq", "<" => "<", "<=" => "\\leq", _ => "?" }; return $"({left} {op} {right})"; } private static string ConvertFunctionToLatex(FunctionExpression func) { var args = string.Join(", ", func.Arguments.Select(ConvertToLatex)); var funcName = EscapeLatex(func.FunctionName); return func.FunctionName.ToUpper() switch { // Aggregate functions "COUNT" => $"\\text{{COUNT}}({args})", "SUM" => $"\\sum({args})", "AVG" => $"\\text{{AVG}}({args})", "MIN" => $"\\min({args})", "MAX" => $"\\max({args})", // Math functions "ABS" => $"|{args}|", "SQRT" => $"\\sqrt{{{args}}}", "POWER" when func.Arguments.Length >= 2 => $"{ConvertToLatex(func.Arguments[0])}^{{{ConvertToLatex(func.Arguments[1])}}}", "LOG" => $"\\log({args})", "EXP" => $"e^{{{args}}}", // Default _ => $"\\text{{{funcName}}}({args})" }; } private static string ConvertCaseToLatex(CaseExpression caseExpr) { var sb = new StringBuilder(); sb.Append("\\begin{cases}\n"); foreach (var (condition, result) in caseExpr.ConditionResultPairs) { sb.Append($" {ConvertToLatex(result)} & \\text{{if }} {ConvertToLatex(condition)} \\\\\n"); } if (caseExpr.ElseResultExpression is not null) { sb.Append($" {ConvertToLatex(caseExpr.ElseResultExpression)} & \\text{{otherwise}}\n"); } sb.Append("\\end{cases}"); return sb.ToString(); } private static string ConvertExpressionToString(Expression expression) { return expression switch { StringLiteralExpression str => str.Value, _ => expression.ToString() ?? "" }; } private static string EscapeLatex(string text) { return text .Replace("\\", "\\\\") .Replace("{", "\\{") .Replace("}", "\\}") .Replace("_", "\\_") .Replace("$", "\\$") .Replace("%", "\\%") .Replace("&", "\\&") .Replace("#", "\\#"); } /// /// Generates a Mermaid tree diagram from an Expression tree. /// private static string GenerateMermaidDiagram(Expression expression) { var sb = new StringBuilder(); sb.AppendLine("```mermaid"); sb.AppendLine("graph TD"); sb.AppendLine(); int nodeCounter = 0; var nodeMap = new Dictionary(); GenerateMermaidNodes(expression, sb, nodeMap, ref nodeCounter); sb.AppendLine("```"); return sb.ToString(); } private static int GenerateMermaidNodes(Expression expression, StringBuilder sb, Dictionary nodeMap, ref int nodeCounter) { var currentNode = nodeCounter++; nodeMap[expression] = currentNode; var nodeLabel = GetNodeLabel(expression); var nodeShape = GetNodeShape(expression); sb.AppendLine($" Node{currentNode}{nodeShape[0]}\"{EscapeMarkdown(nodeLabel)}\"{nodeShape[1]}"); // Process child expressions switch (expression) { case ComparisonOperatorExpression comp: var leftId = GenerateMermaidNodes(comp.ExpressionA, sb, nodeMap, ref nodeCounter); var rightId = GenerateMermaidNodes(comp.ExpressionB, sb, nodeMap, ref nodeCounter); sb.AppendLine($" Node{currentNode} --> Node{leftId}"); sb.AppendLine($" Node{currentNode} --> Node{rightId}"); break; case AndExpression and: var andLeftId = GenerateMermaidNodes(and.ExpressionA, sb, nodeMap, ref nodeCounter); var andRightId = GenerateMermaidNodes(and.ExpressionB, sb, nodeMap, ref nodeCounter); sb.AppendLine($" Node{currentNode} -->|Left| Node{andLeftId}"); sb.AppendLine($" Node{currentNode} -->|Right| Node{andRightId}"); break; case OrExpression or: var orLeftId = GenerateMermaidNodes(or.ExpressionA, sb, nodeMap, ref nodeCounter); var orRightId = GenerateMermaidNodes(or.ExpressionB, sb, nodeMap, ref nodeCounter); sb.AppendLine($" Node{currentNode} -->|Left| Node{orLeftId}"); sb.AppendLine($" Node{currentNode} -->|Right| Node{orRightId}"); break; case NotExpression not: var notId = GenerateMermaidNodes(not.ExpressionA, sb, nodeMap, ref nodeCounter); sb.AppendLine($" Node{currentNode} --> Node{notId}"); break; case ArithmeticExpression arith: var arithmLeftId = GenerateMermaidNodes(arith.ExpressionA, sb, nodeMap, ref nodeCounter); var arithmRightId = GenerateMermaidNodes(arith.ExpressionB, sb, nodeMap, ref nodeCounter); sb.AppendLine($" Node{currentNode} --> Node{arithmLeftId}"); sb.AppendLine($" Node{currentNode} --> Node{arithmRightId}"); break; case FunctionExpression func: foreach (var arg in func.Arguments) { var argId = GenerateMermaidNodes(arg, sb, nodeMap, ref nodeCounter); sb.AppendLine($" Node{currentNode} --> Node{argId}"); } break; } return currentNode; } private static string GetNodeLabel(Expression expression) { return expression switch { NumberLiteralExpression num => $"Number: {num.Value}", StringLiteralExpression str => $"String: {TruncateText(str.Value, 20)}", DateTimeLiteralExpression dt => $"DateTime: {dt.Value:yyyy-MM-dd}", BooleanLiteralExpression b => $"Boolean: {b.Value}", NullLiteralExpression => "NULL", ComparisonOperatorExpression comp => $"Comparison: {comp.Operator}", AndExpression => "AND", OrExpression => "OR", NotExpression => "NOT", ArithmeticExpression arith => $"Arithmetic: {arith.ArithmeticOperator}", FunctionExpression func => $"Function: {func.FunctionName}", ParameterExpression param => $"Parameter: @{param.ParameterName}", _ => expression.GetType().Name }; } private static string[] GetNodeShape(Expression expression) { return expression switch { NumberLiteralExpression or StringLiteralExpression or DateTimeLiteralExpression or BooleanLiteralExpression or NullLiteralExpression => new[] { "[", "]" }, ComparisonOperatorExpression => new[] { "{", "}" }, AndExpression or OrExpression or NotExpression => new[] { "{", "}" }, FunctionExpression => new[] { "[[", "]]" }, _ => new[] { "(", ")" } }; } private string Indent() => new string(' ', _indentLevel * _indentString.Length); private static string EscapeMarkdown(string text) { return text .Replace("\"", """) .Replace("[", "[") .Replace("]", "]"); } private static string TruncateText(string text, int maxLength) { if (string.IsNullOrWhiteSpace(text) || text.Length <= maxLength) { return text; } return string.Concat(text.AsSpan(0, maxLength), "..."); } #region IVisitor Implementation public string VisitTableSource(TableSource tableSource) { return $"{Indent()}TableSource: {tableSource.TableName}"; } public string VisitColumnExpression(ColumnExpression column) where TSource : SelectSource { return $"{Indent()}Column: {column.ColumnName}"; } public string VisitSelectClauseColumn(SelectClauseColumn selectClauseColumn) { var sb = new StringBuilder(); sb.AppendLine($"{Indent()}SelectClauseColumn:"); _indentLevel++; sb.AppendLine(selectClauseColumn.Expression.Accept(this)); if (!string.IsNullOrWhiteSpace(selectClauseColumn.Alias)) { sb.AppendLine($"{Indent()}Alias: {selectClauseColumn.Alias}"); } _indentLevel--; return sb.ToString(); } public string VisitParameterExpression(ParameterExpression parameterExpression) { return $"{Indent()}Parameter: @{parameterExpression.ParameterName}"; } public string VisitNumberLiteralExpression(NumberLiteralExpression numberLiteral) { return $"{Indent()}Number: {numberLiteral.Value}"; } public string VisitStringLiteralExpression(StringLiteralExpression stringLiteral) { return $"{Indent()}String: '{stringLiteral.Value}'"; } public string VisitDateTimeLiteralExpression(DateTimeLiteralExpression dateTimeLiteral) { return $"{Indent()}DateTime: {dateTimeLiteral.Value:yyyy-MM-dd HH:mm:ss}"; } public string VisitNullLiteralExpression(NullLiteralExpression nullLiteral) { return $"{Indent()}NULL"; } public string VisitBooleanLiteralExpression(BooleanLiteralExpression booleanLiteral) { return $"{Indent()}Boolean: {booleanLiteral.Value}"; } public string VisitParameterLiteralExpression(ParameterLiteralExpression parameterLiteral) { return $"{Indent()}Parameter: {parameterLiteral.Value}"; } public string VisitSymbolLiteralExpression(SymbolLiteralExpression symbolLiteral) { return $"{Indent()}Symbol: {symbolLiteral.Value}"; } public string VisitComparisonExpression(ComparisonOperatorExpression comparison) { var sb = new StringBuilder(); sb.AppendLine($"{Indent()}Comparison ({comparison.Operator}):"); _indentLevel++; sb.AppendLine($"{Indent()}Left:"); _indentLevel++; sb.AppendLine(comparison.ExpressionA.Accept(this)); _indentLevel--; sb.AppendLine($"{Indent()}Right:"); _indentLevel++; sb.AppendLine(comparison.ExpressionB.Accept(this)); _indentLevel--; _indentLevel--; return sb.ToString(); } public string VisitAndExpression(AndExpression logical) { var sb = new StringBuilder(); sb.AppendLine($"{Indent()}AND:"); _indentLevel++; sb.AppendLine(logical.ExpressionA.Accept(this)); sb.AppendLine(logical.ExpressionB.Accept(this)); _indentLevel--; return sb.ToString(); } public string VisitOrExpression(OrExpression logical) { var sb = new StringBuilder(); sb.AppendLine($"{Indent()}OR:"); _indentLevel++; sb.AppendLine(logical.ExpressionA.Accept(this)); sb.AppendLine(logical.ExpressionB.Accept(this)); _indentLevel--; return sb.ToString(); } public string VisitNotExpression(NotExpression logical) { var sb = new StringBuilder(); sb.AppendLine($"{Indent()}NOT:"); _indentLevel++; sb.AppendLine(logical.ExpressionA.Accept(this)); _indentLevel--; return sb.ToString(); } public string VisitInExpression(InExpression inExpression) { var sb = new StringBuilder(); sb.AppendLine($"{Indent()}IN:"); _indentLevel++; sb.AppendLine($"{Indent()}Search Expression:"); _indentLevel++; sb.AppendLine(inExpression.SearchExpression.Accept(this)); _indentLevel--; sb.AppendLine($"{Indent()}Values:"); _indentLevel++; foreach (var value in inExpression.ValuesToCompare) { sb.AppendLine(value.Accept(this)); } _indentLevel--; _indentLevel--; return sb.ToString(); } public string VisitNotInExpression(NotInExpression inExpression) { var sb = new StringBuilder(); sb.AppendLine($"{Indent()}NOT IN:"); _indentLevel++; sb.AppendLine($"{Indent()}Search Expression:"); _indentLevel++; sb.AppendLine(inExpression.SearchExpression.Accept(this)); _indentLevel--; sb.AppendLine($"{Indent()}Values:"); _indentLevel++; foreach (var value in inExpression.ValuesToCompare) { sb.AppendLine(value.Accept(this)); } _indentLevel--; _indentLevel--; return sb.ToString(); } public string VisitLikeExpression(LikeExpression likeExpression) { var sb = new StringBuilder(); sb.AppendLine($"{Indent()}LIKE (Case {(likeExpression.CaseInsensitive ? "Insensitive" : "Sensitive")}):"); _indentLevel++; sb.AppendLine($"{Indent()}Subject:"); _indentLevel++; sb.AppendLine(likeExpression.Subject.Accept(this)); _indentLevel--; sb.AppendLine($"{Indent()}Pattern:"); _indentLevel++; sb.AppendLine(likeExpression.Pattern.Accept(this)); _indentLevel--; _indentLevel--; return sb.ToString(); } public string VisitNotLikeExpression(NotLikeExpression notLikeExpression) { var sb = new StringBuilder(); sb.AppendLine($"{Indent()}NOT LIKE:"); _indentLevel++; sb.AppendLine($"{Indent()}Subject:"); _indentLevel++; sb.AppendLine(notLikeExpression.Subject.Accept(this)); _indentLevel--; sb.AppendLine($"{Indent()}Pattern:"); _indentLevel++; sb.AppendLine(notLikeExpression.Pattern.Accept(this)); _indentLevel--; _indentLevel--; return sb.ToString(); } public string VisitBetweenExpression(BetweenExpression betweenExpression) { var sb = new StringBuilder(); sb.AppendLine($"{Indent()}BETWEEN:"); _indentLevel++; sb.AppendLine($"{Indent()}Expression:"); _indentLevel++; sb.AppendLine(betweenExpression.Expression.Accept(this)); _indentLevel--; sb.AppendLine($"{Indent()}Lower Bound:"); _indentLevel++; sb.AppendLine(betweenExpression.LowerBound.Accept(this)); _indentLevel--; sb.AppendLine($"{Indent()}Upper Bound:"); _indentLevel++; sb.AppendLine(betweenExpression.UpperBound.Accept(this)); _indentLevel--; _indentLevel--; return sb.ToString(); } public string VisitFunctionExpression(FunctionExpression function) { var sb = new StringBuilder(); sb.AppendLine($"{Indent()}Function: {function.FunctionName}"); if (function.Arguments.Length != 0) { _indentLevel++; sb.AppendLine($"{Indent()}Arguments:"); _indentLevel++; foreach (var arg in function.Arguments) { sb.AppendLine(arg.Accept(this)); } _indentLevel--; _indentLevel--; } return sb.ToString(); } public string VisitAggregateFunctionExpression(AggregateFunctionExpression aggregateFunction) { var sb = new StringBuilder(); sb.AppendLine($"{Indent()}Aggregate Function: {aggregateFunction.FunctionName}"); if (aggregateFunction.Arguments.Length != 0) { _indentLevel++; sb.AppendLine($"{Indent()}Arguments:"); _indentLevel++; foreach (var arg in aggregateFunction.Arguments) { sb.AppendLine(arg.Accept(this)); } _indentLevel--; _indentLevel--; } return sb.ToString(); } public string VisitCaseFunctionExpression(CaseExpression caseFunction) { var sb = new StringBuilder(); sb.AppendLine($"{Indent()}CASE:"); _indentLevel++; foreach (var (condition, result) in caseFunction.ConditionResultPairs) { sb.AppendLine($"{Indent()}WHEN:"); _indentLevel++; sb.AppendLine(condition.Accept(this)); _indentLevel--; sb.AppendLine($"{Indent()}THEN:"); _indentLevel++; sb.AppendLine(result.Accept(this)); _indentLevel--; } if (caseFunction.ElseResultExpression is not null) { sb.AppendLine($"{Indent()}ELSE:"); _indentLevel++; sb.AppendLine(caseFunction.ElseResultExpression.Accept(this)); _indentLevel--; } _indentLevel--; return sb.ToString(); } public string VisitArithmeticExpression(ArithmeticExpression arithmeticExpression) { var sb = new StringBuilder(); sb.AppendLine($"{Indent()}Arithmetic ({arithmeticExpression.ArithmeticOperator}):"); _indentLevel++; sb.AppendLine($"{Indent()}Left:"); _indentLevel++; sb.AppendLine(arithmeticExpression.ExpressionA.Accept(this)); _indentLevel--; sb.AppendLine($"{Indent()}Right:"); _indentLevel++; sb.AppendLine(arithmeticExpression.ExpressionB.Accept(this)); _indentLevel--; _indentLevel--; return sb.ToString(); } public string VisitInputPropertyExpression(InputPropertyExpression inputPropertyExpression) { return $"{Indent()}InputProperty: {inputPropertyExpression.DataKeyLookup}"; } #endregion }