Added advanced section and fixed linter
This commit is contained in:
parent
f6e04df6ac
commit
e0891f0c7f
@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using SessionZero.Pages;
|
||||
|
||||
namespace SessionZero.Data;
|
||||
|
||||
@ -138,6 +139,29 @@ public abstract class SzfObject
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if required metadata fields are present and adds errors if they are missing.
|
||||
/// </summary>
|
||||
/// <param name="metadata">Dictionary containing metadata fields</param>
|
||||
/// <param name="requiredFields">Array of required field names</param>
|
||||
/// <param name="errors">List to add errors to</param>
|
||||
protected void CheckRequiredMetadata(Dictionary<string, object> metadata, string[] requiredFields, List<SzfEditor.SzfError> errors)
|
||||
{
|
||||
if (metadata == null)
|
||||
{
|
||||
errors.Add(new SzfEditor.SzfError(0, "Missing Metadata section"));
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var field in requiredFields)
|
||||
{
|
||||
if (!metadata.ContainsKey(field) || metadata[field] == null || string.IsNullOrEmpty(metadata[field].ToString()))
|
||||
{
|
||||
errors.Add(new SzfEditor.SzfError(0, $"Missing required metadata: {field}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SzfValidationResult
|
||||
|
@ -13,6 +13,10 @@
|
||||
<img src="res/icons/outline/user/users-group.svg" class="nav-icon" />
|
||||
Characters
|
||||
</NavLink>
|
||||
<NavLink class="nav-link" href="advanced">
|
||||
<img src="res/icons/outline/general/bug.svg" class="nav-icon" />
|
||||
Advanced
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
17
SessionZero/Pages/Advanced.razor
Normal file
17
SessionZero/Pages/Advanced.razor
Normal file
@ -0,0 +1,17 @@
|
||||
@page "/Advanced"
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<h1 class="page-title">Advanced Features</h1>
|
||||
|
||||
<div class="library-card-container">
|
||||
<NavLink class="card" onclick="@(() => { NavigationManager.NavigateTo("/szf-editor"); })">
|
||||
<img src="res/icons/outline/general/edit.svg" class="icon" />
|
||||
<h1>SZF Editor</h1>
|
||||
<p>Manually create and edit SZF files with syntax highlighting and basic linting</p>
|
||||
</NavLink>
|
||||
<NavLink class="card" onclick="@(() => { NavigationManager.NavigateTo("/szf-parser-test"); })">
|
||||
<img src="res/icons/outline/general/bug.svg" class="icon"/>
|
||||
<h1>SZF Testing Grounds</h1>
|
||||
<p>Internal development tool for testing SZF parsing, saving, and retrieving</p>
|
||||
</NavLink>
|
||||
</div>
|
@ -1,5 +1,9 @@
|
||||
@page "/"
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<h1 class="page-title">Welcome to SessionZero</h1>
|
||||
|
||||
<p class="page-description">SessionZero is a tool designed to help you manage your tabletop RPG sessions, characters, and campaigns.</p>
|
||||
|
||||
|
@ -85,47 +85,13 @@
|
||||
|
||||
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"
|
||||
"text", "text-field", "number", "bool", "calculated",
|
||||
"system", "entry-reference", "entry-reference-list"
|
||||
};
|
||||
|
||||
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
|
||||
";
|
||||
var exampleSzf = "";
|
||||
|
||||
SetContent(exampleSzf);
|
||||
}
|
||||
@ -265,12 +231,39 @@ ArmorClass (calculated) = 10 + DexterityMod
|
||||
var current = structure;
|
||||
var path = new List<string>();
|
||||
var errors = new List<SzfError>();
|
||||
var fileType = "";
|
||||
var schemaVersion = "";
|
||||
var hasRequiredHeader = false;
|
||||
|
||||
// First pass - verify header requirements
|
||||
if (lines.Length >= 2)
|
||||
{
|
||||
var typeMatch = Regex.Match(lines[0].Trim(), @"^!type:\s*([\w_-]+)$");
|
||||
var schemaMatch = Regex.Match(lines[1].Trim(), @"^!schema:\s*([\d.]+)$");
|
||||
|
||||
if (typeMatch.Success && schemaMatch.Success)
|
||||
{
|
||||
fileType = typeMatch.Groups[1].Value;
|
||||
schemaVersion = schemaMatch.Groups[1].Value;
|
||||
hasRequiredHeader = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasRequiredHeader)
|
||||
{
|
||||
errors.Add(new SzfError(0, "Missing required header. First two lines must be !type: and !schema:"));
|
||||
}
|
||||
else if (!new[] { "dataset", "character_template", "character", "session" }.Contains(fileType))
|
||||
{
|
||||
errors.Add(new SzfError(0, $"Invalid file type: {fileType}. Must be one of: dataset, character_template, character, session"));
|
||||
}
|
||||
|
||||
// Second pass - parse content
|
||||
for (int idx = 0; idx < lines.Length; idx++)
|
||||
{
|
||||
var line = lines[idx];
|
||||
var trimmed = line.Trim();
|
||||
|
||||
|
||||
if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith("#"))
|
||||
continue;
|
||||
|
||||
@ -288,9 +281,34 @@ ArmorClass (calculated) = 10 + DexterityMod
|
||||
}
|
||||
else if (trimmed.StartsWith("[") && trimmed.EndsWith("]"))
|
||||
{
|
||||
// Extract section name (remove brackets)
|
||||
var sectionPath = trimmed.Substring(1, trimmed.Length - 2).Replace(": ", ".");
|
||||
|
||||
// Parse the section path and validate each segment
|
||||
var sections = sectionPath.Split('.');
|
||||
for (int i = 0; i < sections.Length; i++)
|
||||
{
|
||||
var section = sections[i].Trim();
|
||||
if (string.IsNullOrWhiteSpace(section))
|
||||
{
|
||||
errors.Add(new SzfError(idx, "Empty section name is not allowed"));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for Entry: prefix for datasets
|
||||
if (fileType == "dataset" && i == 0 && section.StartsWith("Entry:"))
|
||||
{
|
||||
// This is valid for datasets
|
||||
}
|
||||
// Otherwise, check if section follows PascalCase
|
||||
else if (!Regex.IsMatch(section, @"^([A-Z][a-z0-9]*)+$") && section != "Required_Datasets")
|
||||
{
|
||||
errors.Add(new SzfError(idx, $"Section name '{section}' must use PascalCase (e.g., AbilityScores, CharacterInfo)"));
|
||||
}
|
||||
}
|
||||
|
||||
path = sectionPath.Split('.').Where(p => !string.IsNullOrWhiteSpace(p)).ToList();
|
||||
|
||||
|
||||
current = structure;
|
||||
foreach (var p in path)
|
||||
{
|
||||
@ -305,6 +323,27 @@ ArmorClass (calculated) = 10 + DexterityMod
|
||||
}
|
||||
else
|
||||
{
|
||||
// For character files, the field syntax is different (no type)
|
||||
if (fileType == "character")
|
||||
{
|
||||
var characterFieldMatch = Regex.Match(trimmed, @"^([\w\d_]+)\s*=\s*(.*)$");
|
||||
if (characterFieldMatch.Success)
|
||||
{
|
||||
var name = characterFieldMatch.Groups[1].Value;
|
||||
var value = characterFieldMatch.Groups[2].Value.Trim();
|
||||
|
||||
// Validate PascalCase for field names in character files
|
||||
if (!Regex.IsMatch(name, @"^([A-Z][a-z0-9]*)+$") && !name.Contains("."))
|
||||
{
|
||||
errors.Add(new SzfError(idx, $"Field name '{name}' must use PascalCase (e.g., CharacterName, MaxHealth)"));
|
||||
}
|
||||
|
||||
current[name] = value;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Standard field definition with type
|
||||
var match = Regex.Match(trimmed, @"^([\w\d_]+)\s+\(([\w\-]+)\)\s*=\s*(.*)$");
|
||||
if (!match.Success)
|
||||
{
|
||||
@ -313,22 +352,89 @@ ArmorClass (calculated) = 10 + DexterityMod
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = match.Groups[1].Value;
|
||||
var type = match.Groups[2].Value;
|
||||
var value = match.Groups[3].Value.Trim();
|
||||
var fieldName = match.Groups[1].Value;
|
||||
var fieldType = match.Groups[2].Value;
|
||||
var fieldValue = match.Groups[3].Value.Trim();
|
||||
|
||||
if (!ValidTypes.Contains(type))
|
||||
// Validate PascalCase for field names
|
||||
if (!Regex.IsMatch(fieldName, @"^([A-Z][a-z0-9]*)+$"))
|
||||
{
|
||||
errors.Add(new SzfError(idx, $"Unknown field type: \"{type}\""));
|
||||
errors.Add(new SzfError(idx, $"Field name '{fieldName}' must use PascalCase (e.g., CharacterName, MaxHealth)"));
|
||||
}
|
||||
|
||||
current[name] = new Dictionary<string, object> { ["type"] = type, ["value"] = value };
|
||||
// Validate field type
|
||||
if (!ValidTypes.Contains(fieldType))
|
||||
{
|
||||
errors.Add(new SzfError(idx, $"Unknown field type: \"{fieldType}\""));
|
||||
}
|
||||
|
||||
// Validate specific field value formats
|
||||
if (fieldType == "number" && !string.IsNullOrEmpty(fieldValue) &&
|
||||
!Regex.IsMatch(fieldValue, @"^-?\d+(\.\d+)?$"))
|
||||
{
|
||||
errors.Add(new SzfError(idx, $"Invalid number format: {fieldValue}"));
|
||||
}
|
||||
else if (fieldType == "bool" && !string.IsNullOrEmpty(fieldValue) &&
|
||||
!Regex.IsMatch(fieldValue.ToLower(), @"^(true|false)$"))
|
||||
{
|
||||
errors.Add(new SzfError(idx, $"Boolean values must be 'true' or 'false', got: {fieldValue}"));
|
||||
}
|
||||
|
||||
current[fieldName] = new Dictionary<string, object> { ["type"] = fieldType, ["value"] = fieldValue };
|
||||
}
|
||||
}
|
||||
|
||||
// Verify required metadata sections based on file type
|
||||
if (hasRequiredHeader)
|
||||
{
|
||||
if (!structure.ContainsKey("Metadata") && !(structure.ContainsKey("Metadata") && structure["Metadata"] is Dictionary<string, object>))
|
||||
{
|
||||
errors.Add(new SzfError(0, "Missing [Metadata] section which is required for all file types"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var metadata = structure["Metadata"] as Dictionary<string, object>;
|
||||
|
||||
// Check file type specific requirements
|
||||
if (fileType == "dataset")
|
||||
{
|
||||
CheckRequiredMetadata(metadata, new[] {"Name", "Type", "Version"}, errors);
|
||||
}
|
||||
else if (fileType == "character_template")
|
||||
{
|
||||
CheckRequiredMetadata(metadata, new[] {"Name", "Version"}, errors);
|
||||
|
||||
// Check for RequiredDatasets section if referenced
|
||||
if (structure.ContainsValue("dataset-reference") || structure.ContainsValue("entry-reference"))
|
||||
{
|
||||
if (!structure.ContainsKey("RequiredDatasets") && !structure.ContainsKey("Required_Datasets"))
|
||||
{
|
||||
errors.Add(new SzfError(0, "Template uses dataset references but is missing [RequiredDatasets] section"));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (fileType == "character")
|
||||
{
|
||||
CheckRequiredMetadata(metadata, new[] {"TemplateRef"}, errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (structure, errors);
|
||||
}
|
||||
|
||||
private void CheckRequiredMetadata(Dictionary<string, object> metadata, string[] requiredFields, List<SzfError> errors)
|
||||
{
|
||||
foreach (var field in requiredFields)
|
||||
{
|
||||
if (!metadata.ContainsKey(field))
|
||||
{
|
||||
// Reporting the error on a general line since this check runs after initial parsing.
|
||||
errors.Add(new SzfError(0, $"Missing required metadata field: '{field}'"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasErrorOnLine(int lineIndex)
|
||||
{
|
||||
return _errors.Any(e => e.Line == lineIndex);
|
||||
|
Loading…
Reference in New Issue
Block a user