();
17 | var value = false;
18 | var level = 0;
19 | foreach (var line in yaml)
20 | {
21 | var depth = 0;
22 | var indent = tabs;
23 | var text = line;
24 | var size = tabs.Length;
25 | while (text.StartsWith(tabs))
26 | {
27 | if (value && depth > level)
28 | {
29 | break;
30 | }
31 | indent += tabs;
32 | depth++;
33 | text = text.Substring(size);
34 | }
35 | if (value && depth <= level)
36 | {
37 | parentData[parentProperty] = prev.TrimStart('\r', '\n');
38 | parentProperty = "";
39 | value = false;
40 | }
41 | else if (value)
42 | {
43 | prev = prev + Environment.NewLine + text;
44 | }
45 | else
46 | {
47 | text = text.TrimEnd(':', ' ', '|', '-', '+');
48 | if (depth < level)
49 | {
50 | level = depth + 1;
51 | path = string.Join('/', path.Split('/').Reverse().Skip(1).Reverse());
52 | }
53 | if (line.TrimEnd().EndsWith(':'))
54 | {
55 | var asIndex = text.IndexOf(" As ");
56 | var asType = "?";
57 | if (asIndex > -1)
58 | {
59 | asType = text.Substring(asIndex + 4).Trim('"');
60 | text = text.Substring(0, asIndex).Trim('"');
61 | }
62 | var suffix = "/" + text;
63 | if (!path.EndsWith(suffix))
64 | {
65 | path += suffix;
66 | types[path] = asType;
67 | data[path] = parentData = new();
68 | }
69 | level = depth + 1;
70 | }
71 | else if (text.Contains(':') && text.Contains('='))
72 | {
73 | var valueIndex = text.IndexOf(':');
74 | var valueText = text.Substring(valueIndex + 1);
75 | text = text.Substring(0, valueIndex);
76 | parentData[text] = valueText.Trim().TrimStart('=');
77 | }
78 | if (line.EndsWith("|-") || line.EndsWith("|+") || line.EndsWith("|"))
79 | {
80 | parentProperty = text;
81 | prev = "";
82 | value = true;
83 | }
84 | else
85 | {
86 | prev = text;
87 | }
88 | }
89 | }
90 | return new(types, data);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Pages/Index.razor.css:
--------------------------------------------------------------------------------
1 | .power-note-app {
2 | display: flex;
3 | flex-direction: column;
4 | flex: 1;
5 | inset: 0;
6 | position: fixed;
7 | background-color: #0B0D21;
8 | }
9 |
10 | .power-note-app header {
11 | flex: 1;
12 | display: flex;
13 | flex-direction: row;
14 | order: 1;
15 | max-height: 70px;
16 | background-color: #22264C;
17 | }
18 |
19 | .power-note-app header .title {
20 | flex: 5;
21 | order: 1;
22 | margin: 0;
23 | line-height: 70px;
24 | cursor: default;
25 | }
26 |
27 | .power-note-app header .files {
28 | flex: 2;
29 | order: 2;
30 | position: relative;
31 | overflow: hidden;
32 | }
33 |
34 | .power-note-app header .files .file-types dl {
35 | margin: 0;
36 | }
37 |
38 | .power-note-app header .files .file-types dt {
39 | margin: 10px 0;
40 | }
41 |
42 | .power-note-app header .files.full-screen {
43 | inset: 0;
44 | position: fixed;
45 | z-index: 1;
46 | }
47 |
48 | .power-note-app header .files.full-screen .file-types {
49 | position: relative;
50 | margin: 50px auto;
51 | padding: 0 30px;
52 | height: 250px;
53 | width: 350px;
54 | background-color: #0B0D21;
55 | border: solid 4px #45496D;
56 | }
57 |
58 | .power-note-app header .files.full-screen .file-types img {
59 | width: 40px;
60 | }
61 |
62 | .power-note-app header .files.full-screen .file-types i {
63 | font-size: .8em;
64 | line-height: 30px;
65 | color: #3E3E42;
66 | }
67 |
68 | .power-note-app header .files.top-right .file-types img {
69 | width: 20px;
70 | }
71 |
72 | .power-note-app header .files.top-right .file-types dl {
73 | text-align: right;
74 | }
75 |
76 | .power-note-app header .files.top-right .file-types dt {
77 | margin-right: 10px;
78 | font-size: .9em;
79 | }
80 |
81 | .power-note-app header .files.top-right .file-types dd {
82 | float: right;
83 | margin-right: 10px;
84 | margin-inline-start: 0;
85 | white-space: nowrap;
86 | font-size: .8em;
87 | }
88 |
89 | .power-note-app header .files.top-right .file-types i {
90 | display: none;
91 | }
92 |
93 | .power-note-app .app-object {
94 | flex: 1;
95 | display: flex;
96 | flex-direction: row;
97 | }
98 |
99 | .power-note-app .app-object span {
100 | flex: 10;
101 | flex-grow: inherit;
102 | line-height: 32px;
103 | }
104 |
105 | .power-note-app .app-object *:last-child {
106 | flex: 1;
107 | width: 30px;
108 | }
109 |
110 | .power-note-app main {
111 | flex: 10;
112 | display: flex;
113 | flex-direction: row;
114 | order: 2;
115 | }
116 |
117 | .power-note-app main aside {
118 | flex: 1;
119 | order: 1;
120 | min-width: 250px;
121 | max-width: 350px;
122 | background-color: #0B0D21;
123 | border-right: solid 4px #45496D;
124 | overflow: auto;
125 | max-height: 100vh;
126 | }
127 |
128 | .power-note-app main .content {
129 | flex: 10;
130 | order: 2;
131 | }
132 |
--------------------------------------------------------------------------------
/src/Properties/ServiceDependencies/MR365app - Web Deploy/profile.arm.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
3 | "contentVersion": "1.0.0.0",
4 | "metadata": {
5 | "_dependencyType": "compute.appService.windows"
6 | },
7 | "parameters": {
8 | "resourceGroupName": {
9 | "type": "string",
10 | "defaultValue": "MR365app-rg",
11 | "metadata": {
12 | "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking."
13 | }
14 | },
15 | "resourceGroupLocation": {
16 | "type": "string",
17 | "defaultValue": "eastus",
18 | "metadata": {
19 | "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support."
20 | }
21 | },
22 | "resourceName": {
23 | "type": "string",
24 | "defaultValue": "MR365app",
25 | "metadata": {
26 | "description": "Name of the main resource to be created by this template."
27 | }
28 | },
29 | "resourceLocation": {
30 | "type": "string",
31 | "defaultValue": "[parameters('resourceGroupLocation')]",
32 | "metadata": {
33 | "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there."
34 | }
35 | }
36 | },
37 | "variables": {
38 | "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
39 | "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]"
40 | },
41 | "resources": [
42 | {
43 | "type": "Microsoft.Resources/resourceGroups",
44 | "name": "[parameters('resourceGroupName')]",
45 | "location": "[parameters('resourceGroupLocation')]",
46 | "apiVersion": "2019-10-01"
47 | },
48 | {
49 | "type": "Microsoft.Resources/deployments",
50 | "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
51 | "resourceGroup": "[parameters('resourceGroupName')]",
52 | "apiVersion": "2019-10-01",
53 | "dependsOn": [
54 | "[parameters('resourceGroupName')]"
55 | ],
56 | "properties": {
57 | "mode": "Incremental",
58 | "template": {
59 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
60 | "contentVersion": "1.0.0.0",
61 | "resources": [
62 | {
63 | "location": "[parameters('resourceLocation')]",
64 | "name": "[parameters('resourceName')]",
65 | "type": "Microsoft.Web/sites",
66 | "apiVersion": "2015-08-01",
67 | "tags": {
68 | "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty"
69 | },
70 | "dependsOn": [
71 | "[variables('appServicePlan_ResourceId')]"
72 | ],
73 | "kind": "app",
74 | "properties": {
75 | "name": "[parameters('resourceName')]",
76 | "kind": "app",
77 | "httpsOnly": true,
78 | "reserved": false,
79 | "serverFarmId": "[variables('appServicePlan_ResourceId')]",
80 | "siteConfig": {
81 | "metadata": [
82 | {
83 | "name": "CURRENT_STACK",
84 | "value": "dotnetcore"
85 | }
86 | ]
87 | }
88 | },
89 | "identity": {
90 | "type": "SystemAssigned"
91 | }
92 | },
93 | {
94 | "location": "[parameters('resourceLocation')]",
95 | "name": "[variables('appServicePlan_name')]",
96 | "type": "Microsoft.Web/serverFarms",
97 | "apiVersion": "2015-08-01",
98 | "sku": {
99 | "name": "S1",
100 | "tier": "Standard",
101 | "family": "S",
102 | "size": "S1"
103 | },
104 | "properties": {
105 | "name": "[variables('appServicePlan_name')]"
106 | }
107 | }
108 | ]
109 | }
110 | }
111 | }
112 | ]
113 | }
--------------------------------------------------------------------------------
/src/Services/AppPreviewer.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using System.Text.Json;
3 | using System.Text.RegularExpressions;
4 |
5 | namespace PowerNote.Services;
6 |
7 | public record PowerAppPreview(string Name, string Component, string Html, string Code);
8 | public class AppPreviewer
9 | {
10 | private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true };
11 |
12 | private static readonly string[] _mappedProperties = new string[]
13 | {
14 | "X",
15 | "Y",
16 | "Height",
17 | "Width",
18 | "ZIndex",
19 | "Size",
20 | "Text",
21 | "Fill",
22 | "Color",
23 | "BorderColor",
24 | "BorderThickness"
25 | };
26 |
27 | public PowerAppPreview GetAppComponentPreview(PowerApp app, PowerAppComponent component)
28 | {
29 | var code = new StringBuilder();
30 | code.AppendLine();
31 |
32 | var html = new StringBuilder();
33 | html.AppendLine("");
34 |
35 | foreach (var obj in component.Objects)
36 | {
37 | if (!obj.Name.StartsWith("Screen"))
38 | {
39 | code.AppendLine(GetObjectTable(component, obj));
40 | html.AppendLine(GetObjectPreview(component, obj));
41 | }
42 | }
43 |
44 | html.AppendLine("
");
45 |
46 | var preview = html.ToString();
47 | var powerfx = code.ToString();
48 | Console.WriteLine("SetOutput: " + preview);
49 | Console.WriteLine("SetCode: " + powerfx);
50 | return new(app.Name, component.Name, preview, powerfx);
51 | }
52 |
53 | public string GetObjectTable(PowerAppComponent component, PowerAppComponentObject obj)
54 | {
55 | var code = new StringBuilder();
56 | code.AppendLine();
57 |
58 | var type = component.Types.TryGetValue(obj.Name, out var t) ? t : "Cell";
59 | var properties = obj.Data
60 | .Where(p => p.Key != "OnSelect" && p.Key != "Text" && !_mappedProperties.Contains(p.Key))
61 | .Select(p => $"{p.Key}: \"{p.Value}\"")
62 | .ToArray();
63 | if (properties.Any())
64 | {
65 | code.AppendLine($"{getName(obj.Name)} = Table({{ Name:\"{obj.Name}\", Type:\"{type}\", {string.Join(", ", properties)} }})");
66 | }
67 | else
68 | {
69 | code.AppendLine($"{getName(obj.Name)} = Table({{ Name:\"{obj.Name}\", Type:\"{type}\" }})");
70 | }
71 | if (obj.Data.TryGetValue("OnSelect", out var onSelect))
72 | {
73 | var val = onSelect.TrimStart('=');
74 | code.AppendLine($"OnSelect = {val}");
75 | }
76 | else if (obj.Data.TryGetValue("Text", out var text))
77 | {
78 | var val = text.TrimStart('=');
79 | code.AppendLine($"Text = {val}");
80 | code.AppendLine();
81 | }
82 |
83 | return code.ToString();
84 | }
85 |
86 | private string getName(string value)
87 | {
88 | return value.TrimStart('/').Replace("/", "_");
89 | }
90 |
91 | public string GetObjectPreview(PowerAppComponent component, PowerAppComponentObject obj)
92 | {
93 | var html = new StringBuilder();
94 | var type = component.Types.TryGetValue(obj.Name, out var t) ? t : "Cell";
95 | html.Append(" $"top:{getNumber(value)}px;",
108 | "Y" => $"left:{getNumber(value)}px;",
109 | "Height" => $"height:{getNumber(value)}px;",
110 | "Width" => $"width:{getNumber(value)}px;",
111 | "ZIndex" => $"z-index:{getNumber(value)};",
112 | "Size" => $"font-size:{getNumber(value)}px;",
113 | "Text" => $"content:'{getText(value)}';",
114 | "Fill" => $"background-color:{getColor(value)};",
115 | "Color" => $"color:{getColor(value)};",
116 | "BorderColor" => $"border-color:{getColor(value)};",
117 | "BorderThickness" => $"border-width:{getNumber(value)}px;",
118 | _ => ""
119 | });
120 | }
121 | }
122 | html.AppendLine("\">");
123 | html.AppendLine($"
{getName(obj.Name)} ({type})
");
124 |
125 | if (obj.Data.TryGetValue("Text", out var text))
126 | {
127 | var val = text.TrimStart('=');
128 | html.AppendLine(val);
129 | }
130 |
131 | html.AppendLine("");
132 | return html.ToString();
133 | }
134 |
135 | private string getColor(string value)
136 | {
137 | if (value.StartsWith("RGBA("))
138 | {
139 | return value.Replace("RGBA", "rgba");
140 | }
141 | else
142 | {
143 | return value;
144 | }
145 | }
146 |
147 | private string getNumber(string value)
148 | {
149 | if (System.Text.RegularExpressions.Regex.IsMatch(value, "\\D"))
150 | {
151 | return "00";
152 | }
153 | else
154 | {
155 | return value;
156 | }
157 | }
158 |
159 | private string getText(string value)
160 | {
161 | if (value.Contains('('))
162 | {
163 | return "";
164 | }
165 | else
166 | {
167 | return value.Replace("\"", "");
168 | }
169 | }
170 |
171 | }
172 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Build results
17 | [Dd]ebug/
18 | [Dd]ebugPublic/
19 | [Rr]elease/
20 | [Rr]eleases/
21 | x64/
22 | x86/
23 | bld/
24 | [Bb]in/
25 | [Oo]bj/
26 | [Ll]og/
27 |
28 | # Visual Studio 2015/2017 cache/options directory
29 | .vs/
30 | # Uncomment if you have tasks that create the project's static files in wwwroot
31 | #wwwroot/
32 |
33 | # Visual Studio 2017 auto generated files
34 | Generated\ Files/
35 |
36 | # MSTest test Results
37 | [Tt]est[Rr]esult*/
38 | [Bb]uild[Ll]og.*
39 |
40 | # NUNIT
41 | *.VisualState.xml
42 | TestResult.xml
43 |
44 | # Build Results of an ATL Project
45 | [Dd]ebugPS/
46 | [Rr]eleasePS/
47 | dlldata.c
48 |
49 | # Benchmark Results
50 | BenchmarkDotNet.Artifacts/
51 |
52 | # .NET Core
53 | project.lock.json
54 | project.fragment.lock.json
55 | artifacts/
56 |
57 | # StyleCop
58 | StyleCopReport.xml
59 |
60 | # Files built by Visual Studio
61 | *_i.c
62 | *_p.c
63 | *_h.h
64 | *.ilk
65 | *.meta
66 | *.obj
67 | *.iobj
68 | *.pch
69 | *.pdb
70 | *.ipdb
71 | *.pgc
72 | *.pgd
73 | *.rsp
74 | *.sbr
75 | *.tlb
76 | *.tli
77 | *.tlh
78 | *.tmp
79 | *.tmp_proj
80 | *_wpftmp.csproj
81 | *.log
82 | *.vspscc
83 | *.vssscc
84 | .builds
85 | *.pidb
86 | *.svclog
87 | *.scc
88 |
89 | # Chutzpah Test files
90 | _Chutzpah*
91 |
92 | # Visual C++ cache files
93 | ipch/
94 | *.aps
95 | *.ncb
96 | *.opendb
97 | *.opensdf
98 | *.sdf
99 | *.cachefile
100 | *.VC.db
101 | *.VC.VC.opendb
102 |
103 | # Visual Studio profiler
104 | *.psess
105 | *.vsp
106 | *.vspx
107 | *.sap
108 |
109 | # Visual Studio Trace Files
110 | *.e2e
111 |
112 | # TFS 2012 Local Workspace
113 | $tf/
114 |
115 | # Guidance Automation Toolkit
116 | *.gpState
117 |
118 | # ReSharper is a .NET coding add-in
119 | _ReSharper*/
120 | *.[Rr]e[Ss]harper
121 | *.DotSettings.user
122 |
123 | # JustCode is a .NET coding add-in
124 | .JustCode
125 |
126 | # TeamCity is a build add-in
127 | _TeamCity*
128 |
129 | # DotCover is a Code Coverage Tool
130 | *.dotCover
131 |
132 | # AxoCover is a Code Coverage Tool
133 | .axoCover/*
134 | !.axoCover/settings.json
135 |
136 | # Visual Studio code coverage results
137 | *.coverage
138 | *.coveragexml
139 |
140 | # NCrunch
141 | _NCrunch_*
142 | .*crunch*.local.xml
143 | nCrunchTemp_*
144 |
145 | # MightyMoose
146 | *.mm.*
147 | AutoTest.Net/
148 |
149 | # Web workbench (sass)
150 | .sass-cache/
151 |
152 | # Installshield output folder
153 | [Ee]xpress/
154 |
155 | # DocProject is a documentation generator add-in
156 | DocProject/buildhelp/
157 | DocProject/Help/*.HxT
158 | DocProject/Help/*.HxC
159 | DocProject/Help/*.hhc
160 | DocProject/Help/*.hhk
161 | DocProject/Help/*.hhp
162 | DocProject/Help/Html2
163 | DocProject/Help/html
164 |
165 | # Click-Once directory
166 | publish/
167 |
168 | # Publish Web Output
169 | *.[Pp]ublish.xml
170 | *.azurePubxml
171 | # Note: Comment the next line if you want to checkin your web deploy settings,
172 | # but database connection strings (with potential passwords) will be unencrypted
173 | *.pubxml
174 | *.publishproj
175 |
176 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
177 | # checkin your Azure Web App publish settings, but sensitive information contained
178 | # in these scripts will be unencrypted
179 | PublishScripts/
180 |
181 | # NuGet Packages
182 | *.nupkg
183 | # The packages folder can be ignored because of Package Restore
184 | **/[Pp]ackages/*
185 | # except build/, which is used as an MSBuild target.
186 | !**/[Pp]ackages/build/
187 | # Uncomment if necessary however generally it will be regenerated when needed
188 | #!**/[Pp]ackages/repositories.config
189 | # NuGet v3's project.json files produces more ignorable files
190 | *.nuget.props
191 | *.nuget.targets
192 |
193 | # Microsoft Azure Build Output
194 | csx/
195 | *.build.csdef
196 |
197 | # Microsoft Azure Emulator
198 | ecf/
199 | rcf/
200 |
201 | # Windows Store app package directories and files
202 | AppPackages/
203 | BundleArtifacts/
204 | Package.StoreAssociation.xml
205 | _pkginfo.txt
206 | *.appx
207 |
208 | # Visual Studio cache files
209 | # files ending in .cache can be ignored
210 | *.[Cc]ache
211 | # but keep track of directories ending in .cache
212 | !*.[Cc]ache/
213 |
214 | # Others
215 | ClientBin/
216 | ~$*
217 | *~
218 | *.dbmdl
219 | *.dbproj.schemaview
220 | *.jfm
221 | *.pfx
222 | *.publishsettings
223 | orleans.codegen.cs
224 |
225 | # Including strong name files can present a security risk
226 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
227 | #*.snk
228 |
229 | # Since there are multiple workflows, uncomment next line to ignore bower_components
230 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
231 | #bower_components/
232 |
233 | # RIA/Silverlight projects
234 | Generated_Code/
235 |
236 | # Backup & report files from converting an old project file
237 | # to a newer Visual Studio version. Backup files are not needed,
238 | # because we have git ;-)
239 | _UpgradeReport_Files/
240 | Backup*/
241 | UpgradeLog*.XML
242 | UpgradeLog*.htm
243 | ServiceFabricBackup/
244 | *.rptproj.bak
245 |
246 | # SQL Server files
247 | *.mdf
248 | *.ldf
249 | *.ndf
250 |
251 | # Business Intelligence projects
252 | *.rdl.data
253 | *.bim.layout
254 | *.bim_*.settings
255 | *.rptproj.rsuser
256 |
257 | # Microsoft Fakes
258 | FakesAssemblies/
259 |
260 | # GhostDoc plugin setting file
261 | *.GhostDoc.xml
262 |
263 | # Node.js Tools for Visual Studio
264 | .ntvs_analysis.dat
265 | node_modules/
266 |
267 | # Visual Studio 6 build log
268 | *.plg
269 |
270 | # Visual Studio 6 workspace options file
271 | *.opt
272 |
273 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
274 | *.vbw
275 |
276 | # Visual Studio LightSwitch build output
277 | **/*.HTMLClient/GeneratedArtifacts
278 | **/*.DesktopClient/GeneratedArtifacts
279 | **/*.DesktopClient/ModelManifest.xml
280 | **/*.Server/GeneratedArtifacts
281 | **/*.Server/ModelManifest.xml
282 | _Pvt_Extensions
283 |
284 | # Paket dependency manager
285 | .paket/paket.exe
286 | paket-files/
287 |
288 | # FAKE - F# Make
289 | .fake/
290 |
291 | # JetBrains Rider
292 | .idea/
293 | *.sln.iml
294 |
295 | # CodeRush personal settings
296 | .cr/personal
297 |
298 | # Python Tools for Visual Studio (PTVS)
299 | __pycache__/
300 | *.pyc
301 |
302 | # Cake - Uncomment if you are using it
303 | # tools/**
304 | # !tools/packages.config
305 |
306 | # Tabs Studio
307 | *.tss
308 |
309 | # Telerik's JustMock configuration file
310 | *.jmconfig
311 |
312 | # BizTalk build output
313 | *.btp.cs
314 | *.btm.cs
315 | *.odx.cs
316 | *.xsd.cs
317 |
318 | # OpenCover UI analysis results
319 | OpenCover/
320 |
321 | # Azure Stream Analytics local run output
322 | ASALocalRun/
323 |
324 | # MSBuild Binary and Structured Log
325 | *.binlog
326 |
327 | # NVidia Nsight GPU debugger configuration file
328 | *.nvuser
329 |
330 | # MFractors (Xamarin productivity tool) working folder
331 | .mfractor/
332 |
333 | # Local History for Visual Studio
334 | .localhistory/
335 |
--------------------------------------------------------------------------------
/src/Services/FileManager.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Components.Forms;
2 | using System.IO.Compression;
3 | using System.Text.RegularExpressions;
4 |
5 | namespace PowerNote.Services;
6 |
7 | public class FileManager
8 | {
9 | private const string Letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
10 | private const string AppsFolderName = "Apps";
11 | private DirectoryInfo AppsFolder => Directory.CreateDirectory(AppsFolderName);
12 | private string AppFilePath(string name) => Path.Combine(AppsFolder.FullName, name);
13 | private string ColumnLetter(int column) =>
14 | column < 1
15 | ? "A"
16 | :
17 | column < Letters.Length
18 | ? Letters[column - 1].ToString()
19 | : string.Concat(Enumerable.Repeat(Letters[column - 1].ToString(), column / Letters.Length));
20 |
21 | private Dictionary _powerApps = new();
22 | private string _codeFromFile;
23 | private readonly AppReader _appReader;
24 | private readonly ExcelReader _excelReader;
25 |
26 | public FileManager(AppReader appReader, ExcelReader excelReader)
27 | {
28 | _appReader = appReader;
29 | _excelReader = excelReader;
30 | }
31 |
32 | public async ValueTask GetPowerAppFileAsync(string name) => await File.ReadAllBytesAsync(_powerApps[name].FilePath);
33 |
34 | public PowerAppComponent[] GetPowerAppComponents(string name) => _powerApps[name].Components;
35 |
36 | public PowerApp[] GetPowerApps() => _powerApps.Values.ToArray();
37 | public string GetLoadedCode() => _codeFromFile;
38 |
39 | public void WriteCode(string name, byte[] bytes) => File.WriteAllBytes(AppFilePath(name + ".pfx"), bytes);
40 |
41 | public void WriteApp(string name, byte[] bytes) => File.WriteAllBytes(AppFilePath(name + ".msapp"), bytes);
42 |
43 | public void WriteExcel(string name, byte[] bytes) => File.WriteAllBytes(AppFilePath(name + ".xlsx"), bytes);
44 |
45 | public void WriteFlow(string name, byte[] bytes) => File.WriteAllBytes(AppFilePath(name + ".json"), bytes);
46 |
47 | public string GetAppUrl(PowerApp app)
48 | {
49 | var ext = Path.GetExtension(app.FilePath).TrimStart('.').ToLower();
50 | return UrlManager.CreateUrl(File.ReadAllBytes(app.FilePath), ext);
51 | }
52 |
53 | public void ReadAppsFolder()
54 | {
55 | var dir = AppsFolder;
56 | var files = Directory.EnumerateFiles(dir.FullName, "*.*", SearchOption.AllDirectories).ToArray();
57 | foreach (var file in files)
58 | {
59 | var name = Path.GetFileNameWithoutExtension(file);
60 | var ext = Path.GetExtension(file).TrimStart('.');
61 | try
62 | {
63 | if (ext.Equals("msapp", StringComparison.InvariantCultureIgnoreCase))
64 | {
65 | if (!_powerApps.TryGetValue(name, out var app))
66 | {
67 | _powerApps[name] = _appReader.ReadApp(file);
68 | }
69 | else
70 | {
71 | Console.WriteLine($"ReadApp ({name}): Already loaded");
72 | }
73 | }
74 | else if (ext.Equals("zip", StringComparison.InvariantCultureIgnoreCase))
75 | {
76 | SaveAppPackage(File.ReadAllBytes(file));
77 | }
78 | else if (ext.Equals("xlsx", StringComparison.InvariantCultureIgnoreCase))
79 | {
80 | var components = ReadExcelFile(file);
81 | _powerApps[name] = new(name, components, file);
82 | }
83 | else if (ext.Equals("json", StringComparison.InvariantCultureIgnoreCase))
84 | {
85 | _powerApps[name] = ReadFlow(file);
86 | }
87 | else if (ext.Equals("pfx", StringComparison.InvariantCultureIgnoreCase))
88 | {
89 | _codeFromFile = File.ReadAllText(file);
90 | }
91 | else
92 | {
93 | Console.WriteLine($"Removing unnecessary file: {file}");
94 | File.Delete(file);
95 | }
96 | }
97 | catch (Exception ex)
98 | {
99 | Console.WriteLine($"ReadApp Error ({name}): {ex.ToString()}");
100 | }
101 | }
102 | }
103 |
104 | public void SaveAppPackage(byte[] zipFile)
105 | {
106 | var apps = WriteAppPackage(zipFile);
107 | foreach(var app in apps)
108 | {
109 | _powerApps[app.Name] = app;
110 | }
111 | }
112 |
113 | private PowerAppComponent[] ReadExcelFile(string xlsxFilePath)
114 | {
115 | var components = new List();
116 | var cells = _excelReader.ReadExcel(xlsxFilePath);
117 | var sheets = cells.GroupBy(c => c.Sheet).ToArray();
118 | foreach (var sheet in sheets)
119 | {
120 | var rows = sheet.GroupBy(c => c.Row).ToArray();
121 | var types = new Dictionary();
122 | var objects = new Dictionary>();
123 | foreach (var row in rows)
124 | {
125 | foreach (var column in row)
126 | {
127 | var cell = column.Formula ?? column.Text;
128 | if (!string.IsNullOrWhiteSpace(cell))
129 | {
130 | objects[$"{ColumnLetter(column.Column)}{column.Row}"] = new()
131 | {
132 | { "Text", cell },
133 | { "X", ((column.Column - 1) * 120).ToString() },
134 | { "Y", ((column.Row - 1) * 20).ToString() },
135 | { "Width", "120" },
136 | { "Height", "120" }
137 | };
138 | }
139 | }
140 | }
141 | components.Add(new(sheet.Key, types, objects));
142 | }
143 | return components.ToArray();
144 | }
145 |
146 | public PowerApp ReadFlow(string jsonFilePath)
147 | {
148 | var name = Path.GetFileNameWithoutExtension(jsonFilePath);
149 | var json = File.ReadAllText(jsonFilePath);
150 | var formulas = Regex.Matches(json, @"""@\{?.*\}?""").Select(m => m.Value).ToArray();
151 | var component = new PowerAppComponent("Formulas", new(), formulas.Distinct().ToDictionary(f => f, formula => new Dictionary()
152 | {
153 | { "Text", formula }
154 | }));
155 | return new(name, new[] { component }, jsonFilePath);
156 | }
157 |
158 | public async ValueTask UploadFilesAsync(IEnumerable files)
159 | {
160 | foreach (var item in files)
161 | {
162 | var path = AppFilePath(item.Name);
163 | using var stream = item.OpenReadStream();
164 | using var file = File.Create(path);
165 | await stream.CopyToAsync(file);
166 | }
167 | }
168 |
169 | private IEnumerable WriteAppPackage(byte[] zipFile)
170 | {
171 | using var stream = new MemoryStream(zipFile);
172 | using var zip = new ZipArchive(stream, ZipArchiveMode.Read);
173 | foreach (var entry in zip.Entries)
174 | {
175 | if (entry.Name.EndsWith(".msapp", StringComparison.InvariantCultureIgnoreCase))
176 | {
177 | var path = AppFilePath(entry.Name);
178 | entry.ExtractToFile(path);
179 | yield return _appReader.ReadApp(path);
180 | }
181 | else if (entry.Name.EndsWith(".json", StringComparison.InvariantCultureIgnoreCase)
182 | && entry.FullName.Contains("workflows", StringComparison.InvariantCultureIgnoreCase))
183 | {
184 | var path = AppFilePath(entry.Name);
185 | entry.ExtractToFile(path);
186 | yield return ReadFlow(path);
187 | }
188 | }
189 | }
190 | }
--------------------------------------------------------------------------------
/src/Services/ExcelReader.cs:
--------------------------------------------------------------------------------
1 | using PowerNote.Models;
2 | using System.IO.Compression;
3 | using System.Text.RegularExpressions;
4 | using System.Xml;
5 | using System.Xml.Serialization;
6 |
7 | namespace PowerNote.Services;
8 |
9 | public record SpreadsheetCellReference(string Sheet, int Row, int Column);
10 | public record SpreadsheetCellRange(string FromSheet, int FromRow, int FromColumn, int ToRow, int ToColumn) : SpreadsheetCellReference(FromSheet, FromRow, FromColumn);
11 | public record SpreadsheetCell(string CellSheet, int CellRow, int CellColumn, string Text, string Formula) : SpreadsheetCellReference(CellSheet, CellRow, CellColumn)
12 | {
13 | public SpreadsheetCellReference MergedToCell { get; set; }
14 | }
15 |
16 | public class ExcelReader
17 | {
18 | public IEnumerable ReadExcel(string xlsxPath)
19 | {
20 | var items = new List();
21 | using var file = File.OpenRead(xlsxPath);
22 | using var zipArchive = new ZipArchive(file, ZipArchiveMode.Read, false);
23 | var workbook = ObjectZipEntry(GetZipArchiveEntry(zipArchive, "xl/workbook.xml"));
24 | var stringTable = ObjectZipEntry(GetZipArchiveEntry(zipArchive, "xl/sharedStrings.xml"))?.Items;
25 | var relationships = XmlDocumentZipEntry(GetZipArchiveEntry(zipArchive, "xl/_rels/workbook.xml.rels"));
26 | foreach (var sheet in workbook.Sheets)
27 | {
28 | if (!string.IsNullOrEmpty(sheet.SheetReferenceId))
29 | {
30 | var sheetPath = FindRelationshipTarget(sheet.SheetReferenceId, relationships);
31 | if (!string.IsNullOrEmpty(sheetPath))
32 | {
33 | var worksheet = ObjectZipEntry(GetZipArchiveEntry(zipArchive, "xl/" + sheetPath));
34 | if (worksheet.Rows != null)
35 | {
36 | var merged_items = new List();
37 | if (worksheet.MergedCells != null && worksheet.MergedCells.Length > 0)
38 | {
39 | merged_items.AddRange(worksheet.MergedCells.Where(m => m.CellRange.Contains(":")).Select(m =>
40 | {
41 | var cellRange = GetCellReference(m.CellRange, sheet.Name) as SpreadsheetCellRange;
42 | if (cellRange != null)
43 | {
44 | return cellRange;
45 | }
46 | else
47 | {
48 | return null;
49 | }
50 | }).Where(r => r != null));
51 | }
52 | var sheetIndex = workbook.Sheets.ToList().IndexOf(sheet);
53 | items.AddRange(from row in worksheet.Rows
54 | where row.Cells != null
55 | from cell in row.Cells
56 | let t = GetText(stringTable, cell)
57 | where !string.IsNullOrEmpty(t) || !string.IsNullOrEmpty(cell.Formula)
58 | let s = sheet.Name
59 | let r = row.RowReference - 1
60 | let c = GetColumnIndex(cell.CellReference)
61 | let m = merged_items.Where(m => m.Sheet == s && m.Row == r && m.Column == c).FirstOrDefault()
62 | select new SpreadsheetCell(s, r, c, t, cell.Formula)
63 | {
64 | MergedToCell = (m != null ? new SpreadsheetCellReference(m.Sheet, m.Row, m.Column) : null)
65 | });
66 | }
67 | }
68 | }
69 | }
70 | return items;
71 | }
72 |
73 | private static string GetText(ExcelWorkbookStringTableText[] stringTableItems, ExcelWorksheetCell cell)
74 | {
75 | int s = 0;
76 | if (cell.CellType == "s" && int.TryParse(cell.Value, out s) && s < stringTableItems.Length)
77 | {
78 | return stringTableItems[s].Text?.Trim();
79 | }
80 | else if (cell.CellType == "str")
81 | {
82 | if (!string.IsNullOrEmpty(cell.Value))
83 | {
84 | return cell.Value.Trim();
85 | }
86 | else
87 | {
88 | return string.Empty;
89 | }
90 | }
91 | else if (!string.IsNullOrEmpty(cell.Value))
92 | {
93 | //double amount;
94 | //if (double.TryParse(cell.Value, out amount))
95 | //{
96 | // var date = DateTime.FromOADate(amount);
97 | // return date.ToShortDateString();
98 | // //return amount.ToString("#,##0.##");
99 | //}
100 | //else
101 | //{
102 | // return cell.Value;
103 | //}
104 | return cell.Value;
105 | }
106 | else
107 | {
108 | return string.Empty;
109 | }
110 | }
111 |
112 | private static int GetColumnIndex(string cellReference)
113 | {
114 | var colLetters = new Regex("[A-Za-z]+").Match(cellReference).Value.ToUpper();
115 | var colIndex = 0;
116 | for (int i = 0; i < colLetters.Length; i++)
117 | {
118 | colIndex *= 26;
119 | colIndex += (colLetters[i] - 'A' + 1);
120 | }
121 | return colIndex - 1;
122 | }
123 |
124 | private static int GetRowIndex(string cellReference)
125 | {
126 | var cellNumbers = new Regex("[0-9]+").Match(cellReference).Value;
127 | if (!string.IsNullOrEmpty(cellNumbers))
128 | {
129 | return Convert.ToInt32(cellNumbers) - 1;
130 | }
131 | else
132 | {
133 | return -1;
134 | }
135 | }
136 |
137 | private static SpreadsheetCellReference GetCellReference(string cellReference, string currentSheet)
138 | {
139 | if (cellReference.Contains(':'))
140 | {
141 | var cellFrom = GetCellReference(cellReference.Split(':')[0], currentSheet);
142 | var cellTo = GetCellReference(cellReference.Split(':')[1], currentSheet);
143 | if (cellFrom.Row > cellTo.Row || cellFrom.Column > cellTo.Column)
144 | {
145 | return null;
146 | }
147 | else
148 | {
149 | return new SpreadsheetCellRange(cellFrom.Sheet, cellFrom.Row, cellFrom.Column, cellTo.Row, cellTo.Column);
150 | }
151 | }
152 | else if (cellReference.Contains("#REF!"))
153 | {
154 | return null;
155 | }
156 | else if (cellReference.Contains('!'))
157 | {
158 | var sheet = cellReference.Split('!')[0].Replace("'", "");
159 | var cell = cellReference.Split('!')[1];
160 | var row = GetRowIndex(cell);
161 | var column = GetColumnIndex(cell);
162 | return new SpreadsheetCellReference(sheet, row, column);
163 | }
164 | else
165 | {
166 | var row = GetRowIndex(cellReference);
167 | var column = GetColumnIndex(cellReference);
168 | return new SpreadsheetCellReference(currentSheet, row, column);
169 | }
170 | }
171 |
172 | private static string FindRelationshipTarget(string relId, XmlDocument relationships)
173 | {
174 | var sheetReference = relationships.SelectSingleNode("//node()[@Id='" + relId + "']");
175 | if (sheetReference != null)
176 | {
177 | var targetAttribute = sheetReference.Attributes["Target"];
178 | if (targetAttribute != null)
179 | {
180 | return targetAttribute.Value;
181 | }
182 | }
183 | return null;
184 | }
185 |
186 | private static ZipArchiveEntry GetZipArchiveEntry(ZipArchive zipArchive, string zipPath)
187 | {
188 | return zipArchive.Entries.First(n => n.FullName.Equals(zipPath));
189 | }
190 |
191 | private static T ObjectZipEntry(ZipArchiveEntry zipArchiveEntry)
192 | {
193 | using (var stream = zipArchiveEntry.Open())
194 | return (T)new XmlSerializer(typeof(T)).Deserialize(XmlReader.Create(stream));
195 | }
196 |
197 | private static XmlDocument XmlDocumentZipEntry(ZipArchiveEntry zipArchiveEntry)
198 | {
199 | var xmlDocument = new XmlDocument();
200 | using (var stream = zipArchiveEntry.Open())
201 | {
202 | xmlDocument.Load(stream);
203 | return xmlDocument;
204 | }
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/src/wwwroot/scripts/MonacoEditor.js:
--------------------------------------------------------------------------------
1 | function createHandler(text) {
2 | return new Function("value", `return ${text}`);
3 | }
4 | var _updateMonacoEditor;
5 | export function updateMonacoEditor(code) {
6 | if (typeof (_updateMonacoEditor) === 'function') {
7 | _updateMonacoEditor(code);
8 | }
9 | }
10 | export function loadMonacoEditor(id, code, onChangeHandler, onHoverHandler, onSuggestHandler, onSaveHandler, onExecuteHandler, onPreviewHandler) {
11 | const onChange = createHandler(onChangeHandler);
12 | const onHover = createHandler(onHoverHandler);
13 | const onSuggest = createHandler(onSuggestHandler);
14 | const onSave = createHandler(onSaveHandler);
15 | const onExecute = createHandler(onExecuteHandler);
16 | const onPreview = createHandler(onPreviewHandler);
17 | const container = document.getElementById(id);
18 | const language = "PowerFx";
19 | const languageExt = "pfx";
20 | const theme = "vs-dark";
21 | const loaderScript = document.createElement("script");
22 | loaderScript.src = "https://www.typescriptlang.org/js/vs.loader.js";
23 | loaderScript.async = true;
24 | loaderScript.onload = () => {
25 | require.config({
26 | paths: {
27 | vs: "https://typescript.azureedge.net/cdn/4.0.5/monaco/min/vs",
28 | sandbox: "https://www.typescriptlang.org/js/sandbox"
29 | },
30 | ignoreDuplicateModules: ["vs/editor/editor.main"]
31 | });
32 | require(["vs/editor/editor.main", "sandbox/index"], async (editorMain, sandboxFactory) => {
33 | monaco.languages.register({
34 | id: language,
35 | aliases: [languageExt],
36 | extensions: [languageExt]
37 | });
38 | const model = monaco.editor.createModel(code, language, monaco.Uri.parse(`file:///index.pfx`));
39 | model.setValue(code);
40 | addMonacoEditorSuggestions(monaco, language, (text, column) => {
41 | const items = [];
42 | const suggested = onSuggest(text);
43 | for (let suggest of suggested) {
44 | items.push({
45 | startColumn: column,
46 | text: suggest.text,
47 | description: suggest.description
48 | });
49 | }
50 | return items;
51 | });
52 | addMonacoEditorHover(monaco, language, (text, column) => onHover(text));
53 | const editor = monaco.editor.create(container, {
54 | model,
55 | language,
56 | theme,
57 | ...defaultMonacoEditorOptions(),
58 | lineNumbers: (lineNumber) => {
59 | const lines = model.getLinesContent();
60 | var line = 0;
61 | for (var i = 0; i < lines.length && i + 1 < lineNumber; i++) {
62 | if (lines[i] && lines[i].trim()) {
63 | line += 1;
64 | }
65 | }
66 | const content = model.getLineContent(lineNumber);
67 | if (content && content.trim()) {
68 | return (line + 1).toString();
69 | }
70 | else {
71 | return "";
72 | }
73 | }
74 | });
75 | _updateMonacoEditor = (code) => editor.setValue(code);
76 | monaco.languages.registerCodeLensProvider(language, new MonacoCodeLensProvider(editor, onChange, onExecute, onPreview));
77 | editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S, () => onSave(editor.getValue()));
78 | window.addEventListener("resize", () => editor.layout());
79 | });
80 | };
81 | document.body.appendChild(loaderScript);
82 | }
83 | function addMonacoEditorHover(monaco, language, onHover) {
84 | monaco.languages.registerHoverProvider(language, {
85 | provideHover: async (model, position) => {
86 | const line = model.getLineContent(position.lineNumber);
87 | const hover = await onHover(line, position.column);
88 | if (hover) {
89 | return {
90 | contents: [{ value: `## ${hover.text}\n${hover.description}` }],
91 | range: {
92 | startLineNumber: position.lineNumber,
93 | endLineNumber: position.lineNumber,
94 | startColumn: hover.startColumn || 1,
95 | endColumn: hover.endColumn || line.length
96 | }
97 | };
98 | }
99 | }
100 | });
101 | }
102 | function addMonacoEditorSuggestions(monaco, language, onSuggest) {
103 | monaco.languages.registerCompletionItemProvider(language, {
104 | provideCompletionItems: async (model, position) => {
105 | const textUntilPosition = model.getValueInRange({
106 | startLineNumber: position.lineNumber,
107 | startColumn: 1,
108 | endLineNumber: position.lineNumber,
109 | endColumn: position.column
110 | });
111 | const lineSuggestions = await onSuggest(textUntilPosition, position.column);
112 | if (lineSuggestions && lineSuggestions.length > 0) {
113 | const word = model.getWordUntilPosition(position);
114 | const suggestions = [];
115 | for (const suggestion of lineSuggestions) {
116 | suggestions.push({
117 | label: suggestion.text,
118 | detail: suggestion.description,
119 | kind: monaco.languages.CompletionItemKind.Value,
120 | insertText: suggestion.text,
121 | range: {
122 | startLineNumber: position.lineNumber,
123 | endLineNumber: position.lineNumber,
124 | startColumn: suggestion.startColumn || word.startColumn,
125 | endColumn: suggestion.endColumn || word.endColumn
126 | }
127 | });
128 | }
129 | return { suggestions };
130 | }
131 | }
132 | });
133 | }
134 | function defaultMonacoEditorOptions() {
135 | return {
136 | lineHeight: 30,
137 | fontSize: 22,
138 | renderLineHighlight: 'all',
139 | wordWrap: 'on',
140 | scrollBeyondLastLine: false,
141 | minimap: {
142 | enabled: false
143 | },
144 | renderValidationDecorations: 'off',
145 | lineDecorationsWidth: 0,
146 | glyphMargin: false,
147 | contextmenu: false,
148 | codeLens: true,
149 | mouseWheelZoom: true,
150 | quickSuggestions: false,
151 | suggest: {
152 | showIssues: false,
153 | shareSuggestSelections: false,
154 | showIcons: false,
155 | showMethods: false,
156 | showFunctions: false,
157 | showVariables: false,
158 | showKeywords: false,
159 | showWords: false,
160 | showClasses: false,
161 | showColors: false,
162 | showConstants: false,
163 | showConstructors: false,
164 | showEnumMembers: false,
165 | showEnums: false,
166 | showEvents: false,
167 | showFields: false,
168 | showFiles: false,
169 | showFolders: false,
170 | showInterfaces: false,
171 | showModules: false,
172 | showOperators: false,
173 | showProperties: false,
174 | showReferences: false,
175 | showSnippets: false,
176 | showStructs: false,
177 | showTypeParameters: false,
178 | showUnits: false,
179 | showValues: true,
180 | filterGraceful: false
181 | }
182 | };
183 | }
184 | class MonacoCodeLensProvider {
185 | constructor(editor, check, execute, preview) {
186 | this.editor = editor;
187 | this.check = check;
188 | this.execute = execute;
189 | this._errorCommand = editor.addCommand(0, function (_, result) {
190 | preview("Error: " + result.text);
191 | }, '');
192 | this._previewCommand = editor.addCommand(1, function (_, result) {
193 | console.log('_previewCommand', arguments);
194 | preview(result.text?.trim());
195 | }, '');
196 | }
197 | resolveCodeLens(model, codeLens) {
198 | return codeLens;
199 | }
200 | async provideCodeLenses(model) {
201 | const lines = model.getLinesContent();
202 | const decorations = [];
203 | const lenses = [];
204 | for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
205 | const lineNum = lineIndex + 1;
206 | const expr = lines[lineIndex];
207 | if (expr) {
208 | const result = await this.check(expr);
209 | console.log("check", { ...result });
210 | if (result.errors) {
211 | const errors = [];
212 | for (var err of result.errors) {
213 | console.log("error", err.description);
214 | decorations.push({
215 | id: `error_${lineNum}_${err.start}_${err.end}`,
216 | range: new monaco.Range(lineNum, err.start + 1, lineNum, err.end + 1),
217 | options: {
218 | inlineClassName: "powerfx-error",
219 | hoverMessage: { value: err.description }
220 | }
221 | });
222 | errors.push(err.description);
223 | }
224 | result.text = errors.join('\n');
225 | lenses.push({
226 | range: new monaco.Range(lineNum, err.start + 1, lineNum, err.end + 1),
227 | id: `preview_${lineNum}_${err.start}_${err.end}`,
228 | command: {
229 | id: this._errorCommand,
230 | title: "Error(s)",
231 | tooltip: result.text,
232 | arguments: [result]
233 | }
234 | });
235 | }
236 | else if (result.type !== "BlankType") {
237 | const output = await this.execute(expr);
238 | console.log("execute", { ...output });
239 | decorations.push({
240 | id: `type_${lineNum}`,
241 | range: new monaco.Range(lineNum, 1, lineNum, 1),
242 | options: {
243 | inlineClassName: "powerfx-type",
244 | hoverMessage: { value: result.type }
245 | }
246 | });
247 | lenses.push({
248 | range: new monaco.Range(lineNum, 1, lineNum, 1),
249 | id: `preview_${lineNum}`,
250 | command: {
251 | id: this._previewCommand,
252 | title: "View Result",
253 | tooltip: result.text,
254 | arguments: [output]
255 | }
256 | });
257 | }
258 | }
259 | }
260 | this._decorations = this.editor.deltaDecorations(this._decorations || [], decorations);
261 | return {
262 | lenses,
263 | dispose: () => { }
264 | };
265 | }
266 | }
267 |
--------------------------------------------------------------------------------
/src/Pages/Index.razor:
--------------------------------------------------------------------------------
1 | @page "/"
2 | @inject IJSInProcessRuntime JSRuntime
3 | @inject PowerFxHost Host
4 | @inject FileManager Files
5 | @inject UrlManager Url
6 | @inject AppPreviewer AppPreview
7 | @using System.Text
8 |
9 | PowerNote
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | PowerNote
19 |
20 |
21 |
22 |
23 |
24 | - Click here to add files
25 | -
26 |
27 | Canvas Apps (.msapp)
28 |
29 | -
30 |
31 | Cloud Flows (.json)
32 |
33 | -
34 |
35 | Excel Documents (.xlsx)
36 |
37 | -
38 |
39 | Dataverse Solutions (.zip)
40 |
41 | @if (!ShowApps)
42 | {
43 | -
44 |
45 | Or press ESC to use the Power Fx editor without a file
46 |
47 |
48 | }
49 |
50 |
Note: Files must be smaller than 512kb at this time.
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
66 |
67 |
68 | @if (ShowApps)
69 | {
70 |
114 | }
115 |
116 |
117 | @if (ShowPreview)
118 | {
119 |
120 | }
121 |
122 |
123 |
124 |
125 |
126 | @code {
127 | private static PowerFxHost _host;
128 | private static Action _showEditor;
129 | private static Action _onSaveCode;
130 | private static Action _showPreview;
131 |
132 | [JSInvokable("SaveMonacoEditorValue")]
133 | public static void SaveCode(string code)
134 | {
135 | _onSaveCode(code);
136 | }
137 |
138 | [JSInvokable("ExecutePowerFxCode")]
139 | public static object ExecuteCode(string code)
140 | {
141 | var result = _host.Execute(code);
142 | return new
143 | {
144 | code = code.Trim(),
145 | name = result.Result.Name,
146 | text = result.Output,
147 | type = result.Result.Value?.Type.GetType().Name ?? "Error"
148 | };
149 | }
150 |
151 | [JSInvokable("CheckPowerFxCode")]
152 | public static object CheckCode(string code)
153 | {
154 | var check = _host.Check(code);
155 | return new
156 | {
157 | code = code.Trim(),
158 | name = check.Name,
159 | text = check.Description,
160 | type = check.Type?.GetType().Name ?? "Error",
161 | errors = check.Errors?.Select(e => new
162 | {
163 | text = e.Text,
164 | description = e.Description,
165 | start = e.Start,
166 | end = e.End
167 | }).ToArray()
168 | };
169 | }
170 |
171 | [JSInvokable("ShowPowerFxCode")]
172 | public static object ShowCode(string code)
173 | {
174 | var suggestion = _host.Show(code);
175 | if (suggestion != null)
176 | {
177 | return new
178 | {
179 | text = suggestion.Text,
180 | description = suggestion.Description,
181 | startColumn = suggestion.Start,
182 | endColumn = suggestion.End
183 | };
184 | }
185 | else
186 | {
187 | return null;
188 | }
189 | }
190 |
191 | [JSInvokable("SuggestPowerFxCode")]
192 | public static object[] SuggestCode(string code)
193 | {
194 | return _host.Suggest(code)?.Select(s => new
195 | {
196 | text = s.Text,
197 | description = s.Description,
198 | startColumn = s.Start,
199 | endColumn = s.End
200 | }).ToArray();
201 | }
202 |
203 | [JSInvokable("ShowPowerFxEditor")]
204 | public static void ShowEditor()
205 | {
206 | _showEditor();
207 | }
208 |
209 | [JSInvokable("PreviewPowerFxCode")]
210 | public static void PreviewCode(string output)
211 | {
212 | _showPreview(output);
213 | }
214 |
215 | public PowerApp[] Apps;
216 | public PowerAppComponent[] Components;
217 | public PowerAppComponentObject[] Objects;
218 | public PowerApp SelectedApp;
219 | public PowerAppComponent SelectedAppComponent;
220 | public PowerAppComponentObject SelectedObject;
221 |
222 | private string Code;
223 | private string Output;
224 | private bool ShowApps;
225 | private bool ShowPreview;
226 | private string FileDropStyle;
227 |
228 | public void showEditor()
229 | {
230 | if (Apps == null)
231 | {
232 | Code = PowerFxSample.Code;
233 | FileDropStyle = "files top-right";
234 | StateHasChanged();
235 | }
236 | }
237 |
238 | public void OnFilesAdded()
239 | {
240 | Files.ReadAppsFolder();
241 | Apps = Files.GetPowerApps();
242 | ShowApps = Apps.Any();
243 | if (Apps.Length == 1)
244 | {
245 | SelectedApp = Apps.First();
246 | Components = SelectedApp.Components;
247 | }
248 | FileDropStyle = "files top-right";
249 | StateHasChanged();
250 | }
251 |
252 | private void onSaveCode(string code)
253 | {
254 | var bytes = System.Text.Encoding.UTF8.GetBytes(code);
255 | Url.SetUrl(UrlManager.CreateUrl(bytes));
256 | }
257 |
258 | private void onSelectApp(PowerApp app)
259 | {
260 | SelectedApp = app;
261 | Components = app.Components;
262 | StateHasChanged();
263 | Url.SetUrl(Files.GetAppUrl(app));
264 | }
265 |
266 | private void onSelectAppComponent(PowerAppComponent component)
267 | {
268 | SelectedAppComponent = component;
269 | Objects = component.Objects;
270 | StateHasChanged();
271 | }
272 |
273 | private void onSelectObject(PowerAppComponentObject obj)
274 | {
275 | SelectedObject = obj;
276 | Code = AppPreview.GetObjectTable(SelectedAppComponent, obj);
277 | StateHasChanged();
278 | }
279 |
280 | private void showComponentPreview()
281 | {
282 | var preview = AppPreview.GetAppComponentPreview(SelectedApp, SelectedAppComponent);
283 | Code = preview.Code;
284 | Output = preview.Html;
285 | ShowPreview = true;
286 | StateHasChanged();
287 | }
288 |
289 | private void showObjectPreview()
290 | {
291 | if (SelectedObject != null)
292 | {
293 | Output = AppPreview.GetObjectPreview(SelectedAppComponent, SelectedObject);
294 | ShowPreview = true;
295 | StateHasChanged();
296 | }
297 | }
298 |
299 | public void showPreview(string output)
300 | {
301 | Console.WriteLine("showPreview: " + output);
302 | Output = output;
303 | ShowPreview = true;
304 | StateHasChanged();
305 | }
306 |
307 | private void hideObjectPreview()
308 | {
309 | ShowPreview = false;
310 | StateHasChanged();
311 | }
312 |
313 | private void clearSelectedApp()
314 | {
315 | SelectedApp = null;
316 | SelectedAppComponent = null;
317 | Components = null;
318 | Objects = null;
319 | StateHasChanged();
320 | }
321 |
322 | private void clearSelectedComponent()
323 | {
324 | SelectedAppComponent = null;
325 | Objects = null;
326 | StateHasChanged();
327 | }
328 |
329 | protected override void OnInitialized()
330 | {
331 | _host = Host;
332 | _showEditor = showEditor;
333 | _showPreview = showPreview;
334 | _onSaveCode = onSaveCode;
335 |
336 | FileDropStyle = "files full-screen";
337 |
338 | Url.ReadUrl(OnFilesAdded);
339 |
340 | Code = Files.GetLoadedCode();
341 |
342 | JSRuntime.InvokeVoid("eval", @"window.onkeyup = function(e) {
343 | if (e.key === 'Escape') {
344 | DotNet.invokeMethod('PowerNote', 'ShowPowerFxEditor')
345 | window.onkeyup = null
346 | }
347 | }");
348 | }
349 | }
--------------------------------------------------------------------------------
/src/Services/PowerFxHost.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.PowerFx;
2 | using Microsoft.PowerFx.Core.Errors;
3 | using Microsoft.PowerFx.Core.Public;
4 | using Microsoft.PowerFx.Core.Public.Types;
5 | using Microsoft.PowerFx.Core.Public.Values;
6 | using PowerNote.Models;
7 | using System.Diagnostics;
8 | using System.Text;
9 | using System.Text.RegularExpressions;
10 |
11 | namespace PowerNote.Services;
12 |
13 | public record PowerFxError(int Begin, int End, ErrorKind Kind, DocumentErrorSeverity Severity, string Message, string Name, FormulaValue Value) : PowerFxResult(Name, Message, Value);
14 |
15 | public record PowerFxResult(string Name, string FormattedText, FormulaValue Value);
16 | public record PowerFxCheck(string Name, string Description, FormulaType Type, PowerFxSuggestion[] Errors);
17 | public record PowerFxSuggestion(string Text, string Description, int Start, int End);
18 |
19 | public class PowerFxHost
20 | {
21 | private RecalcEngine _engine;
22 | private readonly FormulaJsHost _formulaJsHost;
23 |
24 | private readonly HashSet _formulas = new();
25 | private readonly HashSet _variables = new();
26 |
27 | public PowerFxHost(FormulaJsHost formulaJsHost)
28 | {
29 | _formulaJsHost = formulaJsHost;
30 | _engine = new();
31 | _formulaJsHost.AddFunctions(_engine);
32 | }
33 |
34 | public PowerFxSuggestion Show(string expr)
35 | {
36 | Console.WriteLine($"Show: {expr}");
37 | var functions = _engine.GetAllFunctionNames().ToArray();
38 | foreach (var function in functions)
39 | {
40 | var functionIndex = expr.IndexOf($"{function}(");
41 | if (functionIndex > -1 && PowerFxGrammar.FunctionDescriptions.ContainsKey(function))
42 | {
43 | return new(function, PowerFxGrammar.FunctionDescriptions[function], functionIndex, functionIndex + function.Length);
44 | }
45 | }
46 | return null;
47 | }
48 |
49 | public IEnumerable Suggest(string expr)
50 | {
51 | Console.WriteLine($"Suggest: {expr}");
52 | var result = _engine.Suggest(expr, null, expr.Length);
53 | foreach (var suggestion in result.Suggestions)
54 | {
55 | if (suggestion.Overloads.Any())
56 | {
57 | foreach(var overload in suggestion.Overloads)
58 | {
59 | yield return new(overload.DisplayText.Text, overload.Definition, overload.DisplayText.HighlightStart, overload.DisplayText.HighlightEnd);
60 | }
61 | }
62 | else
63 | {
64 | yield return new(suggestion.DisplayText.Text, suggestion.Definition, suggestion.DisplayText.HighlightStart, suggestion.DisplayText.HighlightEnd);
65 | }
66 | }
67 | var functions = _engine.GetAllFunctionNames().ToArray();
68 | foreach (var function in functions)
69 | {
70 | if (string.IsNullOrEmpty(expr))
71 | {
72 | yield return new(function, PowerFxGrammar.FunctionDescriptions[function], 1, 1);
73 | }
74 | else
75 | {
76 | var functionIndex = expr.IndexOf($"{function}(");
77 | if (functionIndex > -1 && PowerFxGrammar.FunctionDescriptions.ContainsKey(function))
78 | {
79 | yield return new(function, PowerFxGrammar.FunctionDescriptions[function], functionIndex, functionIndex + function.Length);
80 | }
81 | }
82 | }
83 | }
84 |
85 | public PowerFxCheck Check(string expr)
86 | {
87 | Debug.WriteLine($"Check: {expr}");
88 | try
89 | {
90 | var result = checkExpression(expr);
91 | if (result.check != null) {
92 | if (result.check.IsSuccess)
93 | {
94 | if (result.isAssignment)
95 | {
96 | return new(result.name, "Assignment", result.check.ReturnType, null);
97 | }
98 | else if (result.isFormula)
99 | {
100 | if (!_formulas.Contains(result.name))
101 | {
102 | _engine.SetFormula(result.name, result.text,
103 | (name, value) =>
104 | {
105 | Console.WriteLine($"PowerFxCheck {name}: {value.ToObject()}");
106 | }
107 | );
108 | _formulas.Add(result.name);
109 | }
110 | return new(result.name, "Formula", result.check.ReturnType, null);
111 | }
112 | else if (result.isExpression)
113 | {
114 | return new(result.name, "Expression", result.check.ReturnType, null);
115 | }
116 | }
117 | else
118 | {
119 | return new(result.check.ReturnType?.GetType().Name ?? "Error", "", result.check.ReturnType, result.check.Errors.Select(e => new PowerFxSuggestion(e.Kind.ToString(), e.Message, e.Span.Min, e.Span.Lim)).ToArray());
120 | }
121 | }
122 | return new(result.name, result.text, null, null);
123 | }
124 | catch (Exception ex)
125 | {
126 | return new("Error", "", null, new[] { new PowerFxSuggestion(ex.GetType().Name, ex.Message, 1, 1) });
127 | }
128 | }
129 |
130 | public (PowerFxResult Result, string Output) Execute(string expr)
131 | {
132 | Debug.WriteLine($"Execute: {expr}");
133 | var output = new StringBuilder();
134 | var addLine = (string line) => { output.AppendLine(line); return line; };
135 | try
136 | {
137 | var check = checkExpression(expr);
138 | if (check.check.IsSuccess)
139 | {
140 | if (check.isAssignment)
141 | {
142 | if (!_variables.Contains(check.name))
143 | {
144 | _variables.Add(check.name);
145 | }
146 | var value = _engine.Eval(check.text);
147 | _engine.UpdateVariable(check.name, value);
148 | var result = createResult(check.name, value);
149 | addLine($"{check.name} -> {result.FormattedText}");
150 | return (result, output.ToString());
151 | }
152 | else if (check.isFormula)
153 | {
154 | if (!_formulas.Contains(check.name))
155 | {
156 | _engine.SetFormula(check.name, check.text,
157 | (name, value) => addLine(createResult(name, value).FormattedText)
158 | );
159 | _formulas.Add(check.name);
160 | }
161 | var value = _engine.GetValue(check.name);
162 | var result = createResult(check.name, value);
163 | addLine($"{check.name} = {result.FormattedText}");
164 | return (result, output.ToString());
165 | }
166 | else if (check.isExpression)
167 | {
168 | var value = _engine.Eval(check.text);
169 | var result = createResult(check.name, value);
170 | addLine(result.FormattedText);
171 | return (result, output.ToString());
172 | }
173 | else if (!string.IsNullOrWhiteSpace(expr))
174 | {
175 | return (createError("Not Recognized"), output.ToString());
176 | }
177 | else
178 | {
179 | return (createError("Empty"), output.ToString());
180 | }
181 | }
182 | else
183 | {
184 | var errors = check.check.Errors.Select(e => new PowerFxSuggestion(e.Kind.ToString(), e.Message, e.Span.Min, e.Span.Lim)).ToArray();
185 | return (createError(string.Join("\n", errors.Select(e => e.ToString()))), check.text);
186 | }
187 | }
188 | catch (Exception ex)
189 | {
190 | return (createError(ex.ToString()), output.ToString());
191 | }
192 | }
193 |
194 | private (bool isAssignment, bool isFormula, bool isExpression, string name, string text, CheckResult check) checkExpression(string expr)
195 | {
196 | Match match;
197 | // variable assignment: Set( , )
198 | if ((match = Regex.Match(expr, @"^\s*Set\(\s*(?\w+)\s*,\s*(?.*)\)\s*$")).Success)
199 | {
200 | var name = match.Groups["ident"].Value;
201 | var text = match.Groups["expr"].Value;
202 | return (true, false, false, name, text, _engine.Check(text));
203 | }
204 | // formula definition: =
205 | else if ((match = Regex.Match(expr, @"^\s*(?\w+)\s*=(?.*)$")).Success)
206 | {
207 | var name = match.Groups["ident"].Value;
208 | var text = match.Groups["formula"].Value;
209 | return (false, true, false, name, text, _engine.Check(text));
210 | }
211 | // everything except single line comments
212 | else if (!Regex.IsMatch(expr, @"^\s*//") && Regex.IsMatch(expr, @"\w"))
213 | {
214 | return (false, false, true, "", expr, _engine.Check(expr));
215 | }
216 | else
217 | {
218 | return (false, false, false, "", expr, null);
219 | }
220 | }
221 |
222 | private PowerFxError createError(string name, ErrorValue errorValue)
223 | {
224 | var error = errorValue.Errors[0];
225 | return new(error.Span?.Min ?? 1, error.Span?.Lim ?? 1, error.Kind, error.Severity, error.Message, name, errorValue);
226 | }
227 |
228 | private PowerFxResult createError(string message)
229 | {
230 | return createResult("Error", FormulaValue.NewError(new ExpressionError
231 | {
232 | Kind = ErrorKind.Unknown,
233 | Severity = DocumentErrorSeverity.Warning,
234 | Message = message
235 | }));
236 | }
237 |
238 | private PowerFxResult createResult(string name, FormulaValue result)
239 | {
240 | return result switch
241 | {
242 | ErrorValue errorValue => createError(name, errorValue),
243 | _ => new PowerFxResult(name, PrintResult(result), result)
244 | };
245 | }
246 |
247 | private string PrintResult(object value)
248 | {
249 | string resultString = "";
250 |
251 | if (value is RecordValue record)
252 | {
253 | var separator = "";
254 | resultString = "{";
255 | foreach (var field in record.Fields)
256 | {
257 | resultString += separator + $"{field.Name}:";
258 | resultString += PrintResult(field.Value);
259 | separator = ", ";
260 | }
261 | resultString += "}";
262 | }
263 | else if (value is TableValue table)
264 | {
265 | int valueSeen = 0, recordsSeen = 0;
266 | string separator = "";
267 |
268 | // check if the table can be represented in simpler [ ] notation,
269 | // where each element is a record with a field named Value.
270 | foreach (var row in table.Rows)
271 | {
272 | recordsSeen++;
273 | if (row.Value is RecordValue scanRecord)
274 | {
275 | foreach (var field in scanRecord.Fields)
276 | if (field.Name == "Value")
277 | {
278 | valueSeen++;
279 | resultString += separator + PrintResult(field.Value);
280 | separator = ", ";
281 | }
282 | else
283 | valueSeen = 0;
284 | }
285 | else
286 | valueSeen = 0;
287 | }
288 |
289 | if (valueSeen == recordsSeen)
290 | return ("[" + resultString + "]");
291 | else
292 | {
293 | // no, table is more complex that a single column of Value fields,
294 | // requires full treatment
295 | resultString = "Table(";
296 | separator = "";
297 | foreach (var row in table.Rows)
298 | {
299 | resultString += separator + PrintResult(row.Value);
300 | separator = ", ";
301 | }
302 | resultString += ")";
303 | }
304 | }
305 | else if (value is ErrorValue errorValue)
306 | resultString = "";
307 | else if (value is StringValue str)
308 | resultString = "\"" + str.ToObject().ToString().Replace("\"", "\"\"") + "\"";
309 | else if (value is FormulaValue fv)
310 | resultString = fv.ToObject().ToString();
311 | else
312 | throw new Exception("unexpected type in PrintResult");
313 |
314 | return (resultString);
315 | }
316 | }
--------------------------------------------------------------------------------
/src/Models/PowerFxGrammar.cs:
--------------------------------------------------------------------------------
1 | namespace PowerNote.Models;
2 |
3 | internal static class PowerFxGrammar
4 | {
5 | public static Dictionary FunctionDescriptions = new()
6 | {
7 | { "Abs", "Absolute value of a number." },
8 | { "Acceleration", "Reads the acceleration sensor in your device." },
9 | { "Acos", "Returns the arccosine of a number, in radians." },
10 | { "Acot", "Returns the arccotangent of a number, in radians." },
11 | { "AddColumns", "Returns a table with columns added." },
12 | { "And", "Boolean logic AND. Returns true if all arguments are true. You can also use the && operator." },
13 | { "App", "Provides information about the currently running app and control over the app's behavior." },
14 | { "Asin", "Returns the arcsine of a number, in radians." },
15 | { "Assert", "Evaluates to true or false in a test." },
16 | { "As", "Names the current record in gallery, form, and record scope functions such as ForAll, With, and Sum." },
17 | { "AsType", "Treats a record reference as a specific table type." },
18 | { "Atan", "Returns the arctangent of a number, in radians." },
19 | { "Atan2", "Returns the arctangent based on an (x,y) coordinate, in radians." },
20 | { "Average", "Calculates the average of a table expression or a set of arguments." },
21 | { "Back", "Displays the previous screen." },
22 | { "Blank", "Returns a blank value that can be used to insert a NULL value in a data source." },
23 | { "Calendar", "Retrieves information about the calendar for the current locale." },
24 | { "Char", "Translates a character code into a string." },
25 | { "Choices", "Returns a table of the possible values for a lookup column." },
26 | { "Clear", "Deletes all data from a collection." },
27 | { "ClearCollect", "Deletes all data from a collection and then adds a set of records." },
28 | { "ClearData", "Clears a collection or all collections from an app host such as a local device." },
29 | { "Clock", "Retrieves information about the clock for the current locale." },
30 | { "Coalesce", "Replaces blank values while leaving non" },
31 | { "Collect", "Creates a collection or adds data to a data source." },
32 | { "Color", "Sets a property to a built" },
33 | { "ColorFade", "Fades a color value." },
34 | { "ColorValue", "Translates a CSS color name or a hex code to a color value." },
35 | { "Compass", "Returns your compass heading." },
36 | { "Concat", "Concatenates strings in a data source." },
37 | { "Concatenate", "Concatenates strings." },
38 | { "Concurrent", "Evaluates multiple formulas concurrently with one another." },
39 | { "Connection", "Returns information about your network connection." },
40 | { "Count", "Counts table records that contain numbers." },
41 | { "Cos", "Returns the cosine of an angle specified in radians." },
42 | { "Cot", "Returns the cotangent of an angle specified in radians." },
43 | { "CountA", "Counts table records that aren't empty." },
44 | { "CountIf", "Counts table records that satisfy a condition." },
45 | { "CountRows", "Counts table records." },
46 | { "DataSourceInfo", "Provides information about a data source." },
47 | { "Date", "Returns a date/time value, based on Year, Month, and Day values." },
48 | { "DateAdd", "Adds days, months, quarters, or years to a date/time value." },
49 | { "DateDiff", "Subtracts two date values, and shows the result in days, months, quarters, or years." },
50 | { "DateTimeValue", "Converts a date and time string to a date/time value." },
51 | { "DateValue", "Converts a date" },
52 | { "Day", "Retrieves the day portion of a date/time value." },
53 | { "Defaults", "Returns the default values for a data source." },
54 | { "Degrees", "Converts radians to degrees." },
55 | { "DisableLocation", "Disables a signal, such as Location for reading the GPS." },
56 | { "Distinct", "Summarizes records of a table, removing duplicates." },
57 | { "Download", "Downloads a file from the web to the local device." },
58 | { "DropColumns", "Returns a table with one or more columns removed." },
59 | { "EditForm", "Resets a form control for editing of an item." },
60 | { "EnableLocation", "Enables a signal, such as Location for reading the GPS." },
61 | { "EncodeUrl", "Encodes special characters using URL encoding." },
62 | { "EndsWith", "Checks whether a text string ends with another text string." },
63 | { "Errors", "Provides error information for previous changes to a data source." },
64 | { "exactin", "Checks if a text string is contained within another text string or table, case dependent. Also used to check if a record is in a table." },
65 | { "Exit", "Exits the currently running app and optionally signs out the current user." },
66 | { "Exp", "Returns e raised to a power." },
67 | { "Filter", "Returns a filtered table based on one or more criteria." },
68 | { "Find", "Checks whether one string appears within another and returns the location." },
69 | { "First", "Returns the first record of a table." },
70 | { "FirstN", "Returns the first set of records (N records) of a table." },
71 | { "ForAll", "Calculates values and performs actions for all records of a table." },
72 | { "GroupBy", "Returns a table with records grouped together." },
73 | { "GUID", "Converts a GUID string to a GUID value or creates a new GUID value." },
74 | { "HashTags", "Extracts the hashtags (#strings) from a string." },
75 | { "Hour", "Returns the hour portion of a date/time value." },
76 | { "If", "Returns one value if a condition is true and another value if not." },
77 | { "IfError", "Detects errors and provides an alternative value or takes action." },
78 | { "in", "Checks if a text string is contained within another text string or table, case independent. Also used to check if a record is in a table." },
79 | { "Int", "Rounds down to the nearest integer." },
80 | { "IsBlank", "Checks for a blank value." },
81 | { "IsBlankOrError", "Checks for a blank value or error." },
82 | { "IsEmpty", "Checks for an empty table." },
83 | { "IsError", "Checks for an error." },
84 | { "IsMatch", "Checks a string against a pattern. Regular expressions can be used." },
85 | { "IsNumeric", "Checks for a numeric value." },
86 | { "ISOWeekNum", "Returns the ISO week number of a date/time value." },
87 | { "IsToday", "Checks whether a date/time value is sometime today." },
88 | { "IsType", "Checks whether a record reference refers to a specific table type." },
89 | { "JSON", "Generates a JSON text string for a table, a record, or a value." },
90 | { "Language", "Returns the language tag of the current user." },
91 | { "Last", "Returns the last record of a table." },
92 | { "LastN", "Returns the last set of records (N records) of a table." },
93 | { "Launch", "Launches a webpage or a canvas app." },
94 | { "Left", "Returns the left" },
95 | { "Len", "Returns the length of a string." },
96 | { "Ln", "Returns the natural log." },
97 | { "LoadData", "Loads a collection from an app host such as a local device." },
98 | { "Location", "Returns your location as a map coordinate by using the Global Positioning System (GPS) and other information." },
99 | { "LookUp", "Looks up a single record in a table based on one or more criteria." },
100 | { "Lower", "Converts letters in a string of text to all lowercase." },
101 | { "Match", "Extracts a substring based on a pattern. Regular expressions can be used." },
102 | { "MatchAll", "Extracts multiple substrings based on a pattern. Regular expressions can be used." },
103 | { "Max", "Maximum value of a table expression or a set of arguments." },
104 | { "Mid", "Returns the middle portion of a string." },
105 | { "Min", "Minimum value of a table expression or a set of arguments." },
106 | { "Minute", "Retrieves the minute portion of a date/time value." },
107 | { "Mod", "Returns the remainder after a dividend is divided by a divisor." },
108 | { "Month", "Retrieves the month portion of a date/time value." },
109 | { "Navigate", "Changes which screen is displayed." },
110 | { "NewForm", "Resets a form control for creation of an item." },
111 | { "Not", "Boolean logic NOT. Returns true if its argument is false, and returns false if its argument is true. You can also use the ! operator." },
112 | { "Notify", "Displays a banner message to the user." },
113 | { "Now", "Returns the current date/time value." },
114 | { "Or", "Boolean logic OR. Returns true if any of its arguments are true. You can also use the || operator." },
115 | { "Param", "Access parameters passed to a canvas app when launched." },
116 | { "Parent", "Provides access to a container control's properties." },
117 | { "Patch", "Modifies or creates a record in a data source, or merges records outside of a data source." },
118 | { "Pi", "Returns the number π." },
119 | { "PlainText", "Removes HTML and XML tags from a string." },
120 | { "Power", "Returns a number raised to a power. You can also use the ^ operator." },
121 | { "Proper", "Converts the first letter of each word in a string to uppercase, and converts the rest to lowercase." },
122 | { "Radians", "Converts degrees to radians." },
123 | { "Rand", "Returns a pseudo" },
124 | { "ReadNFC", "Reads a Near Field Communication (NFC) tag." },
125 | { "RecordInfo", "Provides information about a record of a data source." },
126 | { "Refresh", "Refreshes the records of a data source." },
127 | { "Relate", "Relates records of two tables through a one" },
128 | { "Remove", "Removes one or more specific records from a data source." },
129 | { "RemoveIf", "Removes records from a data source based on a condition." },
130 | { "RenameColumns", "Renames columns of a table." },
131 | { "Replace", "Replaces part of a string with another string, by starting position of the string." },
132 | { "RequestHide", "Hides a SharePoint form." },
133 | { "Reset", "Resets an input control to its default value, discarding any user changes." },
134 | { "ResetForm", "Resets a form control for editing of an existing item." },
135 | { "Revert", "Reloads and clears errors for the records of a data source." },
136 | { "RGBA", "Returns a color value for a set of red, green, blue, and alpha components." },
137 | { "Right", "Returns the right" },
138 | { "Round", "Rounds to the closest number." },
139 | { "RoundDown", "Rounds down to the largest previous number." },
140 | { "RoundUp", "Rounds up to the smallest next number." },
141 | { "SaveData", "Saves a collection to an app host such as a local device." },
142 | { "Search", "Finds records in a table that contain a string in one of their columns." },
143 | { "Second", "Retrieves the second portion of a date/time value." },
144 | { "Select", "Simulates a select action on a control, causing the OnSelect formula to be evaluated." },
145 | { "Self", "Provides access to the properties of the current control." },
146 | { "Sequence", "Generate a table of sequential numbers, useful when iterating with ForAll." },
147 | { "Set", "Sets the value of a global variable." },
148 | { "SetFocus", "Moves input focus to a specific control." },
149 | { "SetProperty", "Simulates interactions with input controls." },
150 | { "ShowColumns", "Returns a table with only selected columns." },
151 | { "Shuffle", "Randomly reorders the records of a table." },
152 | { "Sin", "Returns the sine of an angle specified in radians." },
153 | { "Sort", "Returns a sorted table based on a formula." },
154 | { "SortByColumns", "Returns a sorted table based on one or more columns." },
155 | { "Split", "Splits a text string into a table of substrings." },
156 | { "Sqrt", "Returns the square root of a number." },
157 | { "StartsWith", "Checks if a text string begins with another text string." },
158 | { "StdevP", "Returns the standard deviation of its arguments." },
159 | { "Substitute", "Replaces part of a string with another string, by matching strings." },
160 | { "SubmitForm", "Saves the item in a form control to the data source." },
161 | { "Sum", "Calculates the sum of a table expression or a set of arguments." },
162 | { "Switch", "Matches with a set of values and then evaluates a corresponding formula." },
163 | { "Table", "Creates a temporary table." },
164 | { "Tan", "Returns the tangent of an angle specified in radians." },
165 | { "Text", "Converts any value and formats a number or date/time value to a string of text." },
166 | { "ThisItem", "Returns the record for the current item in a gallery or form control." },
167 | { "ThisRecord", "Returns the record for the current item in a record scope function, such as ForAll, With, and Sum." },
168 | { "Time", "Returns a date/time value, based on Hour, Minute, and Second values." },
169 | { "TimeValue", "Converts a time" },
170 | { "TimeZoneOffset", "Returns the difference between UTC and the user's local time in minutes." },
171 | { "Today", "Returns the current date/time value." },
172 | { "Trace", "Provide additional information in your test results." },
173 | { "Trim", "Removes extra spaces from the ends and interior of a string of text." },
174 | { "TrimEnds", "Removes extra spaces from the ends of a string of text only." },
175 | { "Trunc", "Truncates the number to only the integer portion by removing any decimal portion." },
176 | { "Ungroup", "Removes a grouping." },
177 | { "Unrelate", "Unrelates records of two tables from a one" },
178 | { "Update", "Replaces a record in a data source." },
179 | { "UpdateContext", "Sets the value of one or more context variables of the current screen." },
180 | { "UpdateIf", "Modifies a set of records in a data source based on a condition." },
181 | { "Upper", "Converts letters in a string of text to all uppercase." },
182 | { "User", "Returns information about the current user." },
183 | { "Validate", "Checks whether the value of a single column or a complete record is valid for a data source." },
184 | { "Value", "Converts a string to a number." },
185 | { "VarP", "Returns the variance of its arguments." },
186 | { "ViewForm", "Resets a form control for viewing of an existing item." },
187 | { "Weekday", "Retrieves the weekday portion of a date/time value." },
188 | { "WeekNum", "Returns the week number of a date/time value." },
189 | { "With", "Calculates values and performs actions for a single record, including inline records of named values." },
190 | { "Year", "Retrieves the year portion of a date/time value." },
191 | };
192 |
193 | }
--------------------------------------------------------------------------------
/src/Modules/MonacoEditor.ts:
--------------------------------------------------------------------------------
1 | declare var require
2 |
3 | declare namespace monaco {
4 | export var KeyMod
5 | export var KeyCode
6 | export var Uri
7 | export var languages
8 | export namespace editor {
9 | export interface ICommandHandler {
10 | (...args: any[]): void;
11 | }
12 | export interface IDisposable {
13 | dispose(): void;
14 | }
15 | export interface IEvent {
16 | (listener: (e: T) => any, thisArg?: any): IDisposable
17 | }
18 | export interface Command {
19 | id: string
20 | title: string
21 | tooltip?: string
22 | arguments?: any[]
23 | }
24 | export interface CodeLens {
25 | range: IRange
26 | id?: string
27 | command?: Command
28 | }
29 | export interface CodeLensList {
30 | lenses: CodeLens[]
31 | dispose(): void
32 | }
33 | export interface CodeLensProvider {
34 | onDidChange?: IEvent
35 | provideCodeLenses(model: editor.ITextModel): Promise | CodeLensList
36 | resolveCodeLens?(model: editor.ITextModel, codeLens: CodeLens): Promise | CodeLens
37 | }
38 | export interface IViewZone {
39 | heightInLines: number
40 | heightInPx?: number
41 | afterLineNumber: number
42 | domNode: HTMLElement
43 | marginDomNode: HTMLElement
44 | suppressMouseDown?: boolean
45 | }
46 | export interface IViewZoneChangeAccessor {
47 | layoutZone(zone: string)
48 | addZone(view: IViewZone)
49 | removeZone(arg0: string)
50 | }
51 | export interface ITextModel {
52 | getLinesContent()
53 | }
54 | export interface IModelDeltaDecoration {
55 | id: string
56 | range: IRange
57 | options: {
58 | hoverMessage?: { value: string },
59 | isWholeLine?: boolean
60 | className?: string
61 | linesDecorationsClassName?: string
62 | inlineClassName?: string
63 | }
64 | }
65 | export interface IStandaloneCodeEditor {
66 | onDidChangeModelContent(arg0: (e: any) => Promise)
67 | getLineDecorations(lineNumber: number): IModelDeltaDecoration[] | null
68 | deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[]
69 | addCommand(keybinding: number, handler: ICommandHandler, context?: string): string | null
70 | }
71 | export interface IStandaloneEditorConstructionOptions { }
72 | export interface IModelContentChange {
73 | range: any
74 | }
75 | export function createModel(language: string, text: string, uri: any)
76 | export function create(container: HTMLElement, options: IStandaloneEditorConstructionOptions)
77 | }
78 | export interface IRange {
79 | startLineNumber: number
80 | startColumn: number
81 | endLineNumber: number
82 | endColumn: number
83 | }
84 | export class Range implements IRange {
85 | public startLineNumber: number
86 | public startColumn: number
87 | public endLineNumber: number
88 | public endColumn: number
89 | constructor(
90 | startLine: number,
91 | startColumn: number,
92 | endLine: number,
93 | endColumn: number
94 | )
95 | }
96 | }
97 |
98 | interface CodeExecutionResult {
99 | code: string
100 | name: string
101 | text: string
102 | type: 'BlankType' | 'BooleanType' | 'NumberType' | 'StringType' | 'TimeType' | 'DateType' | 'DateTimeType' | 'DateTimeNoTimeZoneType' | 'OptionSetValueType' | 'Error'
103 | errors: {
104 | text: string
105 | description: string
106 | start: number
107 | end: number
108 | }[]
109 | }
110 |
111 | interface CodeSuggestionResult {
112 | text: string
113 | description: string
114 | startColumn?: number
115 | endColumn?: number
116 | }
117 |
118 | function createHandler(text: string): (value: A) => T {
119 | return new Function("value", `return ${text}`) as (value: A) => T
120 | }
121 |
122 | var _updateMonacoEditor: (code: string) => void
123 | export function updateMonacoEditor(code: string) {
124 | if (typeof (_updateMonacoEditor) === 'function') {
125 | _updateMonacoEditor(code)
126 | }
127 | }
128 |
129 | export function loadMonacoEditor(id: string, code: string, onChangeHandler: string, onHoverHandler: string, onSuggestHandler: string, onSaveHandler: string, onExecuteHandler: string, onPreviewHandler: string) {
130 | const onChange = createHandler(onChangeHandler)
131 | const onHover = createHandler(onHoverHandler)
132 | const onSuggest = createHandler(onSuggestHandler)
133 | const onSave = createHandler(onSaveHandler)
134 | const onExecute = createHandler(onExecuteHandler)
135 | const onPreview = createHandler(onPreviewHandler)
136 | const container = document.getElementById(id)
137 | const language = "PowerFx"
138 | const languageExt = "pfx"
139 | const theme = "vs-dark"
140 | const loaderScript = document.createElement("script")
141 | loaderScript.src = "https://www.typescriptlang.org/js/vs.loader.js"
142 | loaderScript.async = true
143 | loaderScript.onload = () => {
144 | require.config({
145 | paths: {
146 | vs: "https://typescript.azureedge.net/cdn/4.0.5/monaco/min/vs",
147 | sandbox: "https://www.typescriptlang.org/js/sandbox"
148 | },
149 | ignoreDuplicateModules: ["vs/editor/editor.main"]
150 | })
151 | require(["vs/editor/editor.main", "sandbox/index"], async (editorMain, sandboxFactory) => {
152 | monaco.languages.register({
153 | id: language,
154 | aliases: [languageExt],
155 | extensions: [languageExt]
156 | })
157 |
158 | const model = monaco.editor.createModel(code, language, monaco.Uri.parse(`file:///index.pfx`))
159 | model.setValue(code)
160 |
161 | addMonacoEditorSuggestions(monaco, language, (text, column) => {
162 | const items = []
163 | const suggested = onSuggest(text)
164 | for (let suggest of suggested) {
165 | items.push({
166 | startColumn: column,
167 | text: suggest.text,
168 | description: suggest.description
169 | })
170 | }
171 | return items
172 | })
173 |
174 | addMonacoEditorHover(monaco, language, (text, column) => onHover(text))
175 |
176 | const editor = monaco.editor.create(container, {
177 | model,
178 | language,
179 | theme,
180 | ...defaultMonacoEditorOptions(),
181 | lineNumbers: (lineNumber) => {
182 | const lines = model.getLinesContent()
183 | var line = 0;
184 | for (var i = 0; i < lines.length && i + 1 < lineNumber; i++) {
185 | if (lines[i] && lines[i].trim()) {
186 | line += 1;
187 | }
188 | }
189 | const content = model.getLineContent(lineNumber)
190 | if (content && content.trim()) {
191 | return (line + 1).toString()
192 | } else {
193 | return ""
194 | }
195 | }
196 | })
197 |
198 | _updateMonacoEditor = (code) => editor.setValue(code)
199 |
200 | monaco.languages.registerCodeLensProvider(language, new MonacoCodeLensProvider(editor, onChange, onExecute, onPreview));
201 |
202 | editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S, () => onSave(editor.getValue()))
203 |
204 | window.addEventListener("resize", () => editor.layout())
205 | })
206 | }
207 | document.body.appendChild(loaderScript)
208 | }
209 |
210 | function addMonacoEditorHover(monaco, language, onHover) {
211 | monaco.languages.registerHoverProvider(language, {
212 | provideHover: async (model, position) => {
213 | const line = model.getLineContent(position.lineNumber)
214 | const hover = await onHover(line, position.column)
215 | if (hover) {
216 | return {
217 | contents: [{ value: `## ${hover.text}\n${hover.description}` }],
218 | range: {
219 | startLineNumber: position.lineNumber,
220 | endLineNumber: position.lineNumber,
221 | startColumn: hover.startColumn || 1,
222 | endColumn: hover.endColumn || line.length
223 | }
224 | }
225 | }
226 | }
227 | })
228 | }
229 |
230 | function addMonacoEditorSuggestions(monaco, language, onSuggest) {
231 | monaco.languages.registerCompletionItemProvider(language, {
232 | provideCompletionItems: async (model, position) => {
233 | const textUntilPosition = model.getValueInRange({
234 | startLineNumber: position.lineNumber,
235 | startColumn: 1,
236 | endLineNumber: position.lineNumber,
237 | endColumn: position.column
238 | })
239 | const lineSuggestions = await onSuggest(textUntilPosition, position.column)
240 | if (lineSuggestions && lineSuggestions.length > 0) {
241 | const word = model.getWordUntilPosition(position)
242 | const suggestions = []
243 | for (const suggestion of lineSuggestions) {
244 | suggestions.push({
245 | label: suggestion.text,
246 | detail: suggestion.description,
247 | kind: monaco.languages.CompletionItemKind.Value,
248 | insertText: suggestion.text,
249 | range: {
250 | startLineNumber: position.lineNumber,
251 | endLineNumber: position.lineNumber,
252 | startColumn: suggestion.startColumn || word.startColumn,
253 | endColumn: suggestion.endColumn || word.endColumn
254 | }
255 | })
256 | }
257 | return { suggestions }
258 | }
259 | }
260 | })
261 | }
262 |
263 | function defaultMonacoEditorOptions(): monaco.editor.IStandaloneEditorConstructionOptions {
264 | return {
265 | lineHeight: 30,
266 | fontSize: 22,
267 | renderLineHighlight: 'all',
268 | wordWrap: 'on',
269 | scrollBeyondLastLine: false,
270 | minimap: {
271 | enabled: false
272 | },
273 | renderValidationDecorations: 'off',
274 | lineDecorationsWidth: 0,
275 | glyphMargin: false,
276 | contextmenu: false,
277 | codeLens: true,
278 | mouseWheelZoom: true,
279 | quickSuggestions: false,
280 | suggest: {
281 | showIssues: false,
282 | shareSuggestSelections: false,
283 | showIcons: false,
284 | showMethods: false,
285 | showFunctions: false,
286 | showVariables: false,
287 | showKeywords: false,
288 | showWords: false,
289 | showClasses: false,
290 | showColors: false,
291 | showConstants: false,
292 | showConstructors: false,
293 | showEnumMembers: false,
294 | showEnums: false,
295 | showEvents: false,
296 | showFields: false,
297 | showFiles: false,
298 | showFolders: false,
299 | showInterfaces: false,
300 | showModules: false,
301 | showOperators: false,
302 | showProperties: false,
303 | showReferences: false,
304 | showSnippets: false,
305 | showStructs: false,
306 | showTypeParameters: false,
307 | showUnits: false,
308 | showValues: true,
309 | filterGraceful: false
310 | }
311 | }
312 | }
313 |
314 | class MonacoCodeLensProvider implements monaco.editor.CodeLensProvider {
315 | private _decorations: string[]
316 | private _errorCommand: string
317 | private _previewCommand: string
318 |
319 | constructor(
320 | private editor: monaco.editor.IStandaloneCodeEditor,
321 | private check: (text: string) => CodeExecutionResult,
322 | private execute: (text: string) => CodeExecutionResult,
323 | preview: (output: string) => void
324 | ) {
325 | this._errorCommand = editor.addCommand(0, function (_,result: CodeExecutionResult) {
326 | preview("Error: " + result.text)
327 | }, '');
328 | this._previewCommand = editor.addCommand(1, function (_,result: CodeExecutionResult) {
329 | console.log('_previewCommand',arguments)
330 | preview(result.text?.trim())
331 | }, '');
332 | }
333 | onDidChange?: monaco.editor.IEvent
334 | resolveCodeLens?(model: monaco.editor.ITextModel, codeLens: monaco.editor.CodeLens) {
335 | return codeLens
336 | }
337 | async provideCodeLenses(model: monaco.editor.ITextModel) {
338 | const lines = model.getLinesContent()
339 | const decorations: monaco.editor.IModelDeltaDecoration[] = []
340 | const lenses: monaco.editor.CodeLens[] = []
341 | for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
342 | const lineNum = lineIndex + 1
343 | const expr = lines[lineIndex]
344 | if (expr) {
345 | //TODO: Only process lines that have changed... (memoization)
346 | const result = await this.check(expr)
347 | console.log("check", { ...result })
348 | if (result.errors) {
349 | const errors: string[] = []
350 | for (var err of result.errors) {
351 | console.log("error", err.description)
352 |
353 | decorations.push({
354 | id: `error_${lineNum}_${err.start}_${err.end}`,
355 | range: new monaco.Range(lineNum, err.start + 1, lineNum, err.end + 1),
356 | options: {
357 | inlineClassName: "powerfx-error",
358 | hoverMessage: { value: err.description }
359 | }
360 | })
361 | errors.push(err.description)
362 | }
363 | result.text = errors.join('\n')
364 | lenses.push({
365 | range: new monaco.Range(lineNum, err.start + 1, lineNum, err.end + 1),
366 | id: `preview_${lineNum}_${err.start}_${err.end}`,
367 | command: {
368 | id: this._errorCommand,
369 | title: "Error(s)",
370 | tooltip: result.text,
371 | arguments: [result]
372 | }
373 | })
374 | } else if (result.type !== "BlankType") {
375 | const output = await this.execute(expr)
376 | console.log("execute", { ...output })
377 | decorations.push({
378 | id: `type_${lineNum}`,
379 | range: new monaco.Range(lineNum, 1, lineNum, 1),
380 | options: {
381 | inlineClassName: "powerfx-type",
382 | hoverMessage: { value: result.type }
383 | }
384 | })
385 | lenses.push({
386 | range: new monaco.Range(lineNum, 1, lineNum, 1),
387 | id: `preview_${lineNum}`,
388 | command: {
389 | id: this._previewCommand,
390 | title: "View Result",
391 | tooltip: result.text,
392 | arguments: [output]
393 | }
394 | })
395 | }
396 | }
397 | }
398 | this._decorations = this.editor.deltaDecorations(this._decorations || [], decorations);
399 | return {
400 | lenses,
401 | dispose: () => { }
402 | }
403 | }
404 | }
--------------------------------------------------------------------------------