Files
sql-utilities/src/Strata.SqlTools.Markdown/Expressions/ExpressionGenerator.cs
T

672 lines
24 KiB
C#

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;
/// <summary>
/// Generates markdown documentation for Expression trees.
/// Creates human-readable documentation with expression structure, type information, and visual representations.
/// </summary>
public class ExpressionGenerator : IVisitor<string>
{
private int _indentLevel = 0;
private readonly string _indentString = " ";
/// <summary>
/// Generates markdown documentation from an Expression tree.
/// </summary>
/// <param name="expression">The expression to document.</param>
/// <param name="title">Optional title for the documentation.</param>
/// <returns>A markdown formatted string documenting the expression.</returns>
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();
}
/// <summary>
/// Generates a mathematical expression using LaTeX notation for GitHub markdown.
/// </summary>
/// <param name="expression">The expression to convert to mathematical notation.</param>
/// <param name="inline">If true, generates inline math ($...$), otherwise block math ($$...$$).</param>
/// <returns>A string containing the LaTeX mathematical expression.</returns>
public static string GenerateMathematicalExpression(Expression expression, bool inline = false)
{
var latex = ConvertToLatex(expression);
return inline ? $"${latex}$" : $"$$\n{latex}\n$$";
}
/// <summary>
/// Generates a mathematical expression from raw LaTeX in markdown format.
/// </summary>
/// <param name="latex">The LaTeX expression.</param>
/// <param name="format">The format to use: "dollar" for $/$$ delimiters, "math" for ```math code fence.</param>
/// <param name="inline">If true and format is "dollar", generates inline math ($...$), otherwise block math ($$...$$). Ignored for "math" format.</param>
/// <returns>A string containing the formatted mathematical expression.</returns>
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("#", "\\#");
}
/// <summary>
/// Generates a Mermaid tree diagram from an Expression tree.
/// </summary>
private string GenerateMermaidDiagram(Expression expression)
{
var sb = new StringBuilder();
sb.AppendLine("```mermaid");
sb.AppendLine("graph TD");
sb.AppendLine();
int nodeCounter = 0;
var nodeMap = new Dictionary<object, int>();
GenerateMermaidNodes(expression, sb, nodeMap, ref nodeCounter);
sb.AppendLine("```");
return sb.ToString();
}
private int GenerateMermaidNodes(Expression expression, StringBuilder sb, Dictionary<object, int> 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("\"", "&quot;")
.Replace("[", "&#91;")
.Replace("]", "&#93;");
}
private static string TruncateText(string text, int maxLength)
{
if (string.IsNullOrWhiteSpace(text) || text.Length <= maxLength)
{
return text;
}
return text.Substring(0, maxLength) + "...";
}
#region IVisitor Implementation
public string VisitTableSource(TableSource tableSource)
{
return $"{Indent()}TableSource: {tableSource.TableName}";
}
public string VisitColumnExpression<TSource>(ColumnExpression<TSource> 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.Any())
{
_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.Any())
{
_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
}