Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions xlparser/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
bin/
obj/
*.sln.iml
.idea/**/*
**/.idea/*
112 changes: 112 additions & 0 deletions xlparser/Apps/XLParserApp.cs
Original file line number Diff line number Diff line change
@@ -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<Colors> _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<string, Colors> _foundTokenTypes = [];

private record ParserState(
IState<string> Formula,
IState<FormulaParseResult> Result,
IState<List<ParseTreeNodeInfo>> Tokens,
IState<ParseTreeNodeInfo?> 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<ParseTreeNodeInfo>()),
SelectedToken: UseState<ParseTreeNodeInfo?>()
);

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;
}
}
34 changes: 34 additions & 0 deletions xlparser/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -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;
12 changes: 12 additions & 0 deletions xlparser/Program.cs
Original file line number Diff line number Diff line change
@@ -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<XLParserApp>()
.UseTabs(preventDuplicates: true);
server.UseChrome(chromeSettings);
await server.RunAsync();
17 changes: 17 additions & 0 deletions xlparser/README.md
Original file line number Diff line number Diff line change
@@ -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
```
28 changes: 28 additions & 0 deletions xlparser/Services/FormulaParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace XLParserDemo.Services;

internal class FormulaParser
{
public static List<ParseTreeNodeInfo> ParseFormula(string formula)
{
var parseTreeNodeRoot = ExcelFormulaParser.Parse(formula);

var nodes = new List<ParseTreeNodeInfo>();
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<ParseTreeNodeInfo> nodes)
{
if (node == null) return;
nodes.Add(new ParseTreeNodeInfo
{
Depth = depth,
TreeNode = node
});
foreach (var child in node.ChildNodes)
{
TraverseNode(child, depth + 1, nodes);
}
}
}
46 changes: 46 additions & 0 deletions xlparser/Services/ParseTreeNodeInfo.cs
Original file line number Diff line number Diff line change
@@ -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<NodeMetadata> 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;
}
}
28 changes: 28 additions & 0 deletions xlparser/XLParserDemo.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoWarn>CS8618;CS8603;CS8602;CS8604;CS9113</NoWarn>
<RootNamespace>XLParserDemo</RootNamespace>
</PropertyGroup>

<ItemGroup>
<EmbeddedResource Include="Assets/**/*" />
</ItemGroup>



<ItemGroup>
<PackageReference Include="Ivy" Version="1.0.113.0" />
<PackageReference Include="XLParser" Version="1.7.5" />
</ItemGroup>


<ItemGroup>
<Folder Include="Connections" />
</ItemGroup>

</Project>
34 changes: 34 additions & 0 deletions xlparser/XLParserDemo.sln
Original file line number Diff line number Diff line change
@@ -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