More UI mockups, and added an szf-editor (need to update the spec on it though)
This commit is contained in:
parent
033e007758
commit
6cad6bc3a1
@ -1,6 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
namespace SessionZero.Pages.Library.Datasets;
|
namespace SessionZero.Components.Datasets;
|
||||||
|
|
||||||
public partial class DatasetEditor : ComponentBase
|
public partial class DatasetEditor : ComponentBase
|
||||||
{
|
{
|
@ -1,6 +0,0 @@
|
|||||||
@page "/Characters"
|
|
||||||
<h1 class="page-title">Characters</h1>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
|
|
||||||
}
|
|
17
SessionZero/Pages/Characters/Characters.razor
Normal file
17
SessionZero/Pages/Characters/Characters.razor
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
@page "/Characters"
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
|
||||||
|
<h1 class="page-title">Characters</h1>
|
||||||
|
|
||||||
|
<div class="library-card-container">
|
||||||
|
<NavLink class="card" onclick="@(() => { NavigationManager.NavigateTo("/characters"); })">
|
||||||
|
<img src="res/icons/outline/user/user-add.svg" class="icon" />
|
||||||
|
<h1>Create</h1>
|
||||||
|
<p>Create a new Character</p>
|
||||||
|
</NavLink>
|
||||||
|
<NavLink class="card" onclick="@(() => { NavigationManager.NavigateTo("/characters"); })">
|
||||||
|
<img src="res/icons/outline/user/address-book.svg" class="icon"/>
|
||||||
|
<h1>Manage</h1>
|
||||||
|
<p>View and manage saved Characters</p>
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
6
SessionZero/Pages/Characters/Characters.razor.cs
Normal file
6
SessionZero/Pages/Characters/Characters.razor.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace SessionZero.Pages.Characters;
|
||||||
|
|
||||||
|
public partial class Characters
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
@page "/library/character-templates"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@switch (_currentPageState)
|
||||||
|
{
|
||||||
|
case PageState.Main:
|
||||||
|
<NavLink class="button-secondary" href="/library/" style="align-self: flex-start;">
|
||||||
|
<img src="res/icons/outline/arrows/arrow-left.svg" class="button-icon" />
|
||||||
|
Back to Library
|
||||||
|
</NavLink>
|
||||||
|
<h1 class="page-title">Character Templates</h1>
|
||||||
|
<div class="library-card-container">
|
||||||
|
|
||||||
|
<div class="card" onclick="@(() => { SetPageState(PageState.Create); })">
|
||||||
|
<img src="res/icons/outline/general/plus.svg" class="icon"/>
|
||||||
|
<h1>Create</h1>
|
||||||
|
<p>Create a new template</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" onclick="@(() => { SetPageState(PageState.List); })">
|
||||||
|
<img src="res/icons/outline/general/edit.svg" class="icon"/>
|
||||||
|
<h1>Manage</h1>
|
||||||
|
<p>Manage and edit saved templates</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" onclick="@(() => { SetPageState(PageState.SessionZeroDb); })">
|
||||||
|
<img src="res/icons/outline/general/search.svg" class="icon"/>
|
||||||
|
<h1>SessionZeroDB</h1>
|
||||||
|
<p>Search online for new templates (NOT FUNCTIONAL)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
break;
|
||||||
|
case PageState.Create:
|
||||||
|
<div class="button-secondary" href="/library/" style="align-self: flex-start;" onclick="@(() => { SetPageState(PageState.Main); })">
|
||||||
|
<img src="res/icons/outline/arrows/arrow-left.svg" class="button-icon" />
|
||||||
|
Go Back
|
||||||
|
</div>
|
||||||
|
<h1 class="page-title">Create new Character</h1>
|
||||||
|
<p>Here you can create a new character template</p>
|
||||||
|
break;
|
||||||
|
case PageState.List:
|
||||||
|
<div class="button-secondary" href="/library/" style="align-self: flex-start;" onclick="@(() => { SetPageState(PageState.Main); })">
|
||||||
|
<img src="res/icons/outline/arrows/arrow-left.svg" class="button-icon" />
|
||||||
|
Go Back
|
||||||
|
</div>
|
||||||
|
<h1 class="page-title">Characters</h1>
|
||||||
|
<p>Here you can manage your saved character templates</p>
|
||||||
|
break;
|
||||||
|
case PageState.SessionZeroDb:
|
||||||
|
<div class="button-secondary" href="/library/" style="align-self: flex-start;" onclick="@(() => { SetPageState(PageState.Main); })">
|
||||||
|
<img src="res/icons/outline/arrows/arrow-left.svg" class="button-icon" />
|
||||||
|
Go Back
|
||||||
|
</div>
|
||||||
|
<h1 class="page-title">Create new Character</h1>
|
||||||
|
<p>Here you will be able to search for and download new character templates</p>
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
<div class="button-secondary" href="/library/" style="align-self: flex-start;" onclick="@(() => { SetPageState(PageState.Main); })">
|
||||||
|
<img src="res/icons/outline/arrows/arrow-left.svg" class="button-icon" />
|
||||||
|
Go Back
|
||||||
|
</div>
|
||||||
|
<h1 class="page-title">Oh no!</h1>
|
||||||
|
<p>You're not supposed to be here! Go back and try again.</p>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace SessionZero.Pages.Library.CharacterTemplates;
|
||||||
|
|
||||||
|
public partial class CharacterTemplates : ComponentBase
|
||||||
|
{
|
||||||
|
private PageState _currentPageState = PageState.Main;
|
||||||
|
|
||||||
|
private enum PageState
|
||||||
|
{
|
||||||
|
Main,
|
||||||
|
Create,
|
||||||
|
View,
|
||||||
|
List,
|
||||||
|
Edit,
|
||||||
|
SessionZeroDb
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetPageState(PageState state)
|
||||||
|
{
|
||||||
|
_currentPageState = state;
|
||||||
|
}
|
||||||
|
}
|
@ -1,35 +0,0 @@
|
|||||||
@page "/library/character-templates"
|
|
||||||
|
|
||||||
<NavLink class="button-secondary" href="/library/" style="align-self: flex-start;">
|
|
||||||
<img src="res/icons/outline/arrows/arrow-left.svg" class="button-icon" />
|
|
||||||
Back to Library
|
|
||||||
</NavLink>
|
|
||||||
|
|
||||||
<h1 class="page-title">Character Templates</h1>
|
|
||||||
|
|
||||||
<div class="library-card-container">
|
|
||||||
|
|
||||||
<div class="card" onclick="@(() => { })">
|
|
||||||
<img src="res/icons/outline/general/plus.svg" class="icon"/>
|
|
||||||
<h1>Create</h1>
|
|
||||||
<p>Create a new template</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" onclick="@(() => { })">
|
|
||||||
<img src="res/icons/outline/general/edit.svg" class="icon"/>
|
|
||||||
<h1>Manage</h1>
|
|
||||||
<p>Manage and edit saved templates</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" onclick="@(() => { })">
|
|
||||||
<img src="res/icons/outline/general/search.svg" class="icon"/>
|
|
||||||
<h1>SessionZeroDB</h1>
|
|
||||||
<p>Search online for new templates (NOT FUNCTIONAL)</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
|
|
||||||
}
|
|
@ -1,6 +1,7 @@
|
|||||||
@page "/Library/Datasets"
|
@page "/Library/Datasets"
|
||||||
@using SessionZero.Data
|
@using SessionZero.Data
|
||||||
@using SessionZero.Models
|
@using SessionZero.Models
|
||||||
|
@using SessionZero.Components.Datasets
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject SessionZero.Services.ISzfStorageService SzfStorageService
|
@inject SessionZero.Services.ISzfStorageService SzfStorageService
|
||||||
@inject SzfParser SzfParser
|
@inject SzfParser SzfParser
|
||||||
|
744
SessionZero/Pages/SzfEditor.razor
Normal file
744
SessionZero/Pages/SzfEditor.razor
Normal file
@ -0,0 +1,744 @@
|
|||||||
|
@page "/szf-editor"
|
||||||
|
@using System.Text
|
||||||
|
@using System.Text.RegularExpressions
|
||||||
|
@using Microsoft.AspNetCore.Components.Rendering
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@inject IJSRuntime JSRuntime
|
||||||
|
|
||||||
|
<PageTitle>SZF Editor</PageTitle>
|
||||||
|
|
||||||
|
<div class="szf-editor">
|
||||||
|
<div class="toolbar">
|
||||||
|
<button class="toolbar-btn" @onclick="OpenFile">
|
||||||
|
Open
|
||||||
|
</button>
|
||||||
|
<InputFile OnChange="HandleFileSelected" style="display: none;" @ref="fileInput" accept=".szf" />
|
||||||
|
<button class="toolbar-btn" @onclick="SaveFile">Save</button>
|
||||||
|
<button class="toolbar-btn" @onclick="ToggleConsole">Toggle Console</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-container">
|
||||||
|
<div class="editor-container">
|
||||||
|
<div class="code-editor" @ref="editorElement">
|
||||||
|
<div class="editor-gutter">
|
||||||
|
@for (int i = 1; i <= _lines.Count; i++)
|
||||||
|
{
|
||||||
|
<div class="line-number @(HasErrorOnLine(i-1) ? "error-line" : "")">@i</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="editor-content">
|
||||||
|
<textarea @bind="_content"
|
||||||
|
@bind:event="oninput"
|
||||||
|
@onkeydown="HandleKeyDown"
|
||||||
|
@onscroll="HandleScroll"
|
||||||
|
class="editor-textarea"
|
||||||
|
spellcheck="false"
|
||||||
|
autocomplete="off"
|
||||||
|
@ref="textareaRef"></textarea>
|
||||||
|
<div class="syntax-overlay" @ref="overlayRef">
|
||||||
|
@foreach (var line in _highlightedLines)
|
||||||
|
{
|
||||||
|
<div class="syntax-line">@((MarkupString)line)</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="console @(_showConsole ? "" : "hidden")">
|
||||||
|
@if (_errors.Any())
|
||||||
|
{
|
||||||
|
@foreach (var error in _errors)
|
||||||
|
{
|
||||||
|
<div class="error">Line @(error.Line + 1): @error.Message</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div>No errors.</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preview-container">
|
||||||
|
<div class="preview">
|
||||||
|
@if (_parsedStructure != null)
|
||||||
|
{
|
||||||
|
@RenderStructure(_parsedStructure)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private ElementReference editorElement;
|
||||||
|
private ElementReference textareaRef;
|
||||||
|
private ElementReference overlayRef;
|
||||||
|
private InputFile fileInput = null!;
|
||||||
|
|
||||||
|
private string _content = "";
|
||||||
|
private List<string> _lines = new();
|
||||||
|
private List<string> _highlightedLines = new();
|
||||||
|
private List<SzfError> _errors = new();
|
||||||
|
private Dictionary<string, object>? _parsedStructure;
|
||||||
|
private bool _showConsole = true;
|
||||||
|
|
||||||
|
private static readonly HashSet<string> ValidTypes = new()
|
||||||
|
{
|
||||||
|
"text", "text-field", "number", "bool", "group", "calculated",
|
||||||
|
"system", "dataset-reference", "dataset-type",
|
||||||
|
"dataset-reference-multiple", "dataset-type-multiple"
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
var exampleSzf = @"!type: character_template
|
||||||
|
!schema: 1.0.0
|
||||||
|
bad line here
|
||||||
|
|
||||||
|
# This is a comment. It will be ignored by the parser.
|
||||||
|
# It's useful for adding notes to your templates.
|
||||||
|
|
||||||
|
[Metadata]
|
||||||
|
Name (text) = Robust Fantasy Character
|
||||||
|
Author (text) = Gemini
|
||||||
|
GameSystem (text) = Fantasy RPG Universal
|
||||||
|
Description (text-field) = A character sheet with calculated values and dependencies.
|
||||||
|
InvalidType (faketype) = some value
|
||||||
|
|
||||||
|
[Required_Datasets]
|
||||||
|
Classes (dataset-reference) = Core Classes|c3d4e5f6-g7h8-9012-cdef-123456789012|1.5.0
|
||||||
|
|
||||||
|
[Section: Ability Scores]
|
||||||
|
Strength (number) = 14
|
||||||
|
Dexterity (number) = 12
|
||||||
|
Constitution (number) = 15
|
||||||
|
IsCool (bool) = true
|
||||||
|
|
||||||
|
[Section.Ability_Scores.Modifiers]
|
||||||
|
# Modifiers are calculated automatically.
|
||||||
|
StrengthMod (calculated) = (Strength - 10) / 2
|
||||||
|
DexterityMod (calculated) = (Dexterity - 10) / 2
|
||||||
|
ConstitutionMod (calculated) = (Constitution - 10) / 2
|
||||||
|
|
||||||
|
[Section: Combat]
|
||||||
|
ProficiencyBonus (number) = 2
|
||||||
|
HitPoints (calculated) = 10 + ConstitutionMod
|
||||||
|
ArmorClass (calculated) = 10 + DexterityMod
|
||||||
|
";
|
||||||
|
|
||||||
|
SetContent(exampleSzf);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetContent(string content)
|
||||||
|
{
|
||||||
|
_content = content;
|
||||||
|
_lines = content.Split('\n').ToList();
|
||||||
|
UpdateSyntaxHighlighting();
|
||||||
|
ParseContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSyntaxHighlighting()
|
||||||
|
{
|
||||||
|
_highlightedLines.Clear();
|
||||||
|
|
||||||
|
foreach (var line in _lines)
|
||||||
|
{
|
||||||
|
_highlightedLines.Add(HighlightLine(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string HighlightLine(string line)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
|
return " ";
|
||||||
|
|
||||||
|
var trimmed = line.Trim();
|
||||||
|
|
||||||
|
// Comments
|
||||||
|
if (trimmed.StartsWith("#"))
|
||||||
|
{
|
||||||
|
return $"<span class=\"cm-comment\">{System.Web.HttpUtility.HtmlEncode(line)}</span>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
if (trimmed.StartsWith("!"))
|
||||||
|
{
|
||||||
|
var parts = line.Split(':', 2);
|
||||||
|
if (parts.Length == 2)
|
||||||
|
{
|
||||||
|
var key = System.Web.HttpUtility.HtmlEncode(parts[0]);
|
||||||
|
var value = System.Web.HttpUtility.HtmlEncode(parts[1]);
|
||||||
|
return $"<span class=\"cm-meta\">{key}:</span>{value}";
|
||||||
|
}
|
||||||
|
return $"<span class=\"cm-meta\">{System.Web.HttpUtility.HtmlEncode(line)}</span>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section headers
|
||||||
|
if (trimmed.StartsWith("[") && trimmed.EndsWith("]"))
|
||||||
|
{
|
||||||
|
return $"<span class=\"cm-header\">{System.Web.HttpUtility.HtmlEncode(line)}</span>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field definitions
|
||||||
|
var fieldMatch = Regex.Match(trimmed, @"^([\w\d_]+)\s+\(([\w\-]+)\)\s*=\s*(.*)$");
|
||||||
|
if (fieldMatch.Success)
|
||||||
|
{
|
||||||
|
var fieldName = fieldMatch.Groups[1].Value;
|
||||||
|
var fieldType = fieldMatch.Groups[2].Value;
|
||||||
|
var fieldValue = fieldMatch.Groups[3].Value;
|
||||||
|
|
||||||
|
var leadingSpaces = line.Length - line.TrimStart().Length;
|
||||||
|
var spaces = leadingSpaces > 0 ? new string(' ', leadingSpaces) : "";
|
||||||
|
|
||||||
|
var highlightedName = $"<span class=\"cm-variable\">{System.Web.HttpUtility.HtmlEncode(fieldName)}</span>";
|
||||||
|
var highlightedType = $"<span class=\"cm-def\">({System.Web.HttpUtility.HtmlEncode(fieldType)})</span>";
|
||||||
|
var highlightedOperator = $"<span class=\"cm-operator\"> = </span>";
|
||||||
|
|
||||||
|
string highlightedValue;
|
||||||
|
if (fieldType == "calculated")
|
||||||
|
{
|
||||||
|
highlightedValue = HighlightCalculatedValue(fieldValue);
|
||||||
|
}
|
||||||
|
else if (fieldType == "number" && Regex.IsMatch(fieldValue.Trim(), @"^-?\d+(\.\d+)?$"))
|
||||||
|
{
|
||||||
|
highlightedValue = $"<span class=\"cm-number\">{System.Web.HttpUtility.HtmlEncode(fieldValue)}</span>";
|
||||||
|
}
|
||||||
|
else if (fieldType == "bool" && Regex.IsMatch(fieldValue.Trim(), @"^(true|false)$", RegexOptions.IgnoreCase))
|
||||||
|
{
|
||||||
|
highlightedValue = $"<span class=\"cm-atom\">{System.Web.HttpUtility.HtmlEncode(fieldValue)}</span>";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
highlightedValue = $"<span class=\"cm-string\">{System.Web.HttpUtility.HtmlEncode(fieldValue)}</span>";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{spaces}{highlightedName} {highlightedType}{highlightedOperator}{highlightedValue}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return System.Web.HttpUtility.HtmlEncode(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string HighlightCalculatedValue(string value)
|
||||||
|
{
|
||||||
|
var result = new StringBuilder();
|
||||||
|
var tokens = Regex.Split(value, @"(\+|\-|\*|\/|\(|\)|[\w\d_]+|\d+(?:\.\d+)?|\s+)");
|
||||||
|
|
||||||
|
foreach (var token in tokens)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(token)) continue;
|
||||||
|
|
||||||
|
if (Regex.IsMatch(token, @"^[+\-*/()]$"))
|
||||||
|
{
|
||||||
|
result.Append($"<span class=\"cm-operator\">{System.Web.HttpUtility.HtmlEncode(token)}</span>");
|
||||||
|
}
|
||||||
|
else if (Regex.IsMatch(token, @"^-?\d+(\.\d+)?$"))
|
||||||
|
{
|
||||||
|
result.Append($"<span class=\"cm-number\">{System.Web.HttpUtility.HtmlEncode(token)}</span>");
|
||||||
|
}
|
||||||
|
else if (Regex.IsMatch(token, @"^[A-Za-z_][\w\d_]*$"))
|
||||||
|
{
|
||||||
|
result.Append($"<span class=\"cm-variable-2\">{System.Web.HttpUtility.HtmlEncode(token)}</span>");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.Append(System.Web.HttpUtility.HtmlEncode(token));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ParseContent()
|
||||||
|
{
|
||||||
|
var result = ParseSzf(_content);
|
||||||
|
_parsedStructure = result.Structure;
|
||||||
|
_errors = result.Errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (Dictionary<string, object> Structure, List<SzfError> Errors) ParseSzf(string text)
|
||||||
|
{
|
||||||
|
var lines = text.Split('\n');
|
||||||
|
var structure = new Dictionary<string, object>();
|
||||||
|
var current = structure;
|
||||||
|
var path = new List<string>();
|
||||||
|
var errors = new List<SzfError>();
|
||||||
|
|
||||||
|
for (int idx = 0; idx < lines.Length; idx++)
|
||||||
|
{
|
||||||
|
var line = lines[idx];
|
||||||
|
var trimmed = line.Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith("#"))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (trimmed.StartsWith("!"))
|
||||||
|
{
|
||||||
|
var parts = trimmed.Substring(1).Split(':', 2);
|
||||||
|
if (parts.Length < 2)
|
||||||
|
{
|
||||||
|
errors.Add(new SzfError(idx, "Invalid metadata: Missing value after ':'"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
structure[parts[0].Trim()] = parts[1].Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (trimmed.StartsWith("[") && trimmed.EndsWith("]"))
|
||||||
|
{
|
||||||
|
var sectionPath = trimmed.Substring(1, trimmed.Length - 2).Replace(": ", ".");
|
||||||
|
path = sectionPath.Split('.').Where(p => !string.IsNullOrWhiteSpace(p)).ToList();
|
||||||
|
|
||||||
|
current = structure;
|
||||||
|
foreach (var p in path)
|
||||||
|
{
|
||||||
|
var cleanP = p.Trim();
|
||||||
|
if (!string.IsNullOrEmpty(cleanP))
|
||||||
|
{
|
||||||
|
if (!current.ContainsKey(cleanP))
|
||||||
|
current[cleanP] = new Dictionary<string, object>();
|
||||||
|
current = (Dictionary<string, object>)current[cleanP];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var match = Regex.Match(trimmed, @"^([\w\d_]+)\s+\(([\w\-]+)\)\s*=\s*(.*)$");
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(trimmed))
|
||||||
|
errors.Add(new SzfError(idx, "Invalid field definition. Expected format: FieldName (type) = value"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = match.Groups[1].Value;
|
||||||
|
var type = match.Groups[2].Value;
|
||||||
|
var value = match.Groups[3].Value.Trim();
|
||||||
|
|
||||||
|
if (!ValidTypes.Contains(type))
|
||||||
|
{
|
||||||
|
errors.Add(new SzfError(idx, $"Unknown field type: \"{type}\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
current[name] = new Dictionary<string, object> { ["type"] = type, ["value"] = value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (structure, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasErrorOnLine(int lineIndex)
|
||||||
|
{
|
||||||
|
return _errors.Any(e => e.Line == lineIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RenderFragment RenderStructure(Dictionary<string, object> structure)
|
||||||
|
{
|
||||||
|
return builder =>
|
||||||
|
{
|
||||||
|
int sequence = 0;
|
||||||
|
foreach (var kvp in structure)
|
||||||
|
{
|
||||||
|
if (kvp.Value is string stringValue)
|
||||||
|
{
|
||||||
|
builder.OpenElement(sequence++, "div");
|
||||||
|
builder.OpenElement(sequence++, "strong");
|
||||||
|
builder.AddContent(sequence++, $"{kvp.Key}:");
|
||||||
|
builder.CloseElement();
|
||||||
|
builder.AddContent(sequence++, $" {stringValue}");
|
||||||
|
builder.CloseElement();
|
||||||
|
}
|
||||||
|
else if (kvp.Value is Dictionary<string, object> dict)
|
||||||
|
{
|
||||||
|
RenderSection(builder, ref sequence, dict, kvp.Key, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenderSection(RenderTreeBuilder builder, ref int sequence, Dictionary<string, object> obj, string title, bool isTopLevel = false)
|
||||||
|
{
|
||||||
|
var sectionContent = new Dictionary<string, object>();
|
||||||
|
var subsections = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
foreach (var kvp in obj)
|
||||||
|
{
|
||||||
|
if (kvp.Value is Dictionary<string, object> dict && !dict.ContainsKey("type"))
|
||||||
|
{
|
||||||
|
subsections[kvp.Key] = dict;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sectionContent[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasContent = sectionContent.Any();
|
||||||
|
if (hasContent || isTopLevel)
|
||||||
|
{
|
||||||
|
builder.OpenElement(sequence++, "div");
|
||||||
|
builder.AddAttribute(sequence++, "class", "section");
|
||||||
|
|
||||||
|
builder.OpenElement(sequence++, "h3");
|
||||||
|
builder.AddContent(sequence++, title.Replace("_", " "));
|
||||||
|
builder.CloseElement();
|
||||||
|
|
||||||
|
if (hasContent)
|
||||||
|
{
|
||||||
|
builder.OpenElement(sequence++, "table");
|
||||||
|
builder.OpenElement(sequence++, "thead");
|
||||||
|
builder.OpenElement(sequence++, "tr");
|
||||||
|
builder.OpenElement(sequence++, "th");
|
||||||
|
builder.AddContent(sequence++, "Name");
|
||||||
|
builder.CloseElement();
|
||||||
|
builder.OpenElement(sequence++, "th");
|
||||||
|
builder.AddContent(sequence++, "Type");
|
||||||
|
builder.CloseElement();
|
||||||
|
builder.OpenElement(sequence++, "th");
|
||||||
|
builder.AddContent(sequence++, "Value");
|
||||||
|
builder.CloseElement();
|
||||||
|
builder.CloseElement();
|
||||||
|
builder.CloseElement();
|
||||||
|
|
||||||
|
builder.OpenElement(sequence++, "tbody");
|
||||||
|
foreach (var kvp in sectionContent)
|
||||||
|
{
|
||||||
|
if (kvp.Value is Dictionary<string, object> field &&
|
||||||
|
field.ContainsKey("type") && field.ContainsKey("value"))
|
||||||
|
{
|
||||||
|
builder.OpenElement(sequence++, "tr");
|
||||||
|
|
||||||
|
builder.OpenElement(sequence++, "td");
|
||||||
|
builder.AddAttribute(sequence++, "class", "field");
|
||||||
|
builder.AddContent(sequence++, kvp.Key);
|
||||||
|
builder.CloseElement();
|
||||||
|
|
||||||
|
builder.OpenElement(sequence++, "td");
|
||||||
|
builder.AddAttribute(sequence++, "class", "type");
|
||||||
|
builder.AddContent(sequence++, field["type"].ToString());
|
||||||
|
builder.CloseElement();
|
||||||
|
|
||||||
|
builder.OpenElement(sequence++, "td");
|
||||||
|
builder.AddAttribute(sequence++, "class", "value");
|
||||||
|
builder.AddContent(sequence++, field["value"].ToString());
|
||||||
|
builder.CloseElement();
|
||||||
|
|
||||||
|
builder.CloseElement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.CloseElement();
|
||||||
|
builder.CloseElement();
|
||||||
|
}
|
||||||
|
else if (isTopLevel && !subsections.Any())
|
||||||
|
{
|
||||||
|
builder.AddContent(sequence++, "No entries.");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.CloseElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var kvp in subsections)
|
||||||
|
{
|
||||||
|
if (kvp.Value is Dictionary<string, object> subDict)
|
||||||
|
{
|
||||||
|
RenderSection(builder, ref sequence, subDict, $"{title}.{kvp.Key}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleKeyDown(KeyboardEventArgs e)
|
||||||
|
{
|
||||||
|
await Task.Delay(1); // Allow the input to be processed
|
||||||
|
await InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
_lines = _content.Split('\n').ToList();
|
||||||
|
UpdateSyntaxHighlighting();
|
||||||
|
ParseContent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleScroll()
|
||||||
|
{
|
||||||
|
// Sync scroll between textarea and overlay
|
||||||
|
await JSRuntime.InvokeVoidAsync("syncScroll", textareaRef, overlayRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OpenFile()
|
||||||
|
{
|
||||||
|
var element = fileInput.Element;
|
||||||
|
if (element.HasValue)
|
||||||
|
{
|
||||||
|
await JSRuntime.InvokeVoidAsync("triggerFileInput", element.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleFileSelected(InputFileChangeEventArgs e)
|
||||||
|
{
|
||||||
|
var file = e.File;
|
||||||
|
if (file != null)
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(file.OpenReadStream());
|
||||||
|
var content = await reader.ReadToEndAsync();
|
||||||
|
SetContent(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveFile()
|
||||||
|
{
|
||||||
|
var fileName = "file.szf";
|
||||||
|
var content = _content;
|
||||||
|
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleConsole()
|
||||||
|
{
|
||||||
|
_showConsole = !_showConsole;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SzfError
|
||||||
|
{
|
||||||
|
public int Line { get; }
|
||||||
|
public string Message { get; }
|
||||||
|
|
||||||
|
public SzfError(int line, string message)
|
||||||
|
{
|
||||||
|
Line = line;
|
||||||
|
Message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
font-family: sans-serif;
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #ddd;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.szf-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #ddd;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
padding: 0.5em;
|
||||||
|
background: #333;
|
||||||
|
gap: 0.5em;
|
||||||
|
border-bottom: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn {
|
||||||
|
background: #444;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 0.4em 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container, .preview-container {
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-editor {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
background: #21252b;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-gutter {
|
||||||
|
background: #21252b;
|
||||||
|
border-right: 1px solid #3a3f4b;
|
||||||
|
padding: 0;
|
||||||
|
min-width: 50px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-number {
|
||||||
|
color: #636d83;
|
||||||
|
padding: 0 8px;
|
||||||
|
text-align: right;
|
||||||
|
user-select: none;
|
||||||
|
height: 21px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-number.error-line {
|
||||||
|
background: #3c1e1e;
|
||||||
|
color: #ff5555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-textarea {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: transparent;
|
||||||
|
color: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 0 8px;
|
||||||
|
white-space: pre;
|
||||||
|
overflow-wrap: normal;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 2;
|
||||||
|
caret-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.syntax-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 0 8px;
|
||||||
|
white-space: pre;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.syntax-line {
|
||||||
|
height: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SZF Syntax Highlighting */
|
||||||
|
.cm-header { color: #61afef; }
|
||||||
|
.cm-meta { color: #c678dd; }
|
||||||
|
.cm-variable { color: #e06c75; }
|
||||||
|
.cm-def { color: #98c379; font-style: italic; }
|
||||||
|
.cm-string { color: #56b6c2; }
|
||||||
|
.cm-operator { color: #c678dd; }
|
||||||
|
.cm-number { color: #d19a66; }
|
||||||
|
.cm-atom { color: #d19a66; }
|
||||||
|
.cm-variable-2 { color: #abb2bf; }
|
||||||
|
.cm-comment { color: #7f848e; font-style: italic; }
|
||||||
|
|
||||||
|
.console {
|
||||||
|
background: #111;
|
||||||
|
padding: 0.5em;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-top: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #ff5555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
flex: 1;
|
||||||
|
background: #21252b;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 1em;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
border: 1px solid #444;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding: 0.5em;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #282c34;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section h3 {
|
||||||
|
margin: 0 0 0.3em 0;
|
||||||
|
color: #61afef;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #444;
|
||||||
|
padding: 0.3em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #333;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field { color: #e06c75; }
|
||||||
|
.type { color: #98c379; }
|
||||||
|
.value { color: #56b6c2; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.syncScroll = (textarea, overlay) => {
|
||||||
|
overlay.scrollTop = textarea.scrollTop;
|
||||||
|
overlay.scrollLeft = textarea.scrollLeft;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.triggerFileInput = (element) => {
|
||||||
|
element.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.downloadFile = (filename, content) => {
|
||||||
|
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
</script>
|
@ -1,4 +1,4 @@
|
|||||||
@page "/szf"
|
@page "/szf-parser-test"
|
||||||
@using SessionZero.Data
|
@using SessionZero.Data
|
||||||
@using SessionZero.Services
|
@using SessionZero.Services
|
||||||
@using SessionZero.Models
|
@using SessionZero.Models
|
||||||
|
@ -23,7 +23,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Components\Datasets\" />
|
|
||||||
<Folder Include="wwwroot\res\images\" />
|
<Folder Include="wwwroot\res\images\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user