From e0891f0c7f936bc4fcc6513fb19b61d3a70644e6 Mon Sep 17 00:00:00 2001 From: Chris Bell Date: Wed, 2 Jul 2025 21:39:42 -0500 Subject: [PATCH] Added advanced section and fixed linter --- SessionZero/Data/SzfObject.cs | 24 ++++ SessionZero/Layout/NavBar.razor | 4 + SessionZero/Pages/Advanced.razor | 17 +++ SessionZero/Pages/Home.razor | 4 + SessionZero/Pages/SzfEditor.razor | 196 +++++++++++++++++++++++------- 5 files changed, 200 insertions(+), 45 deletions(-) create mode 100644 SessionZero/Pages/Advanced.razor diff --git a/SessionZero/Data/SzfObject.cs b/SessionZero/Data/SzfObject.cs index 7f05c94..21df1ae 100644 --- a/SessionZero/Data/SzfObject.cs +++ b/SessionZero/Data/SzfObject.cs @@ -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; } + + /// + /// Checks if required metadata fields are present and adds errors if they are missing. + /// + /// Dictionary containing metadata fields + /// Array of required field names + /// List to add errors to + protected void CheckRequiredMetadata(Dictionary metadata, string[] requiredFields, List 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 diff --git a/SessionZero/Layout/NavBar.razor b/SessionZero/Layout/NavBar.razor index 38bfb55..04f7656 100644 --- a/SessionZero/Layout/NavBar.razor +++ b/SessionZero/Layout/NavBar.razor @@ -13,6 +13,10 @@ Characters + + + Advanced + @code { diff --git a/SessionZero/Pages/Advanced.razor b/SessionZero/Pages/Advanced.razor new file mode 100644 index 0000000..85db2da --- /dev/null +++ b/SessionZero/Pages/Advanced.razor @@ -0,0 +1,17 @@ +@page "/Advanced" +@inject NavigationManager NavigationManager + +

Advanced Features

+ +
+ + +

SZF Editor

+

Manually create and edit SZF files with syntax highlighting and basic linting

+
+ + +

SZF Testing Grounds

+

Internal development tool for testing SZF parsing, saving, and retrieving

+
+
\ No newline at end of file diff --git a/SessionZero/Pages/Home.razor b/SessionZero/Pages/Home.razor index 2953ccc..23cbc83 100644 --- a/SessionZero/Pages/Home.razor +++ b/SessionZero/Pages/Home.razor @@ -1,5 +1,9 @@ @page "/" +@inject NavigationManager NavigationManager Home

Welcome to SessionZero

+ +

SessionZero is a tool designed to help you manage your tabletop RPG sessions, characters, and campaigns.

+ diff --git a/SessionZero/Pages/SzfEditor.razor b/SessionZero/Pages/SzfEditor.razor index 0fe2633..09add20 100644 --- a/SessionZero/Pages/SzfEditor.razor +++ b/SessionZero/Pages/SzfEditor.razor @@ -85,47 +85,13 @@ private static readonly HashSet 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(); var errors = new List(); + 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 { ["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 { ["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)) + { + errors.Add(new SzfError(0, "Missing [Metadata] section which is required for all file types")); + } + else + { + var metadata = structure["Metadata"] as Dictionary; + + // 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 metadata, string[] requiredFields, List 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);