diff --git a/package-lock.json b/package-lock.json index 90ae64f..1345920 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "ajv": "^8.17.1", "bootstrap": "^5.3.2", "gatsby": "^5.13.1", "gatsby-plugin-feed": "^5.13.0", @@ -1989,6 +1990,21 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@eslint/eslintrc/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2027,6 +2043,11 @@ "node": ">= 4" } }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "node_modules/@eslint/eslintrc/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4492,28 +4513,20 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/anser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/anser/-/anser-2.1.1.tgz", @@ -4896,6 +4909,34 @@ "webpack": ">=2" } }, + "node_modules/babel-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/babel-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/babel-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "node_modules/babel-loader/node_modules/schema-utils": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", @@ -7763,6 +7804,21 @@ "@babel/highlight": "^7.10.4" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -7823,6 +7879,11 @@ "node": ">= 4" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "node_modules/eslint/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -8133,6 +8194,11 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, + "node_modules/fast-uri": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==" + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -8452,6 +8518,29 @@ } } }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", @@ -8481,6 +8570,11 @@ "node": ">=10" } }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", @@ -11223,9 +11317,9 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -15459,6 +15553,34 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "node_modules/section-matter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", @@ -16456,26 +16578,6 @@ "node": ">=10.0.0" } }, - "node_modules/table/node_modules/ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/table/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -19292,6 +19394,17 @@ "strip-json-comments": "^3.1.1" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -19313,6 +19426,11 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -21143,22 +21261,16 @@ } }, "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" } }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "requires": {} - }, "anser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/anser/-/anser-2.1.1.tgz", @@ -21428,6 +21540,28 @@ "schema-utils": "^2.6.5" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "requires": {} + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "schema-utils": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", @@ -23324,6 +23458,17 @@ "@babel/highlight": "^7.10.4" } }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -23360,6 +23505,11 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -23831,6 +23981,11 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, + "fast-uri": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==" + }, "fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -24051,6 +24206,23 @@ "tapable": "^1.0.0" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "requires": {} + }, "cosmiconfig": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", @@ -24074,6 +24246,11 @@ "universalify": "^2.0.0" } }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "schema-utils": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", @@ -26020,9 +26197,9 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -29045,6 +29222,30 @@ "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "requires": {} + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + } } }, "section-matter": { @@ -29809,24 +30010,6 @@ "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" - }, - "dependencies": { - "ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - } } }, "tapable": { diff --git a/package.json b/package.json index 9e46cfa..dd52dc3 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "url": "https://github.com/lottie/lottie.github.io/issues" }, "dependencies": { + "ajv": "^8.17.1", "bootstrap": "^5.3.2", "gatsby": "^5.13.1", "gatsby-plugin-feed": "^5.13.0", diff --git a/src/assets/lottie.schema.json b/src/assets/lottie.schema.json new file mode 100644 index 0000000..9aa55e7 --- /dev/null +++ b/src/assets/lottie.schema.json @@ -0,0 +1,2258 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://lottie.github.io/lottie-spec/specs/schema/0.1.0", + "$ref": "#/$defs/composition/animation", + "$version": 100, + "$defs": { + "assets": { + "precomposition": { + "type": "object", + "title": "Precomposition", + "description": "Asset containing a composition that can be referenced by layers.", + "allOf": [ + { + "$ref": "#/$defs/assets/asset" + }, + { + "$ref": "#/$defs/composition/composition" + } + ] + }, + "asset": { + "type": "object", + "title": "Asset", + "allOf": [ + { + "$ref": "#/$defs/helpers/visual-object" + }, + { + "type": "object", + "properties": { + "id": { + "title": "ID", + "description": "Unique identifier used by layers when referencing this asset", + "type": "string" + } + }, + "required": [ + "id" + ] + } + ] + }, + "image": { + "type": "object", + "title": "Image", + "description": "Asset containing an image that can be referenced by layers.", + "allOf": [ + { + "$ref": "#/$defs/assets/asset" + }, + { + "$ref": "#/$defs/helpers/slottable-object" + }, + { + "type": "object", + "properties": { + "w": { + "title": "Width", + "description": "Width of the image", + "type": "number" + }, + "h": { + "title": "Height", + "description": "Height of the image", + "type": "number" + }, + "p": { + "title": "File Name", + "description": "Name of the image file or a data url", + "type": "string" + }, + "u": { + "title": "File Path", + "description": "Path to the image file", + "type": "string" + }, + "e": { + "title": "Embedded", + "description": "If '1', 'p' is a Data URL", + "$ref": "#/$defs/values/int-boolean" + } + }, + "allOf": [ + { + "if": { + "properties": { + "e": { + "const": 1 + } + }, + "required": [ + "e" + ] + }, + "then": { + "properties": { + "p": { + "$ref": "#/$defs/values/data-url" + } + } + } + } + ], + "if": { + "required": [ + "sid" + ] + }, + "else": { + "required": [ + "w", + "h", + "p" + ] + } + } + ] + }, + "all-assets": { + "oneOf": [ + { + "$ref": "#/$defs/assets/precomposition" + }, + { + "$ref": "#/$defs/assets/image" + } + ] + } + }, + "composition": { + "animation": { + "type": "object", + "title": "Animation", + "description": "Top level object, describing the animation", + "allOf": [ + { + "$ref": "#/$defs/helpers/visual-object" + }, + { + "type": "object", + "properties": { + "ver": { + "title": "Specification Version", + "description": "Specification version this Lottie is targeting. This is a 6 digit number with version components encoded as `MMmmpp`, with `MM` being major version, `mm` being minor and `pp` being patch.", + "type": "integer", + "minimum": 100 + }, + "fr": { + "title": "Framerate", + "description": "Framerate in frames per second", + "type": "number", + "exclusiveMinimum": 0 + }, + "ip": { + "title": "In Point", + "description": "Frame the animation starts at (usually 0)", + "type": "number" + }, + "op": { + "title": "Out Point", + "description": "Frame the animation stops/loops at, which makes this the duration in frames when `ip` is 0", + "type": "number" + }, + "w": { + "title": "Width", + "description": "Width of the animation", + "type": "integer", + "minimum": 0 + }, + "h": { + "title": "Height", + "description": "Height of the animation", + "type": "integer", + "minimum": 0 + }, + "assets": { + "title": "Assets", + "type": "array", + "description": "List of assets that can be referenced by layers", + "items": { + "$ref": "#/$defs/assets/all-assets" + } + }, + "markers": { + "title": "Markers", + "description": "Markers defining named sections of the composition.", + "type": "array", + "items": { + "$ref": "#/$defs/helpers/marker" + } + }, + "slots": { + "title": "Slots", + "description": "Dictionary of slot ids that will replace matching properties.", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/helpers/slot" + } + } + }, + "required": [ + "w", + "h", + "fr", + "op", + "ip" + ] + }, + { + "$ref": "#/$defs/composition/composition" + } + ] + }, + "composition": { + "type": "object", + "title": "Composition", + "description": "An object that contains a list of layers", + "properties": { + "layers": { + "title": "Layers", + "type": "array", + "items": { + "$ref": "#/$defs/layers/all-layers" + } + } + }, + "required": [ + "layers" + ] + } + }, + "constants": { + "gradient-type": { + "type": "integer", + "title": "Gradient Type", + "description": "Whether a Gradient is a linear or radial.", + "oneOf": [ + { + "title": "Linear", + "description": "Colors transition in a single linear direction.", + "const": 1 + }, + { + "title": "Radial", + "description": "Colors transition outward from a center point.", + "const": 2 + } + ] + }, + "line-cap": { + "type": "integer", + "title": "Line Cap", + "description": "Style at the end of a stoked line", + "oneOf": [ + { + "title": "Butt", + "const": 1 + }, + { + "title": "Round", + "const": 2 + }, + { + "title": "Square", + "const": 3 + } + ] + }, + "fill-rule": { + "type": "integer", + "title": "Fill Rule", + "description": "Rule used to handle multiple shapes rendered with the same fill object", + "oneOf": [ + { + "title": "Non Zero", + "description": "Everything is colored (You can think of this as an OR)", + "const": 1 + }, + { + "title": "Even Odd", + "description": "Colored based on intersections and path direction, can be used to create \"holes\"", + "const": 2 + } + ] + }, + "matte-mode": { + "type": "integer", + "title": "Matte Mode", + "description": "How a layer should mask another layer", + "oneOf": [ + { + "title": "Normal", + "description": "The layer is not used as a track matte", + "const": 0 + }, + { + "title": "Alpha", + "description": "The masked layer opacity is modulated by the track matte layer opacity", + "const": 1 + }, + { + "title": "Inverted Alpha", + "description": "The masked layer opacity is modulated by the inverted track matte layer opacity", + "const": 2 + }, + { + "title": "Luma", + "description": "The masked layer opacity is modulated by the track matte layer luminance", + "const": 3 + }, + { + "title": "Inverted Luma", + "description": "The masked layer opacity is modulated by the inverted track matte layer luminance", + "const": 4 + } + ] + }, + "star-type": { + "type": "integer", + "title": "Star Type", + "description": "Whether a PolyStar is a star or a polygon", + "oneOf": [ + { + "title": "Star", + "const": 1 + }, + { + "title": "Polygon", + "const": 2 + } + ] + }, + "trim-multiple-shapes": { + "type": "integer", + "title": "Trim Multiple Shapes", + "description": "How to handle multiple shapes in trim path", + "oneOf": [ + { + "title": "Parallel", + "description": "All shapes apply the trim at the same time", + "const": 1 + }, + { + "title": "Sequential", + "description": "Shapes are considered as a continuous sequence", + "const": 2 + } + ] + }, + "mask-mode": { + "type": "string", + "title": "Mask Mode", + "description": "Describes how a mask interacts (blends) with the preceding masks in the stack.", + "oneOf": [ + { + "title": "None", + "const": "n", + "description": "The mask is ignored." + }, + { + "title": "Add", + "const": "a", + "description": "Mask coverage is added (Normal blending)." + }, + { + "title": "Subtract", + "const": "s", + "description": "Mask coverage is subtracted (Subtract blending)." + }, + { + "title": "Intersect", + "const": "i", + "description": "Mask coverage is intersected (Source-In blending)." + } + ] + }, + "line-join": { + "type": "integer", + "title": "Line Join", + "description": "Style at a sharp corner of a stoked line", + "oneOf": [ + { + "title": "Miter", + "const": 1 + }, + { + "title": "Round", + "const": 2 + }, + { + "title": "Bevel", + "const": 3 + } + ] + }, + "shape-direction": { + "type": "integer", + "title": "Shape Direction", + "description": "Drawing direction of the shape curve, useful for trim path", + "oneOf": [ + { + "title": "Normal", + "description": "Usually clockwise", + "const": 1 + }, + { + "title": "Reversed", + "description": "Usually counter clockwise", + "const": 3 + } + ] + }, + "stroke-dash-type": { + "type": "string", + "title": "Stroke Dash Type", + "description": "Type of a dash item in a stroked line", + "oneOf": [ + { + "title": "Dash", + "const": "d" + }, + { + "title": "Gap", + "const": "g" + }, + { + "title": "Offset", + "const": "o" + } + ] + } + }, + "helpers": { + "marker": { + "type": "object", + "title": "Marker", + "description": "Defines named portions of the composition.", + "properties": { + "cm": { + "title": "Comment", + "type": "string" + }, + "tm": { + "title": "Time", + "type": "number" + }, + "dr": { + "title": "Duration", + "type": "number" + } + } + }, + "slottable-object": { + "type": "object", + "title": "Slottable Object", + "description": "Object that may have its value replaced with a slot value", + "properties": { + "sid": { + "title": "Slot Id", + "description": "Identifier to look up the slot", + "type": "string" + } + } + }, + "transform": { + "type": "object", + "title": "Transform", + "description": "Layer transform", + "allOf": [ + { + "properties": { + "a": { + "title": "Anchor Point", + "description": "Anchor point: a position (relative to its parent) around which transformations are applied (ie: center for rotation / scale)", + "$ref": "#/$defs/properties/position-property" + }, + "p": { + "title": "Position", + "description": "Position / Translation", + "$ref": "#/$defs/properties/splittable-position-property" + }, + "r": { + "title": "Rotation", + "description": "Rotation in degrees, clockwise", + "$ref": "#/$defs/properties/scalar-property" + }, + "s": { + "title": "Scale", + "description": "Scale factor, `[100, 100]` for no scaling", + "$ref": "#/$defs/properties/vector-property" + }, + "o": { + "title": "Opacity", + "$ref": "#/$defs/properties/scalar-property" + }, + "sk": { + "title": "Skew", + "description": "Skew amount as an angle in degrees", + "$ref": "#/$defs/properties/scalar-property" + }, + "sa": { + "title": "Skew Axis", + "description": "Direction along which skew is applied, in degrees (`0` skews along the X axis, `90` along the Y axis)", + "$ref": "#/$defs/properties/scalar-property" + } + } + } + ] + }, + "mask": { + "type": "object", + "title": "Mask", + "description": "Mask for layer content.", + "allOf": [ + { + "properties": { + "mode": { + "title": "Mode", + "$ref": "#/$defs/constants/mask-mode", + "default": "i" + }, + "o": { + "title": "Opacity", + "description": "Mask opacity, as a percentage [0..100].", + "$ref": "#/$defs/properties/scalar-property", + "default": 100 + }, + "pt": { + "title": "Shape", + "description": "Mask shape", + "$ref": "#/$defs/properties/bezier-property" + } + }, + "required": [ + "pt" + ] + } + ] + }, + "slot": { + "type": "object", + "title": "Slot", + "description": "Defines a property value that will be set to all matched properties", + "properties": { + "p": { + "title": "Property Value", + "description": "Property Value" + } + }, + "required": [ + "p" + ] + }, + "slottable-property": { + "type": "object", + "title": "Slottable Property", + "description": "Property that may have its value replaced with a slot value", + "allOf": [ + { + "$ref": "#/$defs/helpers/slottable-object" + } + ], + "if": { + "required": [ + "sid" + ] + }, + "else": { + "required": [ + "a", + "k" + ] + } + }, + "visual-object": { + "type": "object", + "title": "Visual Object", + "description": "", + "allOf": [ + { + "type": "object", + "properties": { + "nm": { + "title": "Name", + "description": "Human readable name, as seen from editors and the like", + "type": "string" + } + }, + "required": [] + } + ] + } + }, + "layers": { + "solid-layer": { + "type": "object", + "title": "Solid Layer", + "description": "Solid color, rectangle-shaped layer", + "allOf": [ + { + "$ref": "#/$defs/layers/visual-layer" + }, + { + "type": "object", + "properties": { + "ty": { + "title": "Type", + "description": "Layer type", + "type": "integer", + "const": 1 + }, + "sw": { + "title": "Width", + "description": "Solid rectangle width", + "type": "integer" + }, + "sh": { + "title": "Height", + "description": "Solid rectangle height", + "type": "integer" + }, + "sc": { + "title": "Color", + "description": "Solid fill color", + "$ref": "#/$defs/values/hexcolor" + } + }, + "required": [ + "ty", + "sw", + "sh", + "sc" + ] + } + ] + }, + "image-layer": { + "type": "object", + "title": "Image Layer", + "description": "Layer containing an image", + "allOf": [ + { + "$ref": "#/$defs/layers/visual-layer" + }, + { + "type": "object", + "properties": { + "ty": { + "title": "Type", + "description": "Layer type", + "type": "integer", + "const": 2 + }, + "refId": { + "title": "Reference Id", + "description": "ID of the image as specified in the assets", + "type": "string" + } + }, + "required": [ + "ty", + "refId" + ] + } + ] + }, + "all-layers": { + "oneOf": [ + { + "$ref": "#/$defs/layers/precomposition-layer" + }, + { + "$ref": "#/$defs/layers/image-layer" + }, + { + "$ref": "#/$defs/layers/null-layer" + }, + { + "$ref": "#/$defs/layers/solid-layer" + }, + { + "$ref": "#/$defs/layers/shape-layer" + }, + { + "$ref": "#/$defs/layers/unknown-layer" + } + ] + }, + "precomposition-layer": { + "type": "object", + "title": "Precomposition Layer", + "description": "Layer that renders a Precomposition asset", + "allOf": [ + { + "$ref": "#/$defs/layers/visual-layer" + }, + { + "type": "object", + "properties": { + "ty": { + "title": "Type", + "description": "Layer type", + "type": "integer", + "const": 0 + }, + "refId": { + "title": "Reference Id", + "description": "ID of the precomp as specified in the assets", + "type": "string" + }, + "w": { + "title": "Width", + "description": "Width of the clipping rect", + "type": "integer" + }, + "h": { + "title": "Height", + "description": "Height of the clipping rect", + "type": "integer" + }, + "sr": { + "title": "Time Stretch", + "type": "number", + "default": 1 + }, + "st": { + "title": "Start Time", + "type": "number", + "default": 0 + }, + "tm": { + "title": "Time Remap", + "description": "Timeline remap function (frame index -> time in seconds)", + "$ref": "#/$defs/properties/scalar-property" + } + }, + "required": [ + "ty", + "refId" + ] + } + ] + }, + "null-layer": { + "type": "object", + "title": "Null Layer", + "description": "Layer with no data, useful to group layers together", + "allOf": [ + { + "$ref": "#/$defs/layers/visual-layer" + }, + { + "type": "object", + "properties": { + "ty": { + "title": "Type", + "description": "Layer type", + "type": "integer", + "const": 3 + } + }, + "required": [ + "ty" + ] + } + ] + }, + "shape-layer": { + "type": "object", + "title": "Shape Layer", + "description": "Layer containing Shapes", + "allOf": [ + { + "$ref": "#/$defs/layers/visual-layer" + }, + { + "type": "object", + "properties": { + "ty": { + "title": "Type", + "description": "Layer type", + "type": "integer", + "const": 4 + }, + "shapes": { + "title": "Shapes", + "type": "array", + "items": { + "$ref": "#/$defs/shapes/all-graphic-elements" + } + } + }, + "required": [ + "ty", + "shapes" + ] + } + ] + }, + "unknown-layer": { + "type": "object", + "title": "Unknown layer types", + "description": "Unknown layer types. Types not defined by the specification are still allowed.", + "properties": { + "ty": { + "not": { + "$comment": "enum list is dynamically generated", + "enum": [ + 0, + 2, + 3, + 1, + 4 + ] + } + } + } + }, + "visual-layer": { + "type": "object", + "title": "Visual Layer", + "description": "Layer used to affect visual elements", + "allOf": [ + { + "$ref": "#/$defs/layers/layer" + }, + { + "type": "object", + "properties": { + "ks": { + "title": "Transform", + "description": "Layer transform", + "$ref": "#/$defs/helpers/transform" + }, + "ao": { + "title": "Auto Orient", + "$ref": "#/$defs/values/int-boolean", + "default": 0, + "description": "If 1, the layer will rotate itself to match its animated position path" + }, + "tt": { + "title": "Matte Mode", + "$ref": "#/$defs/constants/matte-mode", + "description": "Defines the track matte mode for the layer" + }, + "tp": { + "title": "Matte Parent", + "type": "integer", + "description": "Index of the layer used as matte, if omitted assume the layer above the current one" + }, + "masksProperties": { + "title": "Masks", + "description": "Optional array of masks for the layer.", + "type": "array", + "items": { + "$ref": "#/$defs/helpers/mask" + } + } + }, + "required": [ + "ks" + ] + } + ] + }, + "layer": { + "type": "object", + "title": "Layer", + "description": "Common properties for all layers", + "allOf": [ + { + "$ref": "#/$defs/helpers/visual-object" + }, + { + "type": "object", + "properties": { + "hd": { + "title": "Hidden", + "description": "Whether the layer is hidden", + "type": "boolean" + }, + "ty": { + "title": "Type", + "description": "Layer Type", + "type": "integer" + }, + "ind": { + "title": "Index", + "type": "integer", + "description": "Index that can be used for parenting and referenced in expressions" + }, + "parent": { + "title": "Parent Index", + "description": "Must be the `ind` property of another layer", + "type": "integer" + }, + "ip": { + "title": "In Point", + "description": "Frame when the layer becomes visible", + "type": "number" + }, + "op": { + "title": "Out Point", + "description": "Frame when the layer becomes invisible", + "type": "number" + } + }, + "required": [ + "ty", + "ip", + "op" + ] + } + ] + } + }, + "properties": { + "vector-property": { + "type": "object", + "title": "Vector Property", + "description": "An animatable property that holds an array of numbers", + "allOf": [ + { + "$ref": "#/$defs/helpers/slottable-property" + } + ], + "oneOf": [ + { + "$comment": "Not animated", + "properties": { + "a": { + "title": "Animated", + "description": "Whether the property is animated", + "$ref": "#/$defs/values/int-boolean", + "const": 0 + }, + "k": { + "title": "Value", + "description": "Static Value", + "$ref": "#/$defs/values/vector" + } + } + }, + { + "$comment": "Animated", + "properties": { + "a": { + "title": "Animated", + "description": "Whether the property is animated", + "$ref": "#/$defs/values/int-boolean", + "const": 1 + }, + "k": { + "type": "array", + "title": "Keyframes", + "description": "Array of keyframes", + "items": { + "$ref": "#/$defs/properties/vector-keyframe" + } + } + } + } + ] + }, + "vector-keyframe": { + "type": "object", + "title": "Vector Keyframe", + "allOf": [ + { + "$ref": "#/$defs/properties/base-keyframe" + }, + { + "properties": { + "s": { + "title": "Value", + "description": "Value at this keyframe.", + "$ref": "#/$defs/values/vector" + } + } + } + ], + "required": [ + "s" + ] + }, + "gradient-keyframe": { + "type": "object", + "title": "Gradient Keyframe", + "allOf": [ + { + "$ref": "#/$defs/properties/base-keyframe" + }, + { + "properties": { + "s": { + "title": "Value", + "description": "Value at this keyframe.", + "$ref": "#/$defs/values/gradient" + } + } + } + ], + "required": [ + "s" + ] + }, + "bezier-property": { + "type": "object", + "title": "Bezier Property", + "description": "An animatable property that holds a Bezier shape", + "oneOf": [ + { + "$comment": "Not animated", + "properties": { + "a": { + "title": "Animated", + "description": "Whether the property is animated", + "$ref": "#/$defs/values/int-boolean", + "const": 0 + }, + "k": { + "title": "Value", + "description": "Static Value", + "$ref": "#/$defs/values/bezier" + } + } + }, + { + "$comment": "Animated", + "properties": { + "a": { + "title": "Animated", + "description": "Whether the property is animated", + "$ref": "#/$defs/values/int-boolean", + "const": 1 + }, + "k": { + "type": "array", + "title": "Keyframes", + "description": "Array of keyframes", + "items": { + "$ref": "#/$defs/properties/bezier-keyframe" + } + } + } + } + ], + "required": [ + "a", + "k" + ] + }, + "position-keyframe": { + "type": "object", + "title": "Position Keyframe", + "allOf": [ + { + "$ref": "#/$defs/properties/vector-keyframe" + }, + { + "properties": { + "ti": { + "title": "Value In Tangent", + "description": "Tangent for values (eg: moving position around a curved path)", + "$ref": "#/$defs/values/vector" + }, + "to": { + "title": "Value Out Tangent", + "description": "Tangent for values (eg: moving position around a curved path)", + "$ref": "#/$defs/values/vector" + } + } + } + ] + }, + "color-keyframe": { + "type": "object", + "title": "Color Keyframe", + "allOf": [ + { + "$ref": "#/$defs/properties/base-keyframe" + }, + { + "properties": { + "s": { + "title": "Value", + "description": "Value at this keyframe.", + "$ref": "#/$defs/values/color" + } + } + } + ], + "required": [ + "s" + ] + }, + "scalar-property": { + "type": "object", + "title": "Scalar Property", + "description": "An animatable property that holds a float", + "allOf": [ + { + "$ref": "#/$defs/helpers/slottable-property" + } + ], + "oneOf": [ + { + "$comment": "Not animated", + "properties": { + "a": { + "title": "Animated", + "description": "Whether the property is animated", + "$ref": "#/$defs/values/int-boolean", + "const": 0 + }, + "k": { + "title": "Value", + "description": "Static Value", + "type": "number" + } + } + }, + { + "$comment": "Animated", + "properties": { + "a": { + "title": "Animated", + "description": "Whether the property is animated", + "$ref": "#/$defs/values/int-boolean", + "const": 1 + }, + "k": { + "type": "array", + "title": "Keyframes", + "description": "Array of keyframes", + "items": { + "$ref": "#/$defs/properties/vector-keyframe" + } + } + } + } + ] + }, + "gradient-property": { + "type": "object", + "title": "Gradient Property", + "description": "An animatable property that holds a Gradient", + "properties": { + "p": { + "title": "Color stop count", + "type": "number" + }, + "k": { + "type": "object", + "title": "Gradient stops", + "description": "Animatable vector representing the gradient stops", + "oneOf": [ + { + "$comment": "Not animated", + "properties": { + "a": { + "title": "Animated", + "description": "Whether the property is animated", + "$ref": "#/$defs/values/int-boolean", + "const": 0 + }, + "k": { + "title": "Value", + "description": "Static Value", + "$ref": "#/$defs/values/gradient" + } + } + }, + { + "$comment": "Animated", + "properties": { + "a": { + "title": "Animated", + "description": "Whether the property is animated", + "$ref": "#/$defs/values/int-boolean", + "const": 1 + }, + "k": { + "type": "array", + "title": "Keyframes", + "description": "Array of keyframes", + "items": { + "$ref": "#/$defs/properties/gradient-keyframe" + } + } + } + } + ], + "required": [ + "a", + "k" + ] + } + } + }, + "easing-handle": { + "type": "object", + "title": "Keyframe Easing", + "description": "Bezier handle for keyframe interpolation", + "properties": { + "x": { + "title": "X", + "description": "Time component:\n0 means start time of the keyframe,\n1 means time of the next keyframe.", + "oneOf": [ + { + "type": "array", + "$ref": "#/$defs/values/vector", + "items": { + "type": "number", + "default": 0, + "minimum": 0, + "maximum": 1 + }, + "minItems": 1 + }, + { + "type": "number", + "default": 0, + "minimum": 0, + "maximum": 1 + } + ] + }, + "y": { + "title": "Y", + "description": "Value interpolation component:\n0 means start value of the keyframe,\n1 means value at the next keyframe.", + "oneOf": [ + { + "type": "array", + "$ref": "#/$defs/values/vector", + "items": { + "type": "number", + "default": 0 + }, + "minItems": 1 + }, + { + "type": "number", + "default": 0 + } + ] + } + }, + "required": [ + "x", + "y" + ] + }, + "base-keyframe": { + "type": "object", + "title": "Base Keyframe", + "description": "A Keyframes specifies the value at a specific time and the interpolation function to reach the next keyframe.", + "allOf": [ + { + "properties": { + "t": { + "title": "Time", + "description": "Frame number", + "type": "number", + "default": 0 + }, + "h": { + "title": "Hold", + "$ref": "#/$defs/values/int-boolean", + "default": 0 + }, + "i": { + "title": "In Tangent", + "description": "Easing tangent going into the next keyframe", + "$ref": "#/$defs/properties/easing-handle" + }, + "o": { + "title": "Out Tangent", + "description": "Easing tangent leaving the current keyframe", + "$ref": "#/$defs/properties/easing-handle" + } + } + } + ], + "required": [ + "t" + ] + }, + "split-position": { + "type": "object", + "title": "Split Position", + "description": "An animatable position where x and y are definied and animated separately.", + "properties": { + "s": { + "title": "Split", + "description": "Whether the position has split values", + "type": "boolean", + "const": true + }, + "x": { + "title": "X Position", + "description": "X Position", + "$ref": "#/$defs/properties/scalar-property" + }, + "y": { + "title": "Y Position", + "description": "Y Position", + "$ref": "#/$defs/properties/scalar-property" + } + }, + "required": [ + "s", + "x", + "y" + ] + }, + "bezier-keyframe": { + "type": "object", + "title": "Shape Keyframe", + "allOf": [ + { + "$ref": "#/$defs/properties/base-keyframe" + }, + { + "properties": { + "s": { + "title": "Value", + "description": "Value at this keyframe.", + "type": "array", + "items": { + "$ref": "#/$defs/values/bezier" + }, + "minItems": 1, + "maxItems": 1 + } + } + } + ], + "required": [ + "s" + ] + }, + "position-property": { + "type": "object", + "title": "Position Property", + "description": "An animatable property to represent a position in space", + "allOf": [ + { + "$ref": "#/$defs/helpers/slottable-property" + } + ], + "oneOf": [ + { + "$comment": "Not animated", + "properties": { + "a": { + "title": "Animated", + "description": "Whether the property is animated", + "$ref": "#/$defs/values/int-boolean", + "const": 0 + }, + "k": { + "title": "Value", + "description": "Static Value", + "$ref": "#/$defs/values/vector" + } + } + }, + { + "$comment": "Animated", + "properties": { + "a": { + "title": "Animated", + "description": "Whether the property is animated", + "$ref": "#/$defs/values/int-boolean", + "const": 1 + }, + "k": { + "type": "array", + "title": "Keyframes", + "description": "Array of keyframes", + "items": { + "$ref": "#/$defs/properties/position-keyframe" + } + } + } + } + ], + "required": [ + "a", + "k" + ] + }, + "color-property": { + "type": "object", + "title": "Color Property", + "description": "An animatable property that holds a Color", + "allOf": [ + { + "$ref": "#/$defs/helpers/slottable-property" + } + ], + "oneOf": [ + { + "$comment": "Not animated", + "properties": { + "a": { + "title": "Animated", + "description": "Whether the property is animated", + "$ref": "#/$defs/values/int-boolean", + "const": 0 + }, + "k": { + "title": "Value", + "description": "Static Value", + "$ref": "#/$defs/values/color" + } + } + }, + { + "$comment": "Animated", + "properties": { + "a": { + "title": "Animated", + "description": "Whether the property is animated", + "$ref": "#/$defs/values/int-boolean", + "const": 1 + }, + "k": { + "type": "array", + "title": "Keyframes", + "description": "Array of keyframes", + "items": { + "$ref": "#/$defs/properties/color-keyframe" + } + } + } + } + ] + }, + "splittable-position-property": { + "type": "object", + "title": "Splittable Position Property", + "description": "An animatable position where position values may be defined and animated separately.", + "oneOf": [ + { + "$comment": "Grouped XY position coordinates", + "$ref": "#/$defs/properties/position-property", + "properties": { + "s": { + "title": "Split", + "description": "Whether the position has split values", + "type": "boolean", + "const": false + } + } + }, + { + "$comment": "Split XY position coordinates", + "$ref": "#/$defs/properties/split-position" + } + ] + } + }, + "shapes": { + "all-graphic-elements": { + "$comment": "List of valid shapes", + "oneOf": [ + { + "$ref": "#/$defs/shapes/ellipse" + }, + { + "$ref": "#/$defs/shapes/fill" + }, + { + "$ref": "#/$defs/shapes/gradient-fill" + }, + { + "$ref": "#/$defs/shapes/gradient-stroke" + }, + { + "$ref": "#/$defs/shapes/group" + }, + { + "$ref": "#/$defs/shapes/path" + }, + { + "$ref": "#/$defs/shapes/polystar" + }, + { + "$ref": "#/$defs/shapes/rectangle" + }, + { + "$ref": "#/$defs/shapes/stroke" + }, + { + "$ref": "#/$defs/shapes/transform" + }, + { + "$ref": "#/$defs/shapes/trim-path" + }, + { + "$ref": "#/$defs/shapes/unknown-shape" + } + ] + }, + "polystar": { + "type": "object", + "title": "PolyStar", + "description": "Star or regular polygon", + "allOf": [ + { + "$ref": "#/$defs/shapes/shape" + }, + { + "type": "object", + "properties": { + "ty": { + "title": "Shape Type", + "type": "string", + "const": "sr" + }, + "p": { + "title": "Position", + "$ref": "#/$defs/properties/position-property" + }, + "or": { + "title": "Outer Radius", + "$ref": "#/$defs/properties/scalar-property" + }, + "os": { + "title": "Outer Roundness", + "description": "Outer Roundness as a percentage", + "$ref": "#/$defs/properties/scalar-property" + }, + "r": { + "title": "Rotation", + "description": "Rotation, clockwise in degrees", + "$ref": "#/$defs/properties/scalar-property" + }, + "pt": { + "title": "Points", + "$ref": "#/$defs/properties/scalar-property" + }, + "sy": { + "title": "Star Type", + "$ref": "#/$defs/constants/star-type", + "default": 1 + }, + "ir": { + "title": "Inner Radius", + "$ref": "#/$defs/properties/scalar-property" + }, + "is": { + "title": "Inner Roundness", + "description": "Inner Roundness as a percentage", + "$ref": "#/$defs/properties/scalar-property" + } + }, + "required": [ + "ty", + "or", + "os", + "pt", + "p", + "r" + ] + }, + { + "if": { + "properties": { + "sy": { + "const": 1 + } + } + }, + "then": { + "required": [ + "ir", + "is" + ] + } + } + ] + }, + "group": { + "type": "object", + "title": "Group", + "description": "Shape Element that can contain other shapes", + "allOf": [ + { + "$ref": "#/$defs/shapes/graphic-element" + }, + { + "type": "object", + "properties": { + "ty": { + "title": "Shape Type", + "type": "string", + "const": "gr" + }, + "np": { + "title": "Number Of Properties", + "type": "number" + }, + "it": { + "title": "Shapes", + "type": "array", + "items": { + "$ref": "#/$defs/shapes/all-graphic-elements" + } + } + }, + "required": [ + "ty" + ] + } + ] + }, + "path": { + "type": "object", + "title": "Path", + "description": "Custom Bezier shape", + "allOf": [ + { + "$ref": "#/$defs/shapes/shape" + }, + { + "type": "object", + "properties": { + "ty": { + "title": "Shape Type", + "type": "string", + "const": "sh" + }, + "ks": { + "title": "Shape", + "description": "Bezier path", + "$ref": "#/$defs/properties/bezier-property" + } + }, + "required": [ + "ty", + "ks" + ] + } + ] + }, + "shape-style": { + "type": "object", + "title": "Shape Style", + "description": "Describes the visual appearance (like fill and stroke) of neighbouring shapes", + "allOf": [ + { + "$ref": "#/$defs/shapes/graphic-element" + }, + { + "type": "object", + "properties": { + "o": { + "title": "Opacity", + "description": "Opacity, 100 means fully opaque", + "$ref": "#/$defs/properties/scalar-property" + } + }, + "required": [ + "o" + ] + } + ] + }, + "shape": { + "type": "object", + "title": "Shape", + "description": "Drawable shape, defines the actual shape but not the style", + "allOf": [ + { + "$ref": "#/$defs/shapes/graphic-element" + }, + { + "type": "object", + "properties": { + "d": { + "title": "Direction", + "description": "Direction the shape is drawn as, mostly relevant when using trim path", + "$ref": "#/$defs/constants/shape-direction" + } + } + } + ] + }, + "fill": { + "type": "object", + "title": "Fill", + "description": "Solid fill color", + "allOf": [ + { + "$ref": "#/$defs/shapes/shape-style" + }, + { + "type": "object", + "properties": { + "ty": { + "title": "Shape Type", + "type": "string", + "const": "fl" + }, + "c": { + "title": "Color", + "$ref": "#/$defs/properties/color-property" + }, + "r": { + "title": "Fill Rule", + "$ref": "#/$defs/constants/fill-rule" + } + }, + "required": [ + "ty", + "c" + ] + } + ] + }, + "gradient-stroke": { + "type": "object", + "title": "Gradient Stroke", + "description": "Gradient stroke", + "allOf": [ + { + "$ref": "#/$defs/shapes/shape-style" + }, + { + "$ref": "#/$defs/shapes/base-stroke" + }, + { + "$ref": "#/$defs/shapes/base-gradient" + }, + { + "type": "object", + "properties": { + "ty": { + "title": "Shape Type", + "type": "string", + "const": "gs" + } + }, + "required": [ + "ty" + ] + } + ] + }, + "trim-path": { + "type": "object", + "title": "Trim Path", + "description": "Trims shapes into a segment", + "allOf": [ + { + "$ref": "#/$defs/shapes/modifier" + }, + { + "type": "object", + "properties": { + "ty": { + "title": "Shape Type", + "type": "string", + "const": "tm" + }, + "s": { + "title": "Start", + "description": "Segment start", + "$ref": "#/$defs/properties/scalar-property" + }, + "e": { + "title": "End", + "description": "Segment end", + "$ref": "#/$defs/properties/scalar-property" + }, + "o": { + "title": "Offset", + "$ref": "#/$defs/properties/scalar-property" + }, + "m": { + "title": "Multiple", + "description": "How to treat multiple copies", + "$ref": "#/$defs/constants/trim-multiple-shapes" + } + }, + "required": [ + "ty", + "o", + "s", + "e" + ] + } + ] + }, + "transform": { + "type": "object", + "title": "Transform Shape", + "description": "Group transform", + "allOf": [ + { + "$ref": "#/$defs/shapes/graphic-element" + }, + { + "$ref": "#/$defs/helpers/transform" + }, + { + "type": "object", + "properties": { + "ty": { + "title": "Shape Type", + "type": "string", + "const": "tr" + } + }, + "required": [ + "ty" + ] + } + ] + }, + "rectangle": { + "type": "object", + "title": "Rectangle", + "description": "A simple rectangle shape", + "allOf": [ + { + "$ref": "#/$defs/shapes/shape" + }, + { + "type": "object", + "properties": { + "ty": { + "title": "Shape Type", + "type": "string", + "const": "rc" + }, + "p": { + "title": "Position", + "description": "Center of the rectangle", + "$ref": "#/$defs/properties/position-property" + }, + "s": { + "title": "Size", + "$ref": "#/$defs/properties/vector-property" + }, + "r": { + "title": "Rounded", + "description": "Rounded corners radius", + "$ref": "#/$defs/properties/scalar-property" + } + }, + "required": [ + "ty", + "s", + "p" + ] + } + ] + }, + "base-gradient": { + "type": "object", + "title": "Base Gradient", + "description": "Common properties for gradients", + "allOf": [ + { + "type": "object", + "properties": { + "g": { + "title": "Colors", + "description": "Gradient colors", + "$ref": "#/$defs/properties/gradient-property" + }, + "s": { + "title": "Start Point", + "description": "Starting point for the gradient", + "$ref": "#/$defs/properties/position-property" + }, + "e": { + "title": "End Point", + "description": "End point for the gradient", + "$ref": "#/$defs/properties/position-property" + }, + "t": { + "title": "Gradient Type", + "description": "Type of the gradient", + "$ref": "#/$defs/constants/gradient-type" + }, + "h": { + "title": "Highlight Length", + "description": "Highlight Length, as a percentage between `s` and `e`", + "$ref": "#/$defs/properties/scalar-property" + }, + "a": { + "title": "Highlight Angle", + "description": "Highlight Angle in clockwise degrees, relative to the direction from `s` to `e`", + "$ref": "#/$defs/properties/scalar-property" + } + }, + "required": [ + "s", + "e", + "g", + "t" + ] + } + ] + }, + "graphic-element": { + "type": "object", + "title": "Graphic Element", + "description": "Element used to display vector data in a shape layer", + "allOf": [ + { + "$ref": "#/$defs/helpers/visual-object" + }, + { + "type": "object", + "properties": { + "hd": { + "title": "Hidden", + "description": "Whether the shape is hidden", + "type": "boolean" + }, + "ty": { + "title": "Shape Type", + "type": "string" + } + }, + "required": [ + "ty" + ] + } + ] + }, + "stroke-dash": { + "type": "object", + "title": "Stroke Dash", + "description": "An item used to described the dash pattern in a stroked path", + "allOf": [ + { + "$ref": "#/$defs/helpers/visual-object" + }, + { + "type": "object", + "properties": { + "n": { + "title": "Dash Type", + "$ref": "#/$defs/constants/stroke-dash-type", + "default": "d" + }, + "v": { + "title": "Length", + "description": "Length of the dash", + "$ref": "#/$defs/properties/scalar-property" + } + }, + "required": [] + } + ] + }, + "gradient-fill": { + "type": "object", + "title": "Gradient", + "description": "Gradient fill color", + "allOf": [ + { + "$ref": "#/$defs/shapes/shape-style" + }, + { + "$ref": "#/$defs/shapes/base-gradient" + }, + { + "type": "object", + "properties": { + "ty": { + "title": "Shape Type", + "type": "string", + "const": "gf" + }, + "r": { + "title": "Fill Rule", + "$ref": "#/$defs/constants/fill-rule" + } + }, + "required": [ + "ty" + ] + } + ] + }, + "base-stroke": { + "type": "object", + "title": "Base Stroke", + "description": "Common properties for stroke styles", + "allOf": [ + { + "type": "object", + "properties": { + "lc": { + "title": "Line Cap", + "$ref": "#/$defs/constants/line-cap", + "default": 2 + }, + "lj": { + "title": "Line Join", + "$ref": "#/$defs/constants/line-join", + "default": 2 + }, + "ml": { + "title": "Miter Limit", + "type": "number", + "default": 0 + }, + "ml2": { + "title": "Miter Limit", + "description": "Animatable alternative to ml", + "$ref": "#/$defs/properties/scalar-property" + }, + "w": { + "title": "Width", + "description": "Stroke width", + "$ref": "#/$defs/properties/scalar-property" + }, + "d": { + "title": "Dashes", + "description": "Dashed line definition", + "type": "array", + "items": { + "$ref": "#/$defs/shapes/stroke-dash" + } + } + }, + "required": [ + "w" + ] + } + ] + }, + "unknown-shape": { + "type": "object", + "title": "Unknown shape types", + "description": "Unknown shape types. Types not defined by the specification are still allowed.", + "properties": { + "ty": { + "not": { + "$comment": "enum list is dynamically generated", + "enum": [ + "el", + "fl", + "gf", + "gs", + "gr", + "sh", + "sr", + "rc", + "st", + "tr", + "tm" + ] + } + } + } + }, + "ellipse": { + "type": "object", + "title": "Ellipse", + "description": "Ellipse shape", + "allOf": [ + { + "$ref": "#/$defs/shapes/shape" + }, + { + "type": "object", + "properties": { + "ty": { + "title": "Shape Type", + "type": "string", + "const": "el" + }, + "p": { + "title": "Position", + "$ref": "#/$defs/properties/position-property" + }, + "s": { + "title": "Size", + "$ref": "#/$defs/properties/vector-property" + } + }, + "required": [ + "ty", + "s", + "p" + ] + } + ] + }, + "modifier": { + "type": "object", + "title": "Modifier", + "description": "Modifiers change the bezier curves of neighbouring shapes", + "allOf": [ + { + "$ref": "#/$defs/shapes/graphic-element" + } + ] + }, + "stroke": { + "type": "object", + "title": "Stroke", + "description": "Solid stroke", + "allOf": [ + { + "$ref": "#/$defs/shapes/shape-style" + }, + { + "$ref": "#/$defs/shapes/base-stroke" + }, + { + "type": "object", + "properties": { + "ty": { + "title": "Shape Type", + "type": "string", + "const": "st" + }, + "c": { + "title": "Color", + "description": "Stroke color", + "$ref": "#/$defs/properties/color-property" + } + }, + "required": [ + "ty", + "c" + ] + } + ] + } + }, + "values": { + "hexcolor": { + "type": "string", + "title": "Hex Color", + "description": "Color value in hexadecimal format, with two digits per component ('#RRGGBB')", + "pattern": "^#([a-fA-F0-9]{6})$", + "examples": [ + "#FF00AA" + ] + }, + "bezier": { + "type": "object", + "title": "Bezier", + "description": "Cubic polybezier", + "properties": { + "c": { + "title": "Closed", + "type": "boolean", + "default": false + }, + "i": { + "title": "In Tangents", + "type": "array", + "description": "Array of points, each point is an array of coordinates.\nThese points are along the `in` tangents relative to the corresponding `v`.", + "items": { + "$ref": "#/$defs/values/vector", + "default": [] + } + }, + "o": { + "title": "Out Tangents", + "type": "array", + "description": "Array of points, each point is an array of coordinates.\nThese points are along the `out` tangents relative to the corresponding `v`.", + "items": { + "$ref": "#/$defs/values/vector", + "default": [] + } + }, + "v": { + "title": "Vertices", + "description": "Array of points, each point is an array of coordinates.\nThese points are along the bezier path", + "type": "array", + "items": { + "$ref": "#/$defs/values/vector", + "default": [] + } + } + }, + "required": [ + "i", + "v", + "o" + ] + }, + "data-url": { + "type": "string", + "title": "Data URL", + "description": "An embedded data object", + "pattern": "^data:([\\w/]+)(;base64)?,(.+)$" + }, + "color": { + "type": "array", + "title": "Color", + "description": "Color as a [r, g, b] array with values in [0, 1]", + "items": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "minItems": 3, + "maxItems": 4 + }, + "int-boolean": { + "type": "integer", + "title": "Integer Boolean", + "description": "Represents boolean values as an integer. `0` is false, `1` is true.", + "default": 0, + "examples": [ + 0 + ], + "enum": [ + 0, + 1 + ], + "oneOf": [ + { + "title": "True", + "const": 1 + }, + { + "title": "False", + "const": 0 + } + ] + }, + "vector": { + "type": "array", + "title": "Vector", + "description": "An array of numbers", + "items": { + "type": "number" + } + }, + "gradient": { + "type": "array", + "title": "Gradient", + "description": "A flat list of color stops followed by optional transparency stops. A color stop is [offset, red, green, blue]. A transparency stop is [offset, transparency]. All values are between 0 and 1", + "items": { + "type": "number", + "minimum": 0, + "maximum": 1 + } + } + } + } +} \ No newline at end of file diff --git a/src/components/footer.js b/src/components/footer.js index 4d855bd..0ccc741 100644 --- a/src/components/footer.js +++ b/src/components/footer.js @@ -68,6 +68,11 @@ export const Footer = () => { {ROUTES.roadmap.text} + + + {ROUTES.validator.text} + + {ROUTES.community.text} diff --git a/src/components/header.js b/src/components/header.js index a9cf11d..6970966 100644 --- a/src/components/header.js +++ b/src/components/header.js @@ -80,6 +80,15 @@ export const Header = () => { {ROUTES.roadmap.text} + + + {ROUTES.validator.text} + + {ROUTES.community.text} diff --git a/src/constants/index.js b/src/constants/index.js index 1835f38..b0fb473 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -28,4 +28,8 @@ export const ROUTES = { route: "/compliance-buttons", text: "Compliance Buttons", }, + validator: { + route: "/validator", + text: "Validator", + }, } diff --git a/src/pages/validator.js b/src/pages/validator.js new file mode 100644 index 0000000..c596bcb --- /dev/null +++ b/src/pages/validator.js @@ -0,0 +1,363 @@ +import * as React from "react" +import { useState, useRef, useEffect } from "react" + +import Ajv from "ajv/dist/2020" +import lottieSchema from "../assets/lottie.schema.json" + +import Container from "react-bootstrap/Container" +import Row from "react-bootstrap/Row" +import Col from "react-bootstrap/Col" +import Tabs from "react-bootstrap/Tabs" +import Tab from "react-bootstrap/Tab" +import Form from "react-bootstrap/Form" +import Table from "react-bootstrap/Table" +import Button from "react-bootstrap/Button" +import ButtonGroup from "react-bootstrap/ButtonGroup" +import FloatingLabel from "react-bootstrap/FloatingLabel" +import Spinner from "react-bootstrap/Spinner" +import Alert from "react-bootstrap/Alert" + +import Layout from "../components/layout" +import Seo from "../components/seo" + +import { isValidUrl } from "../utils/helpers" +import { Validator } from "../utils/validator" + +const content = { + title: "Lottie Validator", + description: + "Validates a given Lottie JSON file against the Lottie specification version", +} + +const tabCssClass = "border border-top-0 rounded-bottom p-4 shadow" + +const ValidatorPage = () => { + // refs + + const lottieFileInputRef = useRef(null) + const lottieTextInputRef = useRef(null) + + // states + + const [validator, setValidator] = useState(null) + + const [loading, setLoading] = useState(false) + + const [currentTab, setCurrentTab] = useState("url") + + const [errorMessage, setErrorMessage] = useState("") + + const [lottieUrl, setLottieUrl] = useState("") + const [lottieFile, setLottieFile] = useState(null) + const [lottieText, setLottieText] = useState("") + + const [lottie, setLottie] = useState("") + const [validationResult, setValidationResult] = useState([]) + const [warnUnknownProps, setWarnUnknownProps] = useState(false) + const [warnUnknownObjTypes, setWarnUnknownObjTypes] = useState(true) + + // handlers + + const validateLottieUrl = () => { + if (!lottieUrl) { + setErrorMessage("Lottie URL cannot be empty") + return + } + + if (!isValidUrl(lottieUrl)) { + setErrorMessage("Invalid Lottie URL") + return + } + + setLoading(true) + + fetch(lottieUrl) + .then(result => result.text()) + .then(setLottie) + .catch(e => + setErrorMessage(`Could not load Lottie file from URL: ${e.message}`) + ) + .finally(() => setLoading(false)) + } + + const validateLottieFile = () => { + if (!lottieFile) { + setErrorMessage("Lottie File cannot be empty") + return + } + + setLoading(true) + + const reader = new FileReader() + reader.onload = e => setLottie(e.target.result) + reader.onerror = _e => setErrorMessage("Could not load file") + reader.readAsText(lottieFile) + + setLoading(false) + } + + const validateLottieText = () => { + if (!lottieText) { + setErrorMessage("Lottie text cannot be empty") + return + } + + if (typeof lottieText !== "string") { + setErrorMessage("Lottie text must be a string") + return + } + + setLottie(lottieText) + } + + // ui handlers + + const onTabsSelect = key => setCurrentTab(key) + const onWarnUnknownObjTypesChange = checked => setWarnUnknownObjTypes(checked) + const onWarnUnknownPropsChange = checked => setWarnUnknownProps(checked) + + const resetStates = () => { + setLottie("") + setValidationResult([]) + setLottieUrl("") + setLottieFile(null) + setLottieText("") + setErrorMessage("") + if (lottieFileInputRef.current) lottieFileInputRef.current.value = "" + if (lottieTextInputRef.current) lottieTextInputRef.current.value = "" + } + + const onValidateBtnClick = () => { + setErrorMessage("") + + switch (currentTab) { + case "url": + validateLottieUrl() + break + case "file": + validateLottieFile() + break + case "text": + validateLottieText() + break + default: + break + } + } + + // render + + const renderTableRows = errors => { + return errors.map((err, index) => { + const { path, path_names, type, message, name, docs } = err + + const docsUrl = docs + ? `https://lottie.github.io/lottie-spec/latest/${docs}` + : "" + + const trClass = + type === "warning" + ? "table-warning" + : type === "error" + ? "table-danger" + : "table-primary" + + const namedPath = path_names + ? path_names.map(n => n ?? "(unnamed)").join(" > ") + : "" + + return ( + + {path || ""} + {namedPath || ""} + {type || ""} + {message || ""} + + {docs && ( + + {name} + + )} + + + ) + }) + } + + // effects + + useEffect(() => { + setLoading(true) + setValidator(new Validator(Ajv.Ajv2020, lottieSchema)) + setLoading(false) + + return () => { + setValidator(null) + } + }, []) + + useEffect(() => { + if (!lottie || !validator) return + + setLoading(true) + + try { + const result = validator.validate(lottie) + const finalResult = [] + + let hasError = false + + result.forEach(item => { + if (!warnUnknownProps && item.warning === "property") return + if (!warnUnknownObjTypes && item.warning === "type") return + if (!hasError && item.type === "error") hasError = true + + finalResult.push(item) + }) + + if (!hasError) { + finalResult.unshift({ + type: "success", + message: "Lottie JSON is valid", + }) + } + + setValidationResult(finalResult) + } catch (e) { + setErrorMessage(e.message) + } + + setLoading(false) + }, [validator, lottie, warnUnknownProps, warnUnknownObjTypes]) + + return ( + +
+ + + +

{content.title}

+
{content.description}
+ +
+
+
+ + + + + + setLottieUrl(e.target.value)} + /> + + + + setLottieFile(e.target.files[0])} + /> + + + + + setLottieText(e.target.value)} + /> + + + + + +
Options
+
+ + onWarnUnknownObjTypesChange(e.currentTarget.checked) + } + /> + + onWarnUnknownPropsChange(e.currentTarget.checked) + } + /> +
+ + + + + +
+ + + {loading && ( +
+ + Loading... + +
+ )} + {!loading && errorMessage && ( + {errorMessage} + )} + {!loading && validationResult.length > 0 && ( + + + + + + + + + + + {renderTableRows(validationResult)} +
PathNamed PathSeverityMessageDocs
+ )} + +
+
+
+ ) +} + +export const Head = () => ( + +) + +export default ValidatorPage diff --git a/src/styles/bootstrap-variables.scss b/src/styles/bootstrap-variables.scss index e9d5c55..2fa531f 100644 --- a/src/styles/bootstrap-variables.scss +++ b/src/styles/bootstrap-variables.scss @@ -1,8 +1,10 @@ $primary: #006a5f; $primary-bg-subtle: #ebfbfa; +$danger: #dc3545; $theme-colors: ( "primary": $primary, + "danger": $danger, ); $headings-font-weight: 700; diff --git a/src/styles/bootstrap.scss b/src/styles/bootstrap.scss index e8e1006..a6dfea9 100644 --- a/src/styles/bootstrap.scss +++ b/src/styles/bootstrap.scss @@ -23,12 +23,12 @@ @import "bootstrap/scss/containers"; @import "bootstrap/scss/grid"; -@import 'bootstrap/scss/tables'; -// @import 'bootstrap/scss/forms'; +@import "bootstrap/scss/tables"; +@import "bootstrap/scss/forms"; @import "bootstrap/scss/buttons"; @import "bootstrap/scss/transitions"; @import "bootstrap/scss/dropdown"; -// @import 'bootstrap/scss/button-group'; +@import "bootstrap/scss/button-group"; @import "bootstrap/scss/nav"; @import "bootstrap/scss/navbar"; @import "bootstrap/scss/card"; @@ -36,7 +36,7 @@ // @import 'bootstrap/scss/breadcrumb'; // @import "bootstrap/scss/pagination"; // @import 'bootstrap/scss/badge'; -// @import "bootstrap/scss/alert"; +@import "bootstrap/scss/alert"; // @import "bootstrap/scss/progress"; // @import 'bootstrap/scss/list-group'; // @import 'bootstrap/scss/close'; @@ -45,7 +45,7 @@ // @import 'bootstrap/scss/tooltip'; // @import "bootstrap/scss/popover"; // @import "bootstrap/scss/carousel"; -// @import 'bootstrap/scss/spinners'; +@import "bootstrap/scss/spinners"; // @import 'bootstrap/scss/offcanvas'; // @import "bootstrap/scss/placeholders"; diff --git a/src/utils/helpers.js b/src/utils/helpers.js new file mode 100644 index 0000000..d047172 --- /dev/null +++ b/src/utils/helpers.js @@ -0,0 +1,8 @@ +export const isValidUrl = str => { + try { + new URL(str) + return true + } catch (_) { + return false + } +} diff --git a/src/utils/validator.js b/src/utils/validator.js new file mode 100644 index 0000000..2a3fed5 --- /dev/null +++ b/src/utils/validator.js @@ -0,0 +1,546 @@ +function extract_schema_ty(schema) { + if (!schema) return + + if ("properties" in schema) { + let ty_prop = schema.properties.ty + if (!ty_prop) return + return ty_prop.const + } + + for (let prop of ["oneOf", "anyOf", "allOf"]) { + if (schema[prop]) { + for (let sub_schema of schema[prop]) { + let ty = extract_schema_ty(sub_schema) + if (ty !== undefined) return ty + } + } + } +} + +function patch_docs_links(schema, url, name, docs_name, within_properties) { + if (typeof schema === "object") { + if (Array.isArray(schema)) { + for (let item of schema) patch_docs_links(item, url, name, docs_name) + } else { + for (let [pname, val] of Object.entries(schema)) { + var sub_name = name + if (within_properties) sub_name += "." + pname + + patch_docs_links(val, url, sub_name, docs_name, pname === "properties") + } + + if (!within_properties) { + schema._docs = url + schema._docs_name = docs_name + schema._name = name + } + } + } +} + +class PropertyList { + constructor(schema) { + this.properties = new Set() + this.references = new Set() + this.schema = schema + this.resolved = false + this.skip = false + } + + valid() { + return !this.skip && (this.properties.size > 0 || this.references.size > 1) + } +} + +class PropertyMap { + constructor() { + this.map = new Map() + this.all_references = new Set() + } + + create(id, schema) { + var map = new PropertyList(schema) + this.map.set(id, map) + return map + } + + finalize() { + for (let [name, prop_list] of this.map) { + if (prop_list.valid() && !this.all_references.has(name)) + prop_list.schema.warn_extra_props = this._get_all_props(prop_list) + } + } + + _get_all_props(prop_list) { + if (!prop_list.resolved) { + prop_list.resolved = true + for (let ref of prop_list.references) + for (let prop of this.get_all_props(ref)) prop_list.properties.add(prop) + } + + return prop_list.properties + } + + get_all_props(id) { + return this._get_all_props(this.map.get(id)) + } + + extract_all_properties(schema, id, prop_list, referencing_base) { + if (typeof schema !== "object" || schema === null) return + + if (Array.isArray(schema)) { + for (let i = 0; i < schema.length; i++) + this.extract_all_properties(schema[i], id + `/${i}`, prop_list, false) + + return + } + + for (let [name, sub_schema] of Object.entries(schema)) { + if (name === "properties") { + for (let [prop_name, prop] of Object.entries(sub_schema)) { + prop_list.properties.add(prop_name) + let prop_id = id + "/properties/" + prop_name + this.extract_all_properties( + prop, + prop_id, + this.create(prop_id, prop), + false + ) + } + } else if (name === "oneOf") { + for (let i = 0; i < sub_schema.length; i++) { + let oneof_id = id + "/oneOf/" + i + let oneof_schema = sub_schema[i] + let oneof_list = id.endsWith("-property") + ? prop_list + : this.create(oneof_id, oneof_schema) + this.extract_all_properties(oneof_schema, oneof_id, oneof_list, false) + } + } else if (name === "allOf") { + for (let i = 0; i < sub_schema.length; i++) { + let oneof_id = id + "/allOf/" + i + let oneof_schema = sub_schema[i] + this.extract_all_properties(oneof_schema, oneof_id, prop_list, true) + } + } else if (name === "additionalProperties") { + prop_list.skip = true + } else if (name === "$ref") { + prop_list.references.add(sub_schema) + if (referencing_base) this.all_references.add(sub_schema) + } else if (name !== "not") { + this.extract_all_properties( + sub_schema, + id + "/" + name, + prop_list, + false + ) + } + } + } +} + +function kebab_to_title(kebab) { + return kebab + .split("-") + .map( + chunk => chunk.charAt(0).toUpperCase() + chunk.substring(1).toLowerCase() + ) + .join(" ") +} + +function custom_discriminator( + propname, + fail_unknown, + default_value = undefined +) { + function validate_fn(schema, data, parent_schema, data_cxt) { + var value = data[propname] + + // Error will be generated by required + if (value === undefined) { + if (default_value === undefined) return true + value = default_value + } + + var sub_schema = schema[value] + if (sub_schema === undefined) { + validate_fn.errors = [ + { + message: `has unknown '${propname}' value ` + JSON.stringify(value), + type: fail_unknown ? "error" : "warning", + warning: "type", + instancePath: data_cxt.instancePath, + parentSchema: parent_schema, + }, + ] + return false + } + + var validate = this.getSchema(sub_schema.id) + if (!validate(data, data_cxt)) { + validate_fn.errors = validate.errors + return false + } + return true + } + + return validate_fn +} + +function patch_schema_enum(schema) { + if ("oneOf" in schema) { + schema.enum_oneof = schema.oneOf + delete schema.oneOf + } +} + +function keyframe_has_t(kf) { + return typeof kf === "object" && typeof kf.t === "number" +} + +export class Validator { + constructor(AjvClass, schema_json, docs_url = "") { + this.schema = schema_json + this.defs = this.schema["$defs"] + var prop_map = new PropertyMap() + + for (let [cat, sub_schemas] of Object.entries(this.defs)) { + let cat_docs = `${docs_url}/specs/${cat}/` + let cat_name = kebab_to_title(cat.replace(/s$/, "")) + for (let [obj, sub_schema] of Object.entries(sub_schemas)) { + let obj_docs = cat_docs + let obj_name = cat_name + if (sub_schema.type && obj !== "base-gradient") { + obj_docs += "#" + obj + obj_name = sub_schema.title || kebab_to_title(obj) + } + patch_docs_links(sub_schema, obj_docs, obj_name, obj_name) + + let id = `#/$defs/${cat}/${obj}` + prop_map.extract_all_properties( + sub_schema, + id, + prop_map.create(id, sub_schema), + false + ) + } + } + let schema_id = this.schema["$id"] + this._patch_ty_schema(schema_id, "layers", "all-layers") + this._patch_ty_schema(schema_id, "shapes", "all-graphic-elements") + for (let [pname, pschema] of Object.entries(this.defs.properties)) { + if (pname.endsWith("-property")) + this._patch_property_schema( + pschema, + schema_id + "#/$defs/properties/" + pname + ) + } + this.defs.properties["base-keyframe"].keyframe = true + + for (let enum_schema of Object.values(this.defs.constants)) + patch_schema_enum(enum_schema) + + this.defs.assets["all-assets"] = { + type: "object", + asset_oneof: schema_id, + } + + for (let layer_type of ["image-layer", "precomposition-layer"]) { + let layer_schema = this.defs.layers[layer_type] + layer_schema.allOf[1].properties.refId.reference_asset = true + } + + prop_map.finalize() + + this.validator = new AjvClass({ + allErrors: true, + verbose: true, + // inlineRefs: false, + // strict: false, + keywords: [ + { keyword: ["_docs", "_name", "_docs_name", "$version"] }, + { + keyword: "ty_oneof", + validate: custom_discriminator("ty", false), + }, + { + keyword: "prop_oneof", + validate: custom_discriminator("a", true), + }, + { + keyword: "asset_oneof", + validate: function validate_asset( + schema, + data, + parent_schema, + data_cxt + ) { + validate_asset.errors = [] + + if (typeof data !== "object" || data === null) return true + + var target_schema + + if ("layers" in data) + target_schema = this.getSchema( + schema + "#/$defs/assets/precomposition" + ) + else target_schema = this.getSchema(schema + "#/$defs/assets/image") + + if (!target_schema(data, data_cxt)) { + validate_asset.errors = target_schema.errors + return false + } + return true + }, + }, + { + keyword: "splitpos_oneof", + validate: custom_discriminator("s", false, false), + }, + { + keyword: "keyframe", + validate: function validate_keyframe( + schema, + data, + parent_schema, + data_cxt + ) { + validate_keyframe.errors = [] + + var require_io = true + if (data.h) require_io = false + + var index = data_cxt.parentData.indexOf(data) + if (index === data_cxt.parentData.length - 1) require_io = false + + if (require_io) { + for (var prop of "io") { + if (!("i" in data)) { + validate_keyframe.errors.push({ + message: `must have required property 'i'`, + type: "error", + instancePath: data_cxt.instancePath, + parentSchema: parent_schema, + }) + } + } + } + + if (index > 0) { + var prev_kf = data_cxt.parentData[index - 1] + if (keyframe_has_t(prev_kf) && typeof data.t === "number") { + if (data.t < prev_kf.t) { + validate_keyframe.errors.push({ + message: `keyframe 't' must be in ascending order`, + type: "error", + instancePath: data_cxt.instancePath, + parentSchema: parent_schema, + }) + } else if (data.t === prev_kf.t && index > 1) { + var prev_prev = data_cxt.parentData[index - 2] + if (keyframe_has_t(prev_prev) && data.t === prev_prev.t) { + validate_keyframe.errors.push({ + message: `there can be at most 2 keyframes with the same 't' value`, + type: "error", + instancePath: data_cxt.instancePath, + parentSchema: parent_schema, + }) + } + } + } + } + + return validate_keyframe.errors.length === 0 + }, + }, + { + keyword: "enum_oneof", + validate: function validate_enum( + schema, + data, + parent_schema, + data_cxt + ) { + validate_enum.errors = [] + for (let value of schema) if (value.const === data) return true + + validate_enum.errors.push({ + message: `${data} is not a valid enumeration value`, + type: "error", + instancePath: data_cxt.instancePath, + parentSchema: parent_schema, + }) + return false + }, + }, + { + keyword: "reference_asset", + validate: function validate_asset_reference( + schema, + data, + parent_schema, + data_ctx + ) { + validate_asset_reference.errors = [] + + if (Array.isArray(data_ctx.rootData.assets)) { + for (let asset of data_ctx.rootData.assets) { + if (asset.id === data) { + // TODO: Validate asset type? + return true + } + } + } + + validate_asset_reference.errors.push({ + message: `${JSON.stringify(data)} is not a valid asset id`, + type: "error", + instancePath: data_ctx.instancePath, + parentSchema: parent_schema, + }) + return false + }, + }, + { + keyword: "warn_extra_props", + validate: function warn_extra_props( + schema, + data, + parent_schema, + data_cxt + ) { + warn_extra_props.errors = [] + + if (typeof data !== "object" || data === null) return true + + for (let prop of Object.keys(data)) { + if (!schema.has(prop)) { + warn_extra_props.errors.push({ + message: `has unknown property '${prop}'`, + type: "warning", + warning: "property", + instancePath: data_cxt.instancePath, + parentSchema: parent_schema, + }) + } + } + + return warn_extra_props.errors.length === 0 + }, + }, + ], + schemas: [this.schema], + }) + this._validate_internal = this.validator.getSchema(schema_id) + } + + _patch_ty_schema(id_base, category, all) { + let found = {} + for (let [name, sub_schema] of Object.entries(this.defs[category])) { + let ty = extract_schema_ty(sub_schema) + if (ty !== undefined) { + let id = `${id_base}#/$defs/${category}/${name}` + found[ty] = { + id: id, + } + } + } + this.defs[category][all].ty_oneof = found + delete this.defs[category][all].oneOf + + return found + } + + _patch_property_schema(schema, id) { + if (id.endsWith("gradient-property")) { + return this._patch_property_schema( + schema.properties.k, + id + "/properties/k" + ) + } + + if (id.endsWith("splittable-position-property")) { + delete schema.oneOf + schema.splitpos_oneof = { + [true]: { + id: this.schema["$id"] + "#/$defs/properties/split-position", + }, + [false]: { + id: this.schema["$id"] + "#/$defs/properties/position-property", + }, + } + + return + } + + schema.prop_oneof = [] + for (let opt of schema.oneOf || []) { + schema.prop_oneof.push({ + schema: { + type: "object", + ...opt, + }, + id: id + "/prop_oneof/" + schema.prop_oneof.length + "/schema", + }) + } + delete schema.oneOf + } + + validate(string) { + var data + try { + data = JSON.parse(string) + } catch (e) { + return [ + { + type: "error", + message: "Document is not a valid JSON file", + }, + { + type: "error", + message: e.message, + }, + ] + } + + let errors = [] + if (!this._validate_internal(data)) + errors = this._validate_internal.errors.map(e => + this._cleaned_error(e, data) + ) + + return errors.sort((a, b) => { + if (a.path < b.path) return -1 + if (a.path > b.path) return 1 + return 0 + }) + } + + _cleaned_error(error, data, prefix = "") { + const path_parts = error.instancePath.split("/") + + const path_names = [] + for (const path_part of path_parts) { + if (path_part === "#" || path_part === "") continue + + data = data[path_part] + + if (!data) break + + // Every layer with a type may be named + // Push a null value if it doesn't exist so display code can handle + if (data.ty) path_names.push(data.nm) + } + + return { + type: error.type ?? "error", + warning: error.warning, + message: (error.parentSchema?._name ?? "Value") + " " + error.message, + path: prefix + (error.instancePath ?? ""), + name: error.parentSchema?._docs_name ?? "Value", + docs: error.parentSchema?._docs, + path_names, + } + } +}