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;
|
||||
|
||||
namespace SessionZero.Pages.Library.Datasets;
|
||||
namespace SessionZero.Components.Datasets;
|
||||
|
||||
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"
|
||||
@using SessionZero.Data
|
||||
@using SessionZero.Models
|
||||
@using SessionZero.Components.Datasets
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject SessionZero.Services.ISzfStorageService SzfStorageService
|
||||
@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.Services
|
||||
@using SessionZero.Models
|
||||
|
@ -23,7 +23,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Components\Datasets\" />
|
||||
<Folder Include="wwwroot\res\images\" />
|
||||
</ItemGroup>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user