From 3ba567eaddeaa0bb2bdd749b56e0601d1cf65a25 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Aug 2019 03:28:34 -0400 Subject: add JSON validator with initial support for manifest format (#654) --- .../Controllers/JsonValidatorController.cs | 217 +++++++++++++++++++++ src/SMAPI.Web/Controllers/LogParserController.cs | 4 +- .../Framework/Clients/Pastebin/IPastebinClient.cs | 3 +- .../Framework/Clients/Pastebin/PastebinClient.cs | 5 +- src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs | 3 + src/SMAPI.Web/SMAPI.Web.csproj | 1 + src/SMAPI.Web/Startup.cs | 2 +- .../JsonValidator/JsonValidatorErrorModel.cs | 36 ++++ .../ViewModels/JsonValidator/JsonValidatorModel.cs | 92 +++++++++ .../JsonValidator/JsonValidatorRequestModel.cs | 15 ++ src/SMAPI.Web/Views/JsonValidator/Index.cshtml | 126 ++++++++++++ src/SMAPI.Web/Views/Shared/_Layout.cshtml | 7 +- src/SMAPI.Web/appsettings.Development.json | 1 + src/SMAPI.Web/appsettings.json | 1 + .../wwwroot/Content/css/json-validator.css | 98 ++++++++++ src/SMAPI.Web/wwwroot/Content/css/main.css | 2 +- src/SMAPI.Web/wwwroot/Content/js/json-validator.js | 60 ++++++ src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 17 +- src/SMAPI.Web/wwwroot/schemas/manifest.json | 120 ++++++++++++ 19 files changed, 792 insertions(+), 18 deletions(-) create mode 100644 src/SMAPI.Web/Controllers/JsonValidatorController.cs create mode 100644 src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs create mode 100644 src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs create mode 100644 src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorRequestModel.cs create mode 100644 src/SMAPI.Web/Views/JsonValidator/Index.cshtml create mode 100644 src/SMAPI.Web/wwwroot/Content/css/json-validator.css create mode 100644 src/SMAPI.Web/wwwroot/Content/js/json-validator.js create mode 100644 src/SMAPI.Web/wwwroot/schemas/manifest.json (limited to 'src') diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs new file mode 100644 index 00000000..9d1685ac --- /dev/null +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Schema; +using StardewModdingAPI.Web.Framework; +using StardewModdingAPI.Web.Framework.Clients.Pastebin; +using StardewModdingAPI.Web.Framework.Compression; +using StardewModdingAPI.Web.Framework.ConfigModels; +using StardewModdingAPI.Web.ViewModels.JsonValidator; + +namespace StardewModdingAPI.Web.Controllers +{ + /// Provides a web UI for validating JSON schemas. + internal class JsonValidatorController : Controller + { + /********* + ** Fields + *********/ + /// The site config settings. + private readonly SiteConfig Config; + + /// The underlying Pastebin client. + private readonly IPastebinClient Pastebin; + + /// The underlying text compression helper. + private readonly IGzipHelper GzipHelper; + + /// The section URL for the schema validator. + private string SectionUrl => this.Config.JsonValidatorUrl; + + /// The supported JSON schemas (names indexed by ID). + private readonly IDictionary SchemaFormats = new Dictionary + { + ["none"] = "None", + ["manifest"] = "Manifest" + }; + + /// The schema ID to use if none was specified. + private string DefaultSchemaID = "manifest"; + + + /********* + ** Public methods + *********/ + /*** + ** Constructor + ***/ + /// Construct an instance. + /// The context config settings. + /// The Pastebin API client. + /// The underlying text compression helper. + public JsonValidatorController(IOptions siteConfig, IPastebinClient pastebin, IGzipHelper gzipHelper) + { + this.Config = siteConfig.Value; + this.Pastebin = pastebin; + this.GzipHelper = gzipHelper; + } + + /*** + ** Web UI + ***/ + /// Render the schema validator UI. + /// The schema name with which to validate the JSON. + /// The paste ID. + [HttpGet] + [Route("json")] + [Route("json/{schemaName}")] + [Route("json/{schemaName}/{id}")] + public async Task Index(string schemaName = null, string id = null) + { + schemaName = this.NormaliseSchemaName(schemaName); + + var result = new JsonValidatorModel(this.SectionUrl, id, schemaName, this.SchemaFormats); + if (string.IsNullOrWhiteSpace(id)) + return this.View("Index", result); + + // fetch raw JSON + PasteInfo paste = await this.GetAsync(id); + if (string.IsNullOrWhiteSpace(paste.Content)) + return this.View("Index", result.SetUploadError("The JSON file seems to be empty.")); + result.SetContent(paste.Content); + + // parse JSON + JToken parsed; + try + { + parsed = JToken.Parse(paste.Content); + } + catch (JsonReaderException ex) + { + return this.View("Index", result.AddErrors(new JsonValidatorErrorModel(ex.LineNumber, ex.Path, ex.Message))); + } + + // skip if no schema selected + if (schemaName == "none") + return this.View("Index", result); + + // load schema + JSchema schema; + { + FileInfo schemaFile = this.FindSchemaFile(schemaName); + if (schemaFile == null) + return this.View("Index", result.SetParseError($"Invalid schema '{schemaName}'.")); + schema = JSchema.Parse(System.IO.File.ReadAllText(schemaFile.FullName)); + } + + // validate JSON + parsed.IsValid(schema, out IList rawErrors); + var errors = rawErrors + .Select(error => new JsonValidatorErrorModel(error.LineNumber, error.Path, this.GetFlattenedError(error))) + .ToArray(); + return this.View("Index", result.AddErrors(errors)); + } + + /*** + ** JSON + ***/ + /// Save raw JSON data. + [HttpPost, AllowLargePosts] + [Route("json")] + public async Task PostAsync(JsonValidatorRequestModel request) + { + if (request == null) + return this.View("Index", new JsonValidatorModel(this.SectionUrl, null, null, this.SchemaFormats).SetUploadError("The request seems to be invalid.")); + + // normalise schema name + string schemaName = this.NormaliseSchemaName(request.SchemaName); + + // get raw log text + string input = request.Content; + if (string.IsNullOrWhiteSpace(input)) + return this.View("Index", new JsonValidatorModel(this.SectionUrl, null, schemaName, this.SchemaFormats).SetUploadError("The JSON file seems to be empty.")); + + // upload log + input = this.GzipHelper.CompressString(input); + SavePasteResult result = await this.Pastebin.PostAsync($"JSON validator {DateTime.UtcNow:s}", input); + + // handle errors + if (!result.Success) + return this.View("Index", new JsonValidatorModel(this.SectionUrl, result.ID, schemaName, this.SchemaFormats).SetUploadError($"Pastebin error: {result.Error ?? "unknown error"}")); + + // redirect to view + UriBuilder uri = new UriBuilder(new Uri(this.SectionUrl)); + uri.Path = $"{uri.Path.TrimEnd('/')}/{schemaName}/{result.ID}"; + return this.Redirect(uri.Uri.ToString()); + } + + + /********* + ** Private methods + *********/ + /// Fetch raw text from Pastebin. + /// The Pastebin paste ID. + private async Task GetAsync(string id) + { + PasteInfo response = await this.Pastebin.GetAsync(id); + response.Content = this.GzipHelper.DecompressString(response.Content); + return response; + } + + /// Get a flattened, human-readable message representing a schema validation error. + /// The error to represent. + /// The indentation level to apply for inner errors. + private string GetFlattenedError(ValidationError error, int indent = 0) + { + // get friendly representation of main error + string message = error.Message; + switch (error.ErrorType) + { + case ErrorType.Enum: + message = $"Invalid value. Found '{error.Value}', but expected one of '{string.Join("', '", error.Schema.Enum)}'."; + break; + } + + // add inner errors + foreach (ValidationError childError in error.ChildErrors) + message += "\n" + "".PadLeft(indent * 2, ' ') + $"==> {childError.Path}: " + this.GetFlattenedError(childError, indent + 1); + return message; + } + + /// Get a normalised schema name, or the if blank. + /// The raw schema name to normalise. + private string NormaliseSchemaName(string schemaName) + { + schemaName = schemaName?.Trim().ToLower(); + return !string.IsNullOrWhiteSpace(schemaName) + ? schemaName + : this.DefaultSchemaID; + } + + /// Get the schema file given its unique ID. + /// The schema ID. + private FileInfo FindSchemaFile(string id) + { + // normalise ID + id = id?.Trim().ToLower(); + if (string.IsNullOrWhiteSpace(id)) + return null; + + // get matching file + DirectoryInfo schemaDir = new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "schemas")); + foreach (FileInfo file in schemaDir.EnumerateFiles("*.json")) + { + if (file.Name.Equals($"{id}.json")) + return file; + } + + return null; + } + } +} diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index dc5895b0..0556a81e 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -84,7 +84,7 @@ namespace StardewModdingAPI.Web.Controllers // upload log input = this.GzipHelper.CompressString(input); - SavePasteResult result = await this.Pastebin.PostAsync(input); + SavePasteResult result = await this.Pastebin.PostAsync($"SMAPI log {DateTime.UtcNow:s}", input); // handle errors if (!result.Success) @@ -108,7 +108,5 @@ namespace StardewModdingAPI.Web.Controllers response.Content = this.GzipHelper.DecompressString(response.Content); return response; } - - } } diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs index 630dfb76..a635abe3 100644 --- a/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs @@ -11,7 +11,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin Task GetAsync(string id); /// Save a paste to Pastebin. + /// The paste name. /// The paste content. - Task PostAsync(string content); + Task PostAsync(string name, string content); } } diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs index 1e46f2dc..2e8a8c68 100644 --- a/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs @@ -67,8 +67,9 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin } /// Save a paste to Pastebin. + /// The paste name. /// The paste content. - public async Task PostAsync(string content) + public async Task PostAsync(string name, string content) { try { @@ -85,7 +86,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin api_user_key = this.UserKey, api_dev_key = this.DevKey, api_paste_private = 1, // unlisted - api_paste_name = $"SMAPI log {DateTime.UtcNow:s}", + api_paste_name = name, api_paste_expire_date = "N", // never expire api_paste_code = content })) diff --git a/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs index d89a4260..bc6e868a 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs @@ -12,6 +12,9 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// The root URL for the log parser. public string LogParserUrl { get; set; } + /// The root URL for the JSON validator. + public string JsonValidatorUrl { get; set; } + /// The root URL for the mod list. public string ModListUrl { get; set; } diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index 26b29fce..98517818 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -26,6 +26,7 @@ + diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index bb43f5a5..de45b8a4 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -203,7 +203,7 @@ namespace StardewModdingAPI.Web redirects.Add(new ConditionalRewriteSubdomainRule( shouldRewrite: req => req.Host.Host != "localhost" - && (req.Host.Host.StartsWith("api.") || req.Host.Host.StartsWith("log.") || req.Host.Host.StartsWith("mods.")) + && (req.Host.Host.StartsWith("api.") || req.Host.Host.StartsWith("json.") || req.Host.Host.StartsWith("log.") || req.Host.Host.StartsWith("mods.")) && !req.Path.StartsWithSegments("/content") && !req.Path.StartsWithSegments("/favicon.ico") )); diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs new file mode 100644 index 00000000..f9497a38 --- /dev/null +++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs @@ -0,0 +1,36 @@ +namespace StardewModdingAPI.Web.ViewModels.JsonValidator +{ + /// The view model for a JSON validator error. + public class JsonValidatorErrorModel + { + /********* + ** Accessors + *********/ + /// The line number on which the error occurred. + public int Line { get; set; } + + /// The field path in the JSON file where the error occurred. + public string Path { get; set; } + + /// A human-readable description of the error. + public string Message { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public JsonValidatorErrorModel() { } + + /// Construct an instance. + /// The line number on which the error occurred. + /// The field path in the JSON file where the error occurred. + /// A human-readable description of the error. + public JsonValidatorErrorModel(int line, string path, string message) + { + this.Line = line; + this.Path = path; + this.Message = message; + } + } +} diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs new file mode 100644 index 00000000..4c122d4f --- /dev/null +++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Web.ViewModels.JsonValidator +{ + /// The view model for the JSON validator page. + public class JsonValidatorModel + { + /********* + ** Accessors + *********/ + /// The root URL for the log parser controller. + public string SectionUrl { get; set; } + + /// The paste ID. + public string PasteID { get; set; } + + /// The schema name with which the JSON was validated. + public string SchemaName { get; set; } + + /// The supported JSON schemas (names indexed by ID). + public readonly IDictionary SchemaFormats; + + /// The validated content. + public string Content { get; set; } + + /// The schema validation errors, if any. + public JsonValidatorErrorModel[] Errors { get; set; } = new JsonValidatorErrorModel[0]; + + /// An error which occurred while uploading the JSON to Pastebin. + public string UploadError { get; set; } + + /// An error which occurred while parsing the JSON. + public string ParseError { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public JsonValidatorModel() { } + + /// Construct an instance. + /// The root URL for the log parser controller. + /// The paste ID. + /// The schema name with which the JSON was validated. + /// The supported JSON schemas (names indexed by ID). + public JsonValidatorModel(string sectionUrl, string pasteID, string schemaName, IDictionary schemaFormats) + { + this.SectionUrl = sectionUrl; + this.PasteID = pasteID; + this.SchemaName = schemaName; + this.SchemaFormats = schemaFormats; + } + + /// Set the validated content. + /// The validated content. + public JsonValidatorModel SetContent(string content) + { + this.Content = content; + + return this; + } + + /// Set the error which occurred while uploading the log to Pastebin. + /// The error message. + public JsonValidatorModel SetUploadError(string error) + { + this.UploadError = error; + + return this; + } + + /// Set the error which occurred while parsing the JSON. + /// The error message. + public JsonValidatorModel SetParseError(string error) + { + this.ParseError = error; + + return this; + } + + /// Add validation errors to the response. + /// The schema validation errors. + public JsonValidatorModel AddErrors(params JsonValidatorErrorModel[] errors) + { + this.Errors = this.Errors.Concat(errors).ToArray(); + + return this; + } + } +} diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorRequestModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorRequestModel.cs new file mode 100644 index 00000000..c8e851bf --- /dev/null +++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorRequestModel.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Web.ViewModels.JsonValidator +{ + /// The view model for a JSON validation request. + public class JsonValidatorRequestModel + { + /********* + ** Accessors + *********/ + /// The schema name with which to validate the JSON. + public string SchemaName { get; set; } + + /// The raw content to validate. + public string Content { get; set; } + } +} diff --git a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml new file mode 100644 index 00000000..cd7ca912 --- /dev/null +++ b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml @@ -0,0 +1,126 @@ +@using StardewModdingAPI.Web.ViewModels.JsonValidator +@model JsonValidatorModel + +@{ + ViewData["Title"] = "JSON validator"; +} + +@section Head { + @if (Model.PasteID != null) + { + + } + + + + + + + + + +} + +@* upload result banner *@ +@if (Model.UploadError != null) +{ + +} +else if (Model.ParseError != null) +{ + +} +else if (Model.PasteID != null) +{ + +} + +@* upload new file *@ +@if (Model.Content == null) +{ +

Upload a JSON file

+
+
    +
  1. + Choose the JSON format:
    + +
  2. +
  3. + Drag the file onto this textbox (or paste the text in):
    + +
  4. +
  5. + Click this button:
    + +
  6. +
+
+} + +@* validation results *@ +@if (Model.Content != null) +{ +
+ @if (Model.UploadError == null) + { +
+ Change JSON format: + +
+ +

Validation errors

+ @if (Model.Errors.Any()) + { + + + + + + + + @foreach (JsonValidatorErrorModel error in Model.Errors) + { + + + + + + } +
LineFieldError
@error.Line@error.Path@error.Message
+ } + else + { +

No errors found.

+ } + } + +

Raw content

+
@Model.Content
+
+} diff --git a/src/SMAPI.Web/Views/Shared/_Layout.cshtml b/src/SMAPI.Web/Views/Shared/_Layout.cshtml index 4c602b29..9911ef0e 100644 --- a/src/SMAPI.Web/Views/Shared/_Layout.cshtml +++ b/src/SMAPI.Web/Views/Shared/_Layout.cshtml @@ -16,9 +16,14 @@

SMAPI

+ +

Tools

+
diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index e6b4a1b1..baf7efb7 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -12,6 +12,7 @@ "RootUrl": "http://localhost:59482/", "ModListUrl": "http://localhost:59482/mods/", "LogParserUrl": "http://localhost:59482/log/", + "JsonValidatorUrl": "http://localhost:59482/json/", "BetaEnabled": false, "BetaBlurb": null }, diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index f9777f87..a440cf42 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -19,6 +19,7 @@ "RootUrl": null, // see top note "ModListUrl": null, // see top note "LogParserUrl": null, // see top note + "JsonValidatorUrl": null, // see top note "BetaEnabled": null, // see top note "BetaBlurb": null // see top note }, diff --git a/src/SMAPI.Web/wwwroot/Content/css/json-validator.css b/src/SMAPI.Web/wwwroot/Content/css/json-validator.css new file mode 100644 index 00000000..f9aeb18b --- /dev/null +++ b/src/SMAPI.Web/wwwroot/Content/css/json-validator.css @@ -0,0 +1,98 @@ +/********* +** Main layout +*********/ +#content { + max-width: 100%; +} + +#output { + padding: 10px; + overflow: auto; +} + +#output table td { + font-family: monospace; +} + +#output table tr th, +#output table tr td { + padding: 0 0.75rem; + white-space: pre-wrap; +} + + +/********* +** Result banner +*********/ +.banner { + border: 2px solid gray; + border-radius: 5px; + margin-top: 1em; + padding: 1em; +} + +.banner.success { + border-color: green; + background: #CFC; +} + +.banner.error { + border-color: red; + background: #FCC; +} + +/********* +** Validation results +*********/ +.table { + border-bottom: 1px dashed #888888; + margin-bottom: 5px; +} + +#metadata th, #metadata td { + text-align: left; + padding-right: 0.7em; +} + +.table { + border: 1px solid #000000; + background: #ffffff; + border-radius: 5px; + border-spacing: 1px; + overflow: hidden; + cursor: default; + box-shadow: 1px 1px 1px 1px #dddddd; +} + +.table tr { + background: #eee; +} + +.table tr:nth-child(even) { + background: #fff; +} + +/********* +** Upload form +*********/ +#input { + width: 100%; + height: 20em; + max-height: 70%; + margin: auto; + box-sizing: border-box; + border-radius: 5px; + border: 1px solid #000088; + outline: none; + box-shadow: inset 0px 0px 1px 1px rgba(0, 0, 192, .2); +} + +#submit { + font-size: 1.5em; + border-radius: 5px; + outline: none; + box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, .2); + cursor: pointer; + border: 1px solid #008800; + background-color: #cfc; +} diff --git a/src/SMAPI.Web/wwwroot/Content/css/main.css b/src/SMAPI.Web/wwwroot/Content/css/main.css index 57eeee88..dcc7a798 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/main.css +++ b/src/SMAPI.Web/wwwroot/Content/css/main.css @@ -73,7 +73,7 @@ a { } #sidebar h4 { - margin: 0 0 0.2em 0; + margin: 1.5em 0 0.2em 0; width: 10em; border-bottom: 1px solid #CCC; font-size: 0.8em; diff --git a/src/SMAPI.Web/wwwroot/Content/js/json-validator.js b/src/SMAPI.Web/wwwroot/Content/js/json-validator.js new file mode 100644 index 00000000..3f7a1775 --- /dev/null +++ b/src/SMAPI.Web/wwwroot/Content/js/json-validator.js @@ -0,0 +1,60 @@ +/* globals $ */ + +var smapi = smapi || {}; +smapi.jsonValidator = function (sectionUrl, pasteID) { + /** + * Rebuild the syntax-highlighted element. + */ + var formatCode = function () { + Sunlight.highlightAll(); + }; + + /** + * Initialise the JSON validator page. + */ + var init = function () { + // code formatting + formatCode(); + + // change format + $("#output #format").on("change", function() { + var schemaName = $(this).val(); + location.href = new URL(schemaName + "/" + pasteID, sectionUrl).toString(); + }); + + // upload form + var input = $("#input"); + if (input.length) { + // disable submit if it's empty + var toggleSubmit = function () { + var hasText = !!input.val().trim(); + submit.prop("disabled", !hasText); + }; + input.on("input", toggleSubmit); + toggleSubmit(); + + // drag & drop file + input.on({ + 'dragover dragenter': function (e) { + e.preventDefault(); + e.stopPropagation(); + }, + 'drop': function (e) { + var dataTransfer = e.originalEvent.dataTransfer; + if (dataTransfer && dataTransfer.files.length) { + e.preventDefault(); + e.stopPropagation(); + var file = dataTransfer.files[0]; + var reader = new FileReader(); + reader.onload = $.proxy(function (file, $input, event) { + $input.val(event.target.result); + toggleSubmit(); + }, this, file, $("#input")); + reader.readAsText(file); + } + } + }); + } + }; + init(); +}; diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index e87a1a5c..e6c7591c 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -23,7 +23,7 @@ smapi.logParser = function (data, sectionUrl) { } // set local time started - if(data) + if (data) data.localTimeStarted = ("0" + data.logStarted.getHours()).slice(-2) + ":" + ("0" + data.logStarted.getMinutes()).slice(-2); // init app @@ -100,7 +100,7 @@ smapi.logParser = function (data, sectionUrl) { updateModFilters(); }, - filtersAllow: function(modId, level) { + filtersAllow: function (modId, level) { return this.showMods[modId] !== false && this.showLevels[level] !== false; }, @@ -121,16 +121,15 @@ smapi.logParser = function (data, sectionUrl) { var submit = $("#submit"); // instruction OS chooser - var chooseSystem = function() { + var chooseSystem = function () { systemInstructions.hide(); systemInstructions.filter("[data-os='" + $("input[name='os']:checked").val() + "']").show(); - } + }; systemOptions.on("click", chooseSystem); chooseSystem(); // disable submit if it's empty - var toggleSubmit = function() - { + var toggleSubmit = function () { var hasText = !!input.val().trim(); submit.prop("disabled", !hasText); } @@ -139,18 +138,18 @@ smapi.logParser = function (data, sectionUrl) { // drag & drop file input.on({ - 'dragover dragenter': function(e) { + 'dragover dragenter': function (e) { e.preventDefault(); e.stopPropagation(); }, - 'drop': function(e) { + 'drop': function (e) { var dataTransfer = e.originalEvent.dataTransfer; if (dataTransfer && dataTransfer.files.length) { e.preventDefault(); e.stopPropagation(); var file = dataTransfer.files[0]; var reader = new FileReader(); - reader.onload = $.proxy(function(file, $input, event) { + reader.onload = $.proxy(function (file, $input, event) { $input.val(event.target.result); toggleSubmit(); }, this, file, $("#input")); diff --git a/src/SMAPI.Web/wwwroot/schemas/manifest.json b/src/SMAPI.Web/wwwroot/schemas/manifest.json new file mode 100644 index 00000000..06173333 --- /dev/null +++ b/src/SMAPI.Web/wwwroot/schemas/manifest.json @@ -0,0 +1,120 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://smapi.io/schemas/manifest.json", + "title": "SMAPI manifest", + "description": "Manifest file for a SMAPI mod or content pack", + "type": "object", + "properties": { + "Name": { + "title": "Mod name", + "description": "The mod's display name. SMAPI uses this in player messages, logs, and errors.", + "type": "string", + "examples": [ "Lookup Anything" ] + }, + "Author": { + "title": "Mod author", + "description": "The name of the person who created the mod. Ideally this should include the username used to publish mods.", + "type": "string", + "examples": [ "Pathoschild" ] + }, + "Version": { + "title": "Mod version", + "description": "The mod's semantic version. Make sure you update this for each release! SMAPI uses this for update checks, mod dependencies, and compatibility blacklists (if the mod breaks in a future version of the game).", + "$ref": "#/definitions/SemanticVersion" + }, + "Description": { + "title": "Mod description", + "description": "A short explanation of what your mod does (one or two sentences), shown in the SMAPI log.", + "type": "string", + "examples": [ "View metadata about anything by pressing a button." ] + }, + "UniqueID": { + "title": "Mod unique ID", + "description": "A unique identifier for your mod. The recommended format is \"Username.ModName\", with no spaces or special characters. SMAPI uses this for update checks, mod dependencies, and compatibility blacklists (if the mod breaks in a future version of the game). When another mod needs to reference this mod, it uses the unique ID.", + "$ref": "#/definitions/ModID" + }, + "EntryDll": { + "title": "Mod entry DLL", + "description": "The DLL filename SMAPI should load for this mod. Mutually exclusive with ContentPackFor.", + "type": "string", + "pattern": "^[a-zA-Z0-9_.-]+\\.dll$", + "examples": "LookupAnything.dll" + }, + "ContentPackFor": { + "title": "Content pack for", + "description": "Specifies the mod which can read this content pack.", + "type": "object", + "properties": { + "UniqueID": { + "title": "Required unique ID", + "description": "The unique ID of the mod which can read this content pack.", + "$ref": "#/definitions/ModID" + }, + "MinimumVersion": { + "title": "Required minimum version", + "description": "The minimum semantic version of the mod which can read this content pack, if applicable.", + "$ref": "#/definitions/SemanticVersion" + } + }, + + "required": [ "UniqueID" ] + }, + "Dependencies": { + "title": "Mod dependencies", + "description": "Specifies other mods to load before this mod. If a dependency is required and a player tries to use the mod without the dependency installed, the mod won't be loaded and they'll see a friendly message saying they need to install those.", + "type": "array", + "items": { + "type": "object", + "properties": { + "UniqueID": { + "title": "Dependency unique ID", + "description": "The unique ID of the mod to load first.", + "$ref": "#/definitions/ModID" + }, + "MinimumVersion": { + "title": "Dependency minimum version", + "description": "The minimum semantic version of the mod to load first, if applicable.", + "$ref": "#/definitions/SemanticVersion" + }, + "IsRequired": { + "title": "Dependency is required", + "description": "Whether the dependency is required. Default true if not specified." + } + }, + "required": [ "UniqueID" ] + } + }, + "UpdateKeys": { + "title": "Mod update keys", + "description": "Specifies where SMAPI should check for mod updates, so it can alert the user with a link to your mod page. See https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks for more info.", + "type": "array", + "items": { + "type": "string", + "pattern": "^(Chucklefish:\\d+|Nexus:\\d+|GitHub:[A-Za-z0-9_]+/[A-Za-z0-9_]+|ModDrop:\\d+)$" + } + } + }, + + "required": [ "Name", "Author", "Version", "Description", "UniqueID" ], + "oneOf": [ + { + "required": [ "EntryDll" ] + }, + { + "required": [ "ContentPackFor" ] + } + ], + + "definitions": { + "SemanticVersion": { + "type": "string", + "pattern": "(?>(?0|[1-9]\\d*))\\.(?>(?0|[1-9]\\d*))(?>(?:\\.(?0|[1-9]\\d*))?)(?:-(?(?>[a-zA-Z0-9]+[\\-\\.]?)+))?", // derived from SMAPI.Toolkit.SemanticVersion + "examples": [ "1.0.0", "1.0.1-beta.2" ] + }, + "ModID": { + "type": "string", + "pattern": "^[a-zA-Z0-9_.-]+$", // derived from SMAPI.Toolkit.Utilities.PathUtilities.IsSlug + "examples": [ "Pathoschild.LookupAnything" ] + } + } +} -- cgit