diff --git a/xlparser/.gitignore b/xlparser/.gitignore new file mode 100644 index 00000000..2cb90536 --- /dev/null +++ b/xlparser/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +*.sln.iml +.idea/**/* +**/.idea/* \ No newline at end of file diff --git a/xlparser/Apps/XLParserApp.cs b/xlparser/Apps/XLParserApp.cs new file mode 100644 index 00000000..f13d8ed3 --- /dev/null +++ b/xlparser/Apps/XLParserApp.cs @@ -0,0 +1,112 @@ +using XLParserDemo.Services; + +namespace XLParserDemo.Apps; + +[App(title: "XLParser", icon: Icons.Sheet)] +public class XLParserApp : ViewBase +{ + private readonly string Title = "XLParser Demo"; + private readonly string Description = "Enter Excel formula and parse it"; + + // Component state for token coloring, preserved across renders. + private readonly Queue _chromaticColors = new([ + Colors.Red, Colors.Orange, Colors.Amber, Colors.Yellow, Colors.Lime, + Colors.Green, Colors.Emerald, Colors.Teal, Colors.Cyan, Colors.Sky, + Colors.Blue, Colors.Indigo, Colors.Violet, Colors.Purple, Colors.Fuchsia, + Colors.Pink, Colors.Rose + ]); + private readonly Dictionary _foundTokenTypes = []; + + private record ParserState( + IState Formula, + IState Result, + IState> Tokens, + IState SelectedToken + ); + + private enum FormulaParseResult + { + Unknown, + Parsed, + NotParsed, + UnexpectedError + }; + + public override object? Build() + { + // State management + var parserState = new ParserState( + Formula: UseState("SUM(A1:A10) + IF(B1>10, MAX(B1:B10), MIN(B1:B10))"), + Result: UseState(FormulaParseResult.Unknown), + Tokens: UseState(new List()), + SelectedToken: UseState() + ); + + return new Card() + .Title(Title) + .Description(Description) + | Layout.Vertical( + // Formula Input Section + Layout.Vertical( + Text.Label("Excel Formula: "), + new TextInput(parserState.Formula), + new Button("Parse Formula", onClick: _ => HandleParse(parserState)) + ), + new Separator(), + // Parse Result Section + parserState.Result.Value switch + { + FormulaParseResult.Unknown => Text.Label("Click 'Parse Formula' to see the result."), + FormulaParseResult.Parsed => Layout.Horizontal( + Layout.Vertical( + Text.Small("Click on tokens to see details."), + Layout.Vertical(parserState.Tokens.Value.Select(token => + { + return new Button(title: token.NodeValue, onClick: _ => parserState.SelectedToken.Set(token)) + .Outline() + .Secondary() + .Foreground(GetTokenColor(token.NodeValue)) + .WithMargin(left: token.Depth, top: 0, right: 0, bottom: 0); + })) + .Gap(1)), + Layout.Vertical( + Text.Label("Selected Token Details:"), + parserState.SelectedToken?.Value?.NodeInfo) + ), + FormulaParseResult.NotParsed => Callout.Error("The formula could not be parsed. Please check the syntax."), + FormulaParseResult.UnexpectedError => Callout.Error("An unexpected error occurred during parsing."), + _ => null + } + ); + } + + private void HandleParse(ParserState state) + { + try + { + var parseTree = FormulaParser.ParseFormula(state.Formula.Value); + + state.Tokens.Set([.. parseTree]); + state.Result.Set(FormulaParseResult.Parsed); + state.SelectedToken.Set(parseTree.FirstOrDefault()); + } + catch (ArgumentException) + { + state.Result.Set(FormulaParseResult.NotParsed); + } + catch (Exception) + { + state.Result.Set(FormulaParseResult.UnexpectedError); + } + } + + private Colors GetTokenColor(string tokenName) + { + if (!_foundTokenTypes.TryGetValue(tokenName, out var color)) + { + color = _chromaticColors.Count > 0 ? _chromaticColors.Dequeue() : Colors.Gray; + _foundTokenTypes[tokenName] = color; + } + return color; + } +} \ No newline at end of file diff --git a/xlparser/GlobalUsings.cs b/xlparser/GlobalUsings.cs new file mode 100644 index 00000000..9a3ee972 --- /dev/null +++ b/xlparser/GlobalUsings.cs @@ -0,0 +1,34 @@ +global using Ivy; +global using Ivy.Apps; +global using Ivy.Auth; +global using Ivy.Chrome; +global using Ivy.Client; +global using Ivy.Core; +global using Ivy.Core.Hooks; +global using Ivy.Helpers; +global using Ivy.Hooks; +global using Ivy.Shared; +global using Ivy.Services; +global using Ivy.Views; +global using Ivy.Views.Alerts; +global using Ivy.Views.Blades; +global using Ivy.Views.Builders; +global using Ivy.Views.Charts; +global using Ivy.Views.Dashboards; +global using Ivy.Views.Forms; +global using Ivy.Views.Tables; +global using Ivy.Widgets.Inputs; +global using Irony.Parsing; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; +global using System.Collections.Immutable; +global using System.ComponentModel.DataAnnotations; +global using System.Globalization; +global using System.Reactive.Linq; +global using XLParser; +global using XLParserDemo.Apps; + + + +namespace XLParserDemo; diff --git a/xlparser/Program.cs b/xlparser/Program.cs new file mode 100644 index 00000000..969dcdec --- /dev/null +++ b/xlparser/Program.cs @@ -0,0 +1,12 @@ +CultureInfo.DefaultThreadCurrentCulture = CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("en-US"); +var server = new Server(); +#if DEBUG +server.UseHotReload(); +#endif +server.AddAppsFromAssembly(); +server.AddConnectionsFromAssembly(); +var chromeSettings = new ChromeSettings() + .DefaultApp() + .UseTabs(preventDuplicates: true); +server.UseChrome(chromeSettings); +await server.RunAsync(); diff --git a/xlparser/README.md b/xlparser/README.md new file mode 100644 index 00000000..ab02cfcb --- /dev/null +++ b/xlparser/README.md @@ -0,0 +1,17 @@ +# XLParserDemo + +Web application created using [Ivy](https://github.com/Ivy-Interactive/Ivy). + +Ivy is a web framework for building interactive web applications using C# and .NET. + +## Run + +``` +dotnet watch +``` + +## Deploy + +``` +ivy deploy +``` \ No newline at end of file diff --git a/xlparser/Services/FormulaParser.cs b/xlparser/Services/FormulaParser.cs new file mode 100644 index 00000000..28e91eff --- /dev/null +++ b/xlparser/Services/FormulaParser.cs @@ -0,0 +1,28 @@ +namespace XLParserDemo.Services; + +internal class FormulaParser +{ + public static List ParseFormula(string formula) + { + var parseTreeNodeRoot = ExcelFormulaParser.Parse(formula); + + var nodes = new List(); + TraverseNode(parseTreeNodeRoot, 0, nodes); + + return nodes.GroupBy(x => $"{x.TreeNode.Print()} {x.TreeNode.FindToken().Location}").Select(x => x.Last()).ToList(); + } + + private static void TraverseNode(ParseTreeNode node, int depth, List nodes) + { + if (node == null) return; + nodes.Add(new ParseTreeNodeInfo + { + Depth = depth, + TreeNode = node + }); + foreach (var child in node.ChildNodes) + { + TraverseNode(child, depth + 1, nodes); + } + } +} \ No newline at end of file diff --git a/xlparser/Services/ParseTreeNodeInfo.cs b/xlparser/Services/ParseTreeNodeInfo.cs new file mode 100644 index 00000000..3eb00ad1 --- /dev/null +++ b/xlparser/Services/ParseTreeNodeInfo.cs @@ -0,0 +1,46 @@ +namespace XLParserDemo.Services; + +internal class ParseTreeNodeInfo +{ + public int Depth { get; set; } + public ParseTreeNode TreeNode { get; set; } + + public string NodeValue => TreeNode.Print(); + + public List NodeInfo => + [ + new("Term", TreeNode.Term.ToString()) , + new("Is token", (TreeNode.Token is not null).ToString()) , + new("Found token", TreeNode.FindToken().ToString()) , + new("Found token location", TreeNode.FindToken().Location.ToString()) , + new("Is binary non-reference operation", TreeNode.IsBinaryNonReferenceOperation().ToString()) , + new("Is binary operation", TreeNode.IsBinaryOperation().ToString()) , + new("Is binary reference operation", TreeNode.IsBinaryReferenceOperation().ToString()) , + new("Is built-in function", TreeNode.IsBuiltinFunction().ToString()) , + new("Is external function", TreeNode.IsExternalUDFunction().ToString()) , + new("Is function", TreeNode.IsFunction().ToString()) , + new("Is intersection", TreeNode.IsIntersection().ToString()) , + new("Is named function", TreeNode.IsNamedFunction().ToString()) , + new("Is number with sign", TreeNode.IsNumberWithSign().ToString()) , + new("Is operation", TreeNode.IsOperation().ToString()) , + new("Is operator", TreeNode.IsOperator().ToString()) , + new("Is parentheses", TreeNode.IsParentheses().ToString()) , + new("Is range", TreeNode.IsRange().ToString()) , + new("Is unary operation", TreeNode.IsUnaryOperation().ToString()) , + new("Is unary postfix operation", TreeNode.IsUnaryPostfixOperation().ToString()) , + new("Is unary prefix operation", TreeNode.IsUnaryPrefixOperation().ToString()) , + new("Is union", TreeNode.IsUnion().ToString()) , + ]; +} + +internal class NodeMetadata +{ + public string Key { get; private set; } + public string Value { get; private set; } + + public NodeMetadata(string key, string value) + { + Key = key; + Value = value; + } +} \ No newline at end of file diff --git a/xlparser/XLParserDemo.csproj b/xlparser/XLParserDemo.csproj new file mode 100644 index 00000000..5141dbf0 --- /dev/null +++ b/xlparser/XLParserDemo.csproj @@ -0,0 +1,28 @@ + + + + Exe + net9.0 + enable + enable + CS8618;CS8603;CS8602;CS8604;CS9113 + XLParserDemo + + + + + + + + + + + + + + + + + + + diff --git a/xlparser/XLParserDemo.sln b/xlparser/XLParserDemo.sln new file mode 100644 index 00000000..76ed1a0a --- /dev/null +++ b/xlparser/XLParserDemo.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XLParserDemo", "XLParserDemo.csproj", "{7342A354-70D0-44DF-AD33-296C728D595B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7342A354-70D0-44DF-AD33-296C728D595B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7342A354-70D0-44DF-AD33-296C728D595B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7342A354-70D0-44DF-AD33-296C728D595B}.Debug|x64.ActiveCfg = Debug|Any CPU + {7342A354-70D0-44DF-AD33-296C728D595B}.Debug|x64.Build.0 = Debug|Any CPU + {7342A354-70D0-44DF-AD33-296C728D595B}.Debug|x86.ActiveCfg = Debug|Any CPU + {7342A354-70D0-44DF-AD33-296C728D595B}.Debug|x86.Build.0 = Debug|Any CPU + {7342A354-70D0-44DF-AD33-296C728D595B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7342A354-70D0-44DF-AD33-296C728D595B}.Release|Any CPU.Build.0 = Release|Any CPU + {7342A354-70D0-44DF-AD33-296C728D595B}.Release|x64.ActiveCfg = Release|Any CPU + {7342A354-70D0-44DF-AD33-296C728D595B}.Release|x64.Build.0 = Release|Any CPU + {7342A354-70D0-44DF-AD33-296C728D595B}.Release|x86.ActiveCfg = Release|Any CPU + {7342A354-70D0-44DF-AD33-296C728D595B}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal