From 14e4ee0aacce0d219263cb62236d8c2fd01c0a36 Mon Sep 17 00:00:00 2001 From: Aliaksei Redzko Date: Wed, 24 Sep 2025 20:51:41 +0200 Subject: [PATCH 1/3] Add XLParser demo solution --- xlparser/.gitignore | 5 ++ xlparser/Apps/XLParserApp.cs | 112 +++++++++++++++++++++++++ xlparser/GlobalUsings.cs | 32 +++++++ xlparser/Program.cs | 13 +++ xlparser/README.md | 17 ++++ xlparser/Services/FormulaParser.cs | 28 +++++++ xlparser/Services/ParseTreeNodeInfo.cs | 46 ++++++++++ xlparser/XLParserDemo.csproj | 28 +++++++ xlparser/XLParserDemo.sln | 34 ++++++++ 9 files changed, 315 insertions(+) create mode 100644 xlparser/.gitignore create mode 100644 xlparser/Apps/XLParserApp.cs create mode 100644 xlparser/GlobalUsings.cs create mode 100644 xlparser/Program.cs create mode 100644 xlparser/README.md create mode 100644 xlparser/Services/FormulaParser.cs create mode 100644 xlparser/Services/ParseTreeNodeInfo.cs create mode 100644 xlparser/XLParserDemo.csproj create mode 100644 xlparser/XLParserDemo.sln 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..d2b3f042 --- /dev/null +++ b/xlparser/GlobalUsings.cs @@ -0,0 +1,32 @@ +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; + + +namespace XLParserDemo; diff --git a/xlparser/Program.cs b/xlparser/Program.cs new file mode 100644 index 00000000..5603d810 --- /dev/null +++ b/xlparser/Program.cs @@ -0,0 +1,13 @@ + +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() + + .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..0919ae28 --- /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 opeation", TreeNode.IsBinaryNonReferenceOperation().ToString()) , + new("Is binary opeation", TreeNode.IsBinaryOperation().ToString()) , + new("Is binary reference opeation", 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().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 From 59bd2df431df1273a793235a2feb16c379970fa5 Mon Sep 17 00:00:00 2001 From: Aliaksei Redzko Date: Fri, 26 Sep 2025 09:20:40 +0200 Subject: [PATCH 2/3] Fix typo and remove redundant tostring --- xlparser/Services/ParseTreeNodeInfo.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/xlparser/Services/ParseTreeNodeInfo.cs b/xlparser/Services/ParseTreeNodeInfo.cs index 0919ae28..3eb00ad1 100644 --- a/xlparser/Services/ParseTreeNodeInfo.cs +++ b/xlparser/Services/ParseTreeNodeInfo.cs @@ -13,9 +13,9 @@ internal class ParseTreeNodeInfo 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 opeation", TreeNode.IsBinaryNonReferenceOperation().ToString()) , - new("Is binary opeation", TreeNode.IsBinaryOperation().ToString()) , - new("Is binary reference opeation", TreeNode.IsBinaryReferenceOperation().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()) , @@ -29,7 +29,7 @@ internal class ParseTreeNodeInfo 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().ToString()) , + new("Is union", TreeNode.IsUnion().ToString()) , ]; } From ee954788cdab0d877ebbae925f68edbb8299ca56 Mon Sep 17 00:00:00 2001 From: Aliaksei Redzko Date: Sat, 27 Sep 2025 09:57:10 +0200 Subject: [PATCH 3/3] Set default page --- xlparser/GlobalUsings.cs | 2 ++ xlparser/Program.cs | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/xlparser/GlobalUsings.cs b/xlparser/GlobalUsings.cs index d2b3f042..9a3ee972 100644 --- a/xlparser/GlobalUsings.cs +++ b/xlparser/GlobalUsings.cs @@ -27,6 +27,8 @@ 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 index 5603d810..969dcdec 100644 --- a/xlparser/Program.cs +++ b/xlparser/Program.cs @@ -1,4 +1,3 @@ - CultureInfo.DefaultThreadCurrentCulture = CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("en-US"); var server = new Server(); #if DEBUG @@ -7,7 +6,7 @@ server.AddAppsFromAssembly(); server.AddConnectionsFromAssembly(); var chromeSettings = new ChromeSettings() - + .DefaultApp() .UseTabs(preventDuplicates: true); server.UseChrome(chromeSettings); await server.RunAsync();