From 6d30bceb3c72ab571661eb9fd5cee00359865f12 Mon Sep 17 00:00:00 2001 From: aidosmf Date: Fri, 13 Sep 2024 10:13:04 +0500 Subject: [PATCH 01/24] feat(pages): validator --- src/pages/validator.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/pages/validator.js diff --git a/src/pages/validator.js b/src/pages/validator.js new file mode 100644 index 0000000..f586b50 --- /dev/null +++ b/src/pages/validator.js @@ -0,0 +1,37 @@ +import * as React from "react" + +import Container from "react-bootstrap/Container" +import Row from "react-bootstrap/Row" +import Col from "react-bootstrap/Col" + +import Layout from "../components/layout" +import Seo from "../components/seo" + +const content = { + title: "Lottie Validator", + description: + "Validates a given Lottie JSON file against the Lottie specification version.", +} + +const ValidatorPage = () => { + return ( + +
+ + + +

{content.title}

+
{content.description}
+ +
+
+
+
+ ) +} + +export const Head = () => ( + +) + +export default ValidatorPage From 89ab89c2479b3eae2a78b3b1f1b632ae0a5c08ba Mon Sep 17 00:00:00 2001 From: aidosmf Date: Fri, 13 Sep 2024 10:20:13 +0500 Subject: [PATCH 02/24] feat: add validator link to header & footer --- src/components/footer.js | 5 +++++ src/components/header.js | 9 +++++++++ src/constants/index.js | 4 ++++ 3 files changed, 18 insertions(+) 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", + }, } From 72ebaeb7f2f346fb33ba521fc386498d9df36532 Mon Sep 17 00:00:00 2001 From: aidosmf Date: Fri, 13 Sep 2024 10:26:43 +0500 Subject: [PATCH 03/24] chore: add ajv deps for validator --- package-lock.json | 327 ++++++++++++++++++++++++++++++++++++---------- package.json | 1 + 2 files changed, 256 insertions(+), 72 deletions(-) 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", From b23373b6dcb5c76c13a807d17bdf7f61edab212f Mon Sep 17 00:00:00 2001 From: aidosmf Date: Fri, 13 Sep 2024 11:55:40 +0500 Subject: [PATCH 04/24] feat: add tabs default ui --- src/pages/validator.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/pages/validator.js b/src/pages/validator.js index f586b50..d315992 100644 --- a/src/pages/validator.js +++ b/src/pages/validator.js @@ -3,6 +3,8 @@ import * as React from "react" 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 Layout from "../components/layout" import Seo from "../components/seo" @@ -13,6 +15,8 @@ const content = { "Validates a given Lottie JSON file against the Lottie specification version.", } +const tabCssClass = "border border-top-0 rounded-bottom p-3 shadow" + const ValidatorPage = () => { return ( @@ -26,6 +30,26 @@ const ValidatorPage = () => { + + + + + + Tab content for url + + + Tab content for file + + + Tab content for Text + + + Tab content for Options + + + + + ) } From 9ba7bd7b7335a9f81b71668abd41851b8a0be717 Mon Sep 17 00:00:00 2001 From: aidosmf Date: Fri, 13 Sep 2024 12:29:48 +0500 Subject: [PATCH 05/24] feat: add checkboxes & mv options outside ui --- src/pages/validator.js | 31 ++++++++++++++++++++++++------- src/styles/bootstrap.scss | 4 ++-- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/pages/validator.js b/src/pages/validator.js index d315992..4996bba 100644 --- a/src/pages/validator.js +++ b/src/pages/validator.js @@ -5,6 +5,7 @@ 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 Layout from "../components/layout" import Seo from "../components/seo" @@ -32,22 +33,38 @@ const ValidatorPage = () => { - + - + Tab content for url - + Tab content for file - + Tab content for Text - - Tab content for Options - + +
Options
+
+ + +
+
diff --git a/src/styles/bootstrap.scss b/src/styles/bootstrap.scss index e8e1006..278cc2c 100644 --- a/src/styles/bootstrap.scss +++ b/src/styles/bootstrap.scss @@ -23,8 +23,8 @@ @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"; From f5555999f563e5a117b40e32779aaa5dadc88220 Mon Sep 17 00:00:00 2001 From: aidosmf Date: Fri, 13 Sep 2024 12:32:08 +0500 Subject: [PATCH 06/24] feat: add table default ui --- src/pages/validator.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/pages/validator.js b/src/pages/validator.js index 4996bba..affaccb 100644 --- a/src/pages/validator.js +++ b/src/pages/validator.js @@ -6,6 +6,7 @@ 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 Layout from "../components/layout" import Seo from "../components/seo" @@ -65,6 +66,28 @@ const ValidatorPage = () => { /> + + + + + + + + + + + + + + + + + + + + +
PathNamed PathSeverityMessageDocs
12345
+ From bdae0962dc7af6fcb71706c1cef9ac06b0fe825e Mon Sep 17 00:00:00 2001 From: aidosmf Date: Fri, 13 Sep 2024 12:33:08 +0500 Subject: [PATCH 07/24] feat: update desc --- src/pages/validator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/validator.js b/src/pages/validator.js index affaccb..33db2aa 100644 --- a/src/pages/validator.js +++ b/src/pages/validator.js @@ -14,7 +14,7 @@ import Seo from "../components/seo" const content = { title: "Lottie Validator", description: - "Validates a given Lottie JSON file against the Lottie specification version.", + "Validates a given Lottie JSON file against the Lottie specification version", } const tabCssClass = "border border-top-0 rounded-bottom p-3 shadow" From e57eed2e7f44e42f0bfac6816cf4d87d33c852f0 Mon Sep 17 00:00:00 2001 From: aidosmf Date: Fri, 13 Sep 2024 13:22:38 +0500 Subject: [PATCH 08/24] feat: add tab content ui --- src/pages/validator.js | 40 +++++++++++++++++++++++++++++++-------- src/styles/bootstrap.scss | 2 +- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/pages/validator.js b/src/pages/validator.js index 33db2aa..f89ce46 100644 --- a/src/pages/validator.js +++ b/src/pages/validator.js @@ -7,6 +7,9 @@ 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 Layout from "../components/layout" import Seo from "../components/seo" @@ -17,7 +20,7 @@ const content = { "Validates a given Lottie JSON file against the Lottie specification version", } -const tabCssClass = "border border-top-0 rounded-bottom p-3 shadow" +const tabCssClass = "border border-top-0 rounded-bottom p-4 shadow" const ValidatorPage = () => { return ( @@ -36,20 +39,35 @@ const ValidatorPage = () => { - - Tab content for url + + - - Tab content for file + + + + - - Tab content for Text + + + +
Options
-
+
{ id="check-warning-property" />
+ + + + diff --git a/src/styles/bootstrap.scss b/src/styles/bootstrap.scss index 278cc2c..d2a68aa 100644 --- a/src/styles/bootstrap.scss +++ b/src/styles/bootstrap.scss @@ -28,7 +28,7 @@ @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"; From ec252c4a120975bd71bc9b40b6a3c77843510eb3 Mon Sep 17 00:00:00 2001 From: aidosmf Date: Tue, 17 Sep 2024 16:16:34 +0500 Subject: [PATCH 09/24] feat: add loading spinner on validate --- src/pages/validator.js | 58 +++++++++++++++++++++++++-------------- src/styles/bootstrap.scss | 2 +- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/pages/validator.js b/src/pages/validator.js index f89ce46..63af1f5 100644 --- a/src/pages/validator.js +++ b/src/pages/validator.js @@ -10,6 +10,7 @@ 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 Layout from "../components/layout" import Seo from "../components/seo" @@ -23,6 +24,12 @@ const content = { const tabCssClass = "border border-top-0 rounded-bottom p-4 shadow" const ValidatorPage = () => { + const [loading, setLoading] = React.useState(true) + + const handleValidateBtn = () => { + setLoading(!loading) + } + return (
@@ -84,33 +91,42 @@ const ValidatorPage = () => { /> - +
-
- - - - - - - - - - - - - - - - - - -
PathNamed PathSeverityMessageDocs
12345
+ {loading && ( +
+ + Loading... + +
+ )} + {!loading && ( + + + + + + + + + + + + + + + + + + + +
PathNamed PathSeverityMessageDocs
12345
+ )} diff --git a/src/styles/bootstrap.scss b/src/styles/bootstrap.scss index d2a68aa..7cf03c4 100644 --- a/src/styles/bootstrap.scss +++ b/src/styles/bootstrap.scss @@ -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"; From 36814721d7cac4a3da73fbab01f21655492d20a3 Mon Sep 17 00:00:00 2001 From: aidosmf Date: Tue, 17 Sep 2024 18:33:30 +0500 Subject: [PATCH 10/24] feat: prepare ui logic before validation --- src/pages/validator.js | 195 ++++++++++++++++++++++++++-- src/styles/bootstrap-variables.scss | 2 + src/styles/bootstrap.scss | 2 +- 3 files changed, 189 insertions(+), 10 deletions(-) diff --git a/src/pages/validator.js b/src/pages/validator.js index 63af1f5..e7f0e24 100644 --- a/src/pages/validator.js +++ b/src/pages/validator.js @@ -1,4 +1,5 @@ import * as React from "react" +import { useState, useRef, useEffect } from "react" import Container from "react-bootstrap/Container" import Row from "react-bootstrap/Row" @@ -11,6 +12,7 @@ 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" @@ -23,13 +25,167 @@ const content = { const tabCssClass = "border border-top-0 rounded-bottom p-4 shadow" +const isValidUrl = str => { + try { + new URL(str) + return true + } catch (_) { + return false + } +} + const ValidatorPage = () => { - const [loading, setLoading] = React.useState(true) + // refs + + const lottieFileInputRef = useRef(null) + + // states + + 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 [warningProperty, setWarningProperty] = useState(false) + const [warningType, setWarningType] = useState(false) + + // handlers + + const validateLottieString = lottieStr => { + if (!lottieStr) { + setErrorMessage("Lottie cannot be empty") + return + } + + if (typeof lottieStr !== "string") { + setErrorMessage("Lottie must be a string") + return + } + + setLoading(true) + + try { + const lottieObj = JSON.parse(lottieStr) + console.log("validation", typeof lottieObj) + } catch (e) { + setErrorMessage(`Could not parse Lottie JSON: ${e.message}`) + } + + setLoading(false) + } + + const validateLottieUrl = url => { + if (!url) { + setErrorMessage("Lottie URL cannot be empty") + return + } + + if (!isValidUrl(url)) { + setErrorMessage("Invalid Lottie URL") + return + } + + setLoading(true) + + fetch(url) + .then(result => result.text()) + .then(setLottie) + .catch(e => + setErrorMessage(`Could not load Lottie file from URL: ${e.message}`) + ) + .finally(() => setLoading(false)) + } + + const validateLottieFile = file => { + if (!file) { + setErrorMessage("Lottie File cannot be empty") + return + } - const handleValidateBtn = () => { - setLoading(!loading) + setLoading(true) + + const reader = new FileReader() + reader.onload = e => setLottie(e.target.result) + reader.onerror = _e => setErrorMessage("Could not load file") + reader.readAsText(file) + + setLoading(false) + } + + const validateLottieText = text => { + if (!text) { + setErrorMessage("Lottie text cannot be empty") + return + } + + if (typeof text !== "string") { + setErrorMessage("Lottie text must be a string") + return + } + + setLottie(text) } + // ui handlers + + const onTabsSelect = key => setCurrentTab(key) + const onWarningTypeChange = checked => setWarningType(checked) + const onWarningPropertyChange = checked => setWarningProperty(checked) + + const onLottieFileChange = e => { + if (e.target.files.length === 0) { + setErrorMessage("Please select a valid Lottie file") + return + } + + setLottieFile(e.target.files[0]) + } + + const resetStates = () => { + setLottie("") + setLottieUrl("") + setLottieFile(null) + setLottieText("") + setErrorMessage("") + if (lottieFileInputRef.current) lottieFileInputRef.current.value = "" + } + + const onValidateBtnClick = () => { + resetStates() + + switch (currentTab) { + case "url": + validateLottieUrl(lottieUrl) + break + case "file": + validateLottieFile(lottieFile) + break + case "text": + validateLottieText(lottieText) + break + default: + break + } + } + + const onResetBtnClick = () => { + resetStates() + } + + // effects + + useEffect(() => { + if (lottie) { + validateLottieString(lottie) + } + }, [lottie]) + return (
@@ -42,20 +198,31 @@ const ValidatorPage = () => {
- + - + setLottieUrl(e.target.value)} /> - + @@ -67,6 +234,7 @@ const ValidatorPage = () => { as="textarea" placeholder="Paste Lottie JSON text" style={{ height: "100px" }} + onChange={e => setLottieText(e.target.value)} /> @@ -81,6 +249,7 @@ const ValidatorPage = () => { name="check-warning-type" type="checkbox" id="check-warning-type" + onChange={e => onWarningTypeChange(e.currentTarget.checked)} /> { name="check-warning-property" type="checkbox" id="check-warning-property" + onChange={e => onWarningPropertyChange(e.currentTarget.checked)} />
- - + + {!lottie && errorMessage && ( + {errorMessage} + )} {loading && (
@@ -105,7 +282,7 @@ const ValidatorPage = () => {
)} - {!loading && ( + {!loading && lottie && ( 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 7cf03c4..a6dfea9 100644 --- a/src/styles/bootstrap.scss +++ b/src/styles/bootstrap.scss @@ -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'; From 987dd67602650093686ebfb136f0f145cb732f19 Mon Sep 17 00:00:00 2001 From: aidosmf Date: Tue, 17 Sep 2024 18:47:35 +0500 Subject: [PATCH 11/24] feat: add lottie schema json --- src/assets/lottie.schema.json | 2258 +++++++++++++++++++++++++++++++++ src/pages/validator.js | 148 ++- 2 files changed, 2337 insertions(+), 69 deletions(-) create mode 100644 src/assets/lottie.schema.json 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/pages/validator.js b/src/pages/validator.js index e7f0e24..49f66fc 100644 --- a/src/pages/validator.js +++ b/src/pages/validator.js @@ -17,6 +17,8 @@ import Alert from "react-bootstrap/Alert" import Layout from "../components/layout" import Seo from "../components/seo" +import LottieSchema from "../assets/lottie.schema.json" + const content = { title: "Lottie Validator", description: @@ -42,6 +44,8 @@ const ValidatorPage = () => { // states const [loading, setLoading] = useState(false) + const [lottieSchema, _setLottieSchema] = useState(LottieSchema) + const [currentTab, setCurrentTab] = useState("url") const [errorMessage, setErrorMessage] = useState("") @@ -199,78 +203,84 @@ const ValidatorPage = () => { - - - - - setLottieUrl(e.target.value)} - /> - - - + {lottieSchema && ( + + + + setLottieUrl(e.target.value)} /> - - - - + + + + + + + + setLottieText(e.target.value)} + /> + + + + + +
Options
+
+ onWarningTypeChange(e.currentTarget.checked)} + /> + + onWarningPropertyChange(e.currentTarget.checked) + } + /> +
+ + + -
Options
-
- onWarningTypeChange(e.currentTarget.checked)} - /> - onWarningPropertyChange(e.currentTarget.checked)} - /> -
- - - - - + reset + + + + + )} + {!lottie && errorMessage && ( {errorMessage} From 4c4c0ffee6e7aacf378d1c489228706f3b2b479c Mon Sep 17 00:00:00 2001 From: aidosmf Date: Tue, 17 Sep 2024 20:06:27 +0500 Subject: [PATCH 12/24] feat: ajv validation --- src/pages/validator.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/pages/validator.js b/src/pages/validator.js index 49f66fc..5ee7dd8 100644 --- a/src/pages/validator.js +++ b/src/pages/validator.js @@ -1,6 +1,8 @@ import * as React from "react" import { useState, useRef, useEffect } from "react" +import Ajv from "ajv" + import Container from "react-bootstrap/Container" import Row from "react-bootstrap/Row" import Col from "react-bootstrap/Col" @@ -55,6 +57,7 @@ const ValidatorPage = () => { const [lottieText, setLottieText] = useState("") const [lottie, setLottie] = useState("") + const [validationResult, setValidationResult] = useState([]) const [warningProperty, setWarningProperty] = useState(false) const [warningType, setWarningType] = useState(false) @@ -75,10 +78,15 @@ const ValidatorPage = () => { setLoading(true) try { - const lottieObj = JSON.parse(lottieStr) - console.log("validation", typeof lottieObj) + const ajv = new Ajv() + + const validate = ajv.compile(lottieSchema) + const lottieJSON = JSON.parse(lottieStr) + const result = validate(lottieJSON) + + setValidationResult(result) } catch (e) { - setErrorMessage(`Could not parse Lottie JSON: ${e.message}`) + setErrorMessage(`Could not validate Lottie JSON: ${e.message}`) } setLoading(false) @@ -161,8 +169,6 @@ const ValidatorPage = () => { } const onValidateBtnClick = () => { - resetStates() - switch (currentTab) { case "url": validateLottieUrl(lottieUrl) @@ -282,9 +288,7 @@ const ValidatorPage = () => { )} - {!lottie && errorMessage && ( - {errorMessage} - )} + {errorMessage && {errorMessage}} {loading && (
@@ -292,7 +296,7 @@ const ValidatorPage = () => {
)} - {!loading && lottie && ( + {!loading && validationResult.length > 0 && (
From 50b3036953a81eca689f3b5a452a2ea21b8e616c Mon Sep 17 00:00:00 2001 From: aidosmf Date: Tue, 17 Sep 2024 22:43:19 +0500 Subject: [PATCH 13/24] feat: render validation results intable --- src/pages/validator.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/pages/validator.js b/src/pages/validator.js index 5ee7dd8..ff7b8d7 100644 --- a/src/pages/validator.js +++ b/src/pages/validator.js @@ -308,13 +308,15 @@ const ValidatorPage = () => { - - - - - - - + {validationResult.map((result, index) => ( + + + + + + + + ))}
12345
{result.dataPath}{result.params.missingProperty}{result.keyword}{result.message}{result.schemaPath}
)} From 5b6f8a18b7aa8716d1aaa96321646b1afbc05c2a Mon Sep 17 00:00:00 2001 From: aidosmf Date: Tue, 17 Sep 2024 23:55:27 +0500 Subject: [PATCH 14/24] feat: ajv validation & render; remove useeffect --- src/pages/validator.js | 75 ++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 43 deletions(-) diff --git a/src/pages/validator.js b/src/pages/validator.js index ff7b8d7..5aae985 100644 --- a/src/pages/validator.js +++ b/src/pages/validator.js @@ -1,7 +1,7 @@ import * as React from "react" -import { useState, useRef, useEffect } from "react" +import { useState, useRef } from "react" -import Ajv from "ajv" +import Ajv from "ajv/dist/2020" import Container from "react-bootstrap/Container" import Row from "react-bootstrap/Row" @@ -19,7 +19,7 @@ import Alert from "react-bootstrap/Alert" import Layout from "../components/layout" import Seo from "../components/seo" -import LottieSchema from "../assets/lottie.schema.json" +import lottieSchema from "../assets/lottie.schema.json" const content = { title: "Lottie Validator", @@ -46,7 +46,6 @@ const ValidatorPage = () => { // states const [loading, setLoading] = useState(false) - const [lottieSchema, _setLottieSchema] = useState(LottieSchema) const [currentTab, setCurrentTab] = useState("url") @@ -57,12 +56,16 @@ const ValidatorPage = () => { const [lottieText, setLottieText] = useState("") const [lottie, setLottie] = useState("") - const [validationResult, setValidationResult] = useState([]) + const [validationErrors, setValidationErrors] = useState([]) const [warningProperty, setWarningProperty] = useState(false) const [warningType, setWarningType] = useState(false) - // handlers + // validator + + const ajv = new Ajv({ + keywords: [{ keyword: "$version" }], + }) const validateLottieString = lottieStr => { if (!lottieStr) { @@ -76,15 +79,14 @@ const ValidatorPage = () => { } setLoading(true) + setLottie(lottieStr) try { - const ajv = new Ajv() - const validate = ajv.compile(lottieSchema) const lottieJSON = JSON.parse(lottieStr) - const result = validate(lottieJSON) + const isValid = validate(lottieJSON) - setValidationResult(result) + if (!isValid) setValidationErrors(validate.errors) } catch (e) { setErrorMessage(`Could not validate Lottie JSON: ${e.message}`) } @@ -92,6 +94,8 @@ const ValidatorPage = () => { setLoading(false) } + // handlers + const validateLottieUrl = url => { if (!url) { setErrorMessage("Lottie URL cannot be empty") @@ -107,7 +111,7 @@ const ValidatorPage = () => { fetch(url) .then(result => result.text()) - .then(setLottie) + .then(validateLottieString) .catch(e => setErrorMessage(`Could not load Lottie file from URL: ${e.message}`) ) @@ -123,7 +127,7 @@ const ValidatorPage = () => { setLoading(true) const reader = new FileReader() - reader.onload = e => setLottie(e.target.result) + reader.onload = e => validateLottieString(e.target.result) reader.onerror = _e => setErrorMessage("Could not load file") reader.readAsText(file) @@ -141,7 +145,7 @@ const ValidatorPage = () => { return } - setLottie(text) + validateLottieString(text) } // ui handlers @@ -150,17 +154,9 @@ const ValidatorPage = () => { const onWarningTypeChange = checked => setWarningType(checked) const onWarningPropertyChange = checked => setWarningProperty(checked) - const onLottieFileChange = e => { - if (e.target.files.length === 0) { - setErrorMessage("Please select a valid Lottie file") - return - } - - setLottieFile(e.target.files[0]) - } - const resetStates = () => { setLottie("") + setValidationErrors("") setLottieUrl("") setLottieFile(null) setLottieText("") @@ -184,18 +180,6 @@ const ValidatorPage = () => { } } - const onResetBtnClick = () => { - resetStates() - } - - // effects - - useEffect(() => { - if (lottie) { - validateLottieString(lottie) - } - }, [lottie]) - return (
@@ -232,7 +216,7 @@ const ValidatorPage = () => { type="file" accept="application/JSON" ref={lottieFileInputRef} - onChange={onLottieFileChange} + onChange={e => setLottieFile(e.target.files[0])} /> @@ -277,8 +261,8 @@ const ValidatorPage = () => { @@ -288,7 +272,6 @@ const ValidatorPage = () => { )} - {errorMessage && {errorMessage}} {loading && (
@@ -296,7 +279,13 @@ const ValidatorPage = () => {
)} - {!loading && validationResult.length > 0 && ( + {errorMessage && {errorMessage}} + {!errorMessage && lottie && validationErrors.length === 0 && ( + + Validation successful with no errors + + )} + {!loading && validationErrors.length > 0 && ( @@ -308,11 +297,11 @@ const ValidatorPage = () => { - {validationResult.map((result, index) => ( + {validationErrors.map((result, index) => ( - - - + + + From a41b1f350659d3302f42543408d9119e8de08b10 Mon Sep 17 00:00:00 2001 From: aidosmf Date: Wed, 18 Sep 2024 16:36:26 +0500 Subject: [PATCH 15/24] refactor: move isValidUrl to helpers dir --- src/pages/validator.js | 10 +--------- src/utils/helpers.js | 8 ++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 src/utils/helpers.js diff --git a/src/pages/validator.js b/src/pages/validator.js index 5aae985..9fed215 100644 --- a/src/pages/validator.js +++ b/src/pages/validator.js @@ -20,6 +20,7 @@ import Layout from "../components/layout" import Seo from "../components/seo" import lottieSchema from "../assets/lottie.schema.json" +import { isValidUrl } from "../utils/helpers" const content = { title: "Lottie Validator", @@ -29,15 +30,6 @@ const content = { const tabCssClass = "border border-top-0 rounded-bottom p-4 shadow" -const isValidUrl = str => { - try { - new URL(str) - return true - } catch (_) { - return false - } -} - const ValidatorPage = () => { // refs 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 + } +} From e7883b069dd4c0e041ac29eb6e14403337655902 Mon Sep 17 00:00:00 2001 From: aidosmf Date: Wed, 18 Sep 2024 17:03:35 +0500 Subject: [PATCH 16/24] refactor: move validator logic to utils --- src/pages/validator.js | 203 ++++++++++++++++++----------------------- src/utils/validator.js | 30 ++++++ 2 files changed, 119 insertions(+), 114 deletions(-) create mode 100644 src/utils/validator.js diff --git a/src/pages/validator.js b/src/pages/validator.js index 9fed215..e88eb19 100644 --- a/src/pages/validator.js +++ b/src/pages/validator.js @@ -1,8 +1,6 @@ import * as React from "react" import { useState, useRef } from "react" -import Ajv from "ajv/dist/2020" - import Container from "react-bootstrap/Container" import Row from "react-bootstrap/Row" import Col from "react-bootstrap/Col" @@ -19,8 +17,8 @@ import Alert from "react-bootstrap/Alert" import Layout from "../components/layout" import Seo from "../components/seo" -import lottieSchema from "../assets/lottie.schema.json" import { isValidUrl } from "../utils/helpers" +import { validate } from "../utils/validator" const content = { title: "Lottie Validator", @@ -53,55 +51,36 @@ const ValidatorPage = () => { const [warningProperty, setWarningProperty] = useState(false) const [warningType, setWarningType] = useState(false) - // validator - - const ajv = new Ajv({ - keywords: [{ keyword: "$version" }], - }) + // handlers const validateLottieString = lottieStr => { - if (!lottieStr) { - setErrorMessage("Lottie cannot be empty") - return - } - - if (typeof lottieStr !== "string") { - setErrorMessage("Lottie must be a string") - return - } - setLoading(true) setLottie(lottieStr) try { - const validate = ajv.compile(lottieSchema) - const lottieJSON = JSON.parse(lottieStr) - const isValid = validate(lottieJSON) - - if (!isValid) setValidationErrors(validate.errors) + const result = validate(lottieStr) + setValidationErrors(result) } catch (e) { - setErrorMessage(`Could not validate Lottie JSON: ${e.message}`) + setErrorMessage(e.message) } setLoading(false) } - // handlers - - const validateLottieUrl = url => { - if (!url) { + const validateLottieUrl = () => { + if (!lottieUrl) { setErrorMessage("Lottie URL cannot be empty") return } - if (!isValidUrl(url)) { + if (!isValidUrl(lottieUrl)) { setErrorMessage("Invalid Lottie URL") return } setLoading(true) - fetch(url) + fetch(lottieUrl) .then(result => result.text()) .then(validateLottieString) .catch(e => @@ -110,8 +89,8 @@ const ValidatorPage = () => { .finally(() => setLoading(false)) } - const validateLottieFile = file => { - if (!file) { + const validateLottieFile = () => { + if (!lottieFile) { setErrorMessage("Lottie File cannot be empty") return } @@ -121,23 +100,23 @@ const ValidatorPage = () => { const reader = new FileReader() reader.onload = e => validateLottieString(e.target.result) reader.onerror = _e => setErrorMessage("Could not load file") - reader.readAsText(file) + reader.readAsText(lottieFile) setLoading(false) } - const validateLottieText = text => { - if (!text) { + const validateLottieText = () => { + if (!lottieText) { setErrorMessage("Lottie text cannot be empty") return } - if (typeof text !== "string") { + if (typeof lottieText !== "string") { setErrorMessage("Lottie text must be a string") return } - validateLottieString(text) + validateLottieString(lottieText) } // ui handlers @@ -159,13 +138,13 @@ const ValidatorPage = () => { const onValidateBtnClick = () => { switch (currentTab) { case "url": - validateLottieUrl(lottieUrl) + validateLottieUrl() break case "file": - validateLottieFile(lottieFile) + validateLottieFile() break case "text": - validateLottieText(lottieText) + validateLottieText() break default: break @@ -185,83 +164,79 @@ const ValidatorPage = () => { - {lottieSchema && ( - - - - + + + + + setLottieUrl(e.target.value)} + /> + + + setLottieUrl(e.target.value)} + type="file" + accept="application/JSON" + ref={lottieFileInputRef} + onChange={e => setLottieFile(e.target.files[0])} /> - - - - setLottieFile(e.target.files[0])} - /> - - - - - setLottieText(e.target.value)} - /> - - - - - -
Options
-
- onWarningTypeChange(e.currentTarget.checked)} - /> - - onWarningPropertyChange(e.currentTarget.checked) - } - /> -
- - - - - - - )} + setLottieText(e.target.value)} + /> + + + + + +
Options
+
+ onWarningTypeChange(e.currentTarget.checked)} + /> + onWarningPropertyChange(e.currentTarget.checked)} + /> +
+ + + + + + {loading && ( diff --git a/src/utils/validator.js b/src/utils/validator.js new file mode 100644 index 0000000..d889fe8 --- /dev/null +++ b/src/utils/validator.js @@ -0,0 +1,30 @@ +import Ajv from "ajv/dist/2020" +import lottieSchema from "../assets/lottie.schema.json" + +const ajv = new Ajv({ + keywords: [{ keyword: "$version" }], +}) + +export const validate = lottieStr => { + if (!lottieStr) { + throw new Error("Lottie cannot be empty") + } + + if (typeof lottieStr !== "string") { + throw new Error("Lottie must be a string") + } + + try { + const validate = ajv.compile(lottieSchema) + const lottieJSON = JSON.parse(lottieStr) + const isValid = validate(lottieJSON) + + if (isValid) { + return [] + } else { + return validate.errors + } + } catch (e) { + throw new Error(`Could not validate Lottie JSON: ${e.message}`) + } +} From 061425625c7fff128d088c0eefd87740a0f69251 Mon Sep 17 00:00:00 2001 From: aidosmf Date: Wed, 18 Sep 2024 17:50:37 +0500 Subject: [PATCH 17/24] feat: integrate glax validator logic --- src/pages/validator.js | 113 ++++++--- src/utils/validator.js | 556 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 616 insertions(+), 53 deletions(-) diff --git a/src/pages/validator.js b/src/pages/validator.js index e88eb19..cafa58f 100644 --- a/src/pages/validator.js +++ b/src/pages/validator.js @@ -1,5 +1,8 @@ import * as React from "react" -import { useState, useRef } 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" @@ -18,7 +21,7 @@ import Layout from "../components/layout" import Seo from "../components/seo" import { isValidUrl } from "../utils/helpers" -import { validate } from "../utils/validator" +import { Validator } from "../utils/validator" const content = { title: "Lottie Validator", @@ -53,20 +56,6 @@ const ValidatorPage = () => { // handlers - const validateLottieString = lottieStr => { - setLoading(true) - setLottie(lottieStr) - - try { - const result = validate(lottieStr) - setValidationErrors(result) - } catch (e) { - setErrorMessage(e.message) - } - - setLoading(false) - } - const validateLottieUrl = () => { if (!lottieUrl) { setErrorMessage("Lottie URL cannot be empty") @@ -82,7 +71,7 @@ const ValidatorPage = () => { fetch(lottieUrl) .then(result => result.text()) - .then(validateLottieString) + .then(setLottie) .catch(e => setErrorMessage(`Could not load Lottie file from URL: ${e.message}`) ) @@ -98,7 +87,7 @@ const ValidatorPage = () => { setLoading(true) const reader = new FileReader() - reader.onload = e => validateLottieString(e.target.result) + reader.onload = e => setLottie(e.target.result) reader.onerror = _e => setErrorMessage("Could not load file") reader.readAsText(lottieFile) @@ -116,7 +105,7 @@ const ValidatorPage = () => { return } - validateLottieString(lottieText) + setLottie(lottieText) } // ui handlers @@ -151,6 +140,63 @@ const ValidatorPage = () => { } } + useEffect(() => { + if (!lottie) return + + setLoading(true) + + const validator = new Validator(Ajv.Ajv2020, lottieSchema) + + try { + const errors = validator.validate(lottie) + console.log(errors) + setValidationErrors(errors) + } catch (e) { + setErrorMessage(e.message) + } + + setLoading(false) + }, [lottie]) + + // render + + const renderTableRows = (errors, warningType, warningProperty) => { + 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" + : "" + + const namedPath = path_names + ? path_names.map(n => n ?? "(unnamed)").join(" > ") + : "" + + return ( + + + + + + + + ) + }) + } + return (
@@ -246,12 +292,17 @@ const ValidatorPage = () => { )} - {errorMessage && {errorMessage}} - {!errorMessage && lottie && validationErrors.length === 0 && ( - - Validation successful with no errors - + {!loading && errorMessage && ( + {errorMessage} )} + {!loading && + !errorMessage && + lottie && + validationErrors.length === 0 && ( + + Validation successful with no errors + + )} {!loading && validationErrors.length > 0 && (
{result.dataPath}{result.params.missingProperty}{result.keyword}{result.instancePath}{`${Object.keys(result.params)}`} {result.message} {result.schemaPath}
{path || ""}{namedPath || ""}{type || ""}{message || ""} + {docs && ( + + {name} + + )} +
@@ -264,15 +315,11 @@ const ValidatorPage = () => { - {validationErrors.map((result, index) => ( - - - - - - - - ))} + {renderTableRows( + validationErrors, + warningType, + warningProperty + )}
{result.instancePath}{`${Object.keys(result.params)}`}{result.message}{result.schemaPath}
)} diff --git a/src/utils/validator.js b/src/utils/validator.js index d889fe8..1499594 100644 --- a/src/utils/validator.js +++ b/src/utils/validator.js @@ -1,30 +1,546 @@ -import Ajv from "ajv/dist/2020" -import lottieSchema from "../assets/lottie.schema.json" +function extract_schema_ty(schema) { + if (!schema) return -const ajv = new Ajv({ - keywords: [{ keyword: "$version" }], -}) + if ("properties" in schema) { + let ty_prop = schema.properties.ty + if (!ty_prop) return + return ty_prop.const + } -export const validate = lottieStr => { - if (!lottieStr) { - throw new Error("Lottie cannot be empty") + 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 + } + } } +} - if (typeof lottieStr !== "string") { - throw new Error("Lottie must be a string") +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 + } + } } +} - try { - const validate = ajv.compile(lottieSchema) - const lottieJSON = JSON.parse(lottieStr) - const isValid = validate(lottieJSON) +class PropertyList { + constructor(schema) { + this.properties = new Set() + this.references = new Set() + this.schema = schema + this.resolved = false + this.skip = false + } - if (isValid) { - return [] - } else { - return validate.errors + 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, } - } catch (e) { - throw new Error(`Could not validate Lottie JSON: ${e.message}`) } } From d384504fd7e3eedfc62bef185f4f6d4a7ed6c260 Mon Sep 17 00:00:00 2001 From: aidosmf Date: Wed, 18 Sep 2024 18:41:13 +0500 Subject: [PATCH 18/24] feat: config filter checkboxes with minor refactoring --- src/pages/validator.js | 113 ++++++++++++++++++++++++----------------- 1 file changed, 67 insertions(+), 46 deletions(-) diff --git a/src/pages/validator.js b/src/pages/validator.js index cafa58f..b295adb 100644 --- a/src/pages/validator.js +++ b/src/pages/validator.js @@ -38,6 +38,8 @@ const ValidatorPage = () => { // states + const [validator, setValidator] = useState(null) + const [loading, setLoading] = useState(false) const [currentTab, setCurrentTab] = useState("url") @@ -49,10 +51,9 @@ const ValidatorPage = () => { const [lottieText, setLottieText] = useState("") const [lottie, setLottie] = useState("") - const [validationErrors, setValidationErrors] = useState([]) - - const [warningProperty, setWarningProperty] = useState(false) - const [warningType, setWarningType] = useState(false) + const [validationResult, setValidationResult] = useState([]) + const [warnUnknownProps, setWarnUnknownProps] = useState(false) + const [warnUnknownObjTypes, setWarnUnknownObjTypes] = useState(true) // handlers @@ -111,12 +112,12 @@ const ValidatorPage = () => { // ui handlers const onTabsSelect = key => setCurrentTab(key) - const onWarningTypeChange = checked => setWarningType(checked) - const onWarningPropertyChange = checked => setWarningProperty(checked) + const onWarnUnknownObjTypesChange = checked => setWarnUnknownObjTypes(checked) + const onWarnUnknownPropsChange = checked => setWarnUnknownProps(checked) const resetStates = () => { setLottie("") - setValidationErrors("") + setValidationResult([]) setLottieUrl("") setLottieFile(null) setLottieText("") @@ -125,6 +126,8 @@ const ValidatorPage = () => { } const onValidateBtnClick = () => { + setErrorMessage("") + switch (currentTab) { case "url": validateLottieUrl() @@ -140,27 +143,9 @@ const ValidatorPage = () => { } } - useEffect(() => { - if (!lottie) return - - setLoading(true) - - const validator = new Validator(Ajv.Ajv2020, lottieSchema) - - try { - const errors = validator.validate(lottie) - console.log(errors) - setValidationErrors(errors) - } catch (e) { - setErrorMessage(e.message) - } - - setLoading(false) - }, [lottie]) - // render - const renderTableRows = (errors, warningType, warningProperty) => { + const renderTableRows = errors => { return errors.map((err, index) => { const { path, path_names, type, message, name, docs } = err @@ -173,7 +158,7 @@ const ValidatorPage = () => { ? "table-warning" : type === "error" ? "table-danger" - : "" + : "table-primary" const namedPath = path_names ? path_names.map(n => n ?? "(unnamed)").join(" > ") @@ -197,6 +182,50 @@ const ValidatorPage = () => { }) } + // effects + + useEffect(() => { + setLoading(true) + setValidator(new Validator(Ajv.Ajv2020, lottieSchema)) + setLoading(false) + }, []) + + useEffect(() => { + if (!lottie || !validator) return + + setLoading(true) + + try { + const result = validator.validate(lottie) + const finalResult = [] + + let hasError = false + + console.log(result) + + 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 (
@@ -260,7 +289,10 @@ const ValidatorPage = () => { name="check-warning-type" type="checkbox" id="check-warning-type" - onChange={e => onWarningTypeChange(e.currentTarget.checked)} + checked={warnUnknownObjTypes} + onChange={e => + onWarnUnknownObjTypesChange(e.currentTarget.checked) + } /> { name="check-warning-property" type="checkbox" id="check-warning-property" - onChange={e => onWarningPropertyChange(e.currentTarget.checked)} + checked={warnUnknownProps} + onChange={e => + onWarnUnknownPropsChange(e.currentTarget.checked) + } /> @@ -276,7 +311,7 @@ const ValidatorPage = () => { @@ -295,15 +330,7 @@ const ValidatorPage = () => { {!loading && errorMessage && ( {errorMessage} )} - {!loading && - !errorMessage && - lottie && - validationErrors.length === 0 && ( - - Validation successful with no errors - - )} - {!loading && validationErrors.length > 0 && ( + {!loading && validationResult.length > 0 && ( @@ -314,13 +341,7 @@ const ValidatorPage = () => { - - {renderTableRows( - validationErrors, - warningType, - warningProperty - )} - + {renderTableRows(validationResult)}
Docs
)} From 2e3733de8a7a0972443fe5a82ba9594d4bf8115e Mon Sep 17 00:00:00 2001 From: aidosmf Date: Wed, 18 Sep 2024 18:41:33 +0500 Subject: [PATCH 19/24] feat: responsive table ui --- src/pages/validator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/validator.js b/src/pages/validator.js index b295adb..927611f 100644 --- a/src/pages/validator.js +++ b/src/pages/validator.js @@ -331,7 +331,7 @@ const ValidatorPage = () => { {errorMessage} )} {!loading && validationResult.length > 0 && ( - +
From 4987d898340ec69662e5baaf54718b3b5b23a70a Mon Sep 17 00:00:00 2001 From: aidosmf Date: Wed, 18 Sep 2024 18:43:33 +0500 Subject: [PATCH 20/24] refactor(validator): remove js warnigns --- src/utils/validator.js | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/utils/validator.js b/src/utils/validator.js index 1499594..0ba9a88 100644 --- a/src/utils/validator.js +++ b/src/utils/validator.js @@ -18,7 +18,7 @@ function extract_schema_ty(schema) { } function patch_docs_links(schema, url, name, docs_name, within_properties) { - if (typeof schema == "object") { + if (typeof schema === "object") { if (Array.isArray(schema)) { for (let item of schema) patch_docs_links(item, url, name, docs_name) } else { @@ -26,7 +26,7 @@ function patch_docs_links(schema, url, name, docs_name, within_properties) { var sub_name = name if (within_properties) sub_name += "." + pname - patch_docs_links(val, url, sub_name, docs_name, pname == "properties") + patch_docs_links(val, url, sub_name, docs_name, pname === "properties") } if (!within_properties) { @@ -86,7 +86,7 @@ class PropertyMap { } extract_all_properties(schema, id, prop_list, referencing_base) { - if (typeof schema != "object" || schema === null) return + if (typeof schema !== "object" || schema === null) return if (Array.isArray(schema)) { for (let i = 0; i < schema.length; i++) @@ -96,7 +96,7 @@ class PropertyMap { } for (let [name, sub_schema] of Object.entries(schema)) { - if (name == "properties") { + 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 @@ -107,7 +107,7 @@ class PropertyMap { false ) } - } else if (name == "oneOf") { + } 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] @@ -116,18 +116,18 @@ class PropertyMap { : this.create(oneof_id, oneof_schema) this.extract_all_properties(oneof_schema, oneof_id, oneof_list, false) } - } else if (name == "allOf") { + } 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") { + } else if (name === "additionalProperties") { prop_list.skip = true - } else if (name == "$ref") { + } else if (name === "$ref") { prop_list.references.add(sub_schema) if (referencing_base) this.all_references.add(sub_schema) - } else if (name != "not") { + } else if (name !== "not") { this.extract_all_properties( sub_schema, id + "/" + name, @@ -195,7 +195,7 @@ function patch_schema_enum(schema) { } function keyframe_has_t(kf) { - return typeof kf == "object" && typeof kf.t == "number" + return typeof kf === "object" && typeof kf.t === "number" } export class Validator { @@ -210,7 +210,7 @@ export class Validator { 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") { + if (sub_schema.type && obj !== "base-gradient") { obj_docs += "#" + obj obj_name = sub_schema.title || kebab_to_title(obj) } @@ -277,7 +277,7 @@ export class Validator { ) { validate_asset.errors = [] - if (typeof data != "object" || data === null) return true + if (typeof data !== "object" || data === null) return true var target_schema @@ -312,7 +312,7 @@ export class Validator { if (data.h) require_io = false var index = data_cxt.parentData.indexOf(data) - if (index == data_cxt.parentData.length - 1) require_io = false + if (index === data_cxt.parentData.length - 1) require_io = false if (require_io) { for (var prop of "io") { @@ -329,7 +329,7 @@ export class Validator { if (index > 0) { var prev_kf = data_cxt.parentData[index - 1] - if (keyframe_has_t(prev_kf) && typeof data.t == "number") { + 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`, @@ -337,9 +337,9 @@ export class Validator { instancePath: data_cxt.instancePath, parentSchema: parent_schema, }) - } else if (data.t == prev_kf.t && index > 1) { + } 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) { + 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", @@ -351,7 +351,7 @@ export class Validator { } } - return validate_keyframe.errors.length == 0 + return validate_keyframe.errors.length === 0 }, }, { @@ -412,7 +412,7 @@ export class Validator { ) { warn_extra_props.errors = [] - if (typeof data != "object" || data === null) return true + if (typeof data !== "object" || data === null) return true for (let prop of Object.keys(data)) { if (!schema.has(prop)) { @@ -426,7 +426,7 @@ export class Validator { } } - return warn_extra_props.errors.length == 0 + return warn_extra_props.errors.length === 0 }, }, ], From a2b19712fc55c00059714c336fbac2811e1877fb Mon Sep 17 00:00:00 2001 From: aidosmf Date: Wed, 18 Sep 2024 18:48:10 +0500 Subject: [PATCH 21/24] fix: reset textarea val --- src/pages/validator.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/validator.js b/src/pages/validator.js index 927611f..0e68770 100644 --- a/src/pages/validator.js +++ b/src/pages/validator.js @@ -35,6 +35,7 @@ const ValidatorPage = () => { // refs const lottieFileInputRef = useRef(null) + const lottieTextInputRef = useRef(null) // states @@ -123,6 +124,7 @@ const ValidatorPage = () => { setLottieText("") setErrorMessage("") if (lottieFileInputRef.current) lottieFileInputRef.current.value = "" + if (lottieTextInputRef.current) lottieTextInputRef.current.value = "" } const onValidateBtnClick = () => { @@ -274,6 +276,7 @@ const ValidatorPage = () => { as="textarea" placeholder="Paste Lottie JSON text" style={{ height: "100px" }} + ref={lottieTextInputRef} onChange={e => setLottieText(e.target.value)} /> From 2c167349d89632967548ab4283228747b654f30c Mon Sep 17 00:00:00 2001 From: aidosmf Date: Mon, 23 Sep 2024 18:16:34 +0500 Subject: [PATCH 22/24] refactor: better name for tr class --- src/pages/validator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/validator.js b/src/pages/validator.js index 0e68770..ea19035 100644 --- a/src/pages/validator.js +++ b/src/pages/validator.js @@ -167,7 +167,7 @@ const ValidatorPage = () => { : "" return ( - + From 88453400669503d310c8fd4ba3fa16636f8d533c Mon Sep 17 00:00:00 2001 From: aidosmf Date: Mon, 23 Sep 2024 18:24:58 +0500 Subject: [PATCH 23/24] fix(validator): schema.oneOf is not iteratable on some cases --- src/utils/validator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/validator.js b/src/utils/validator.js index 0ba9a88..2a3fed5 100644 --- a/src/utils/validator.js +++ b/src/utils/validator.js @@ -475,7 +475,7 @@ export class Validator { } schema.prop_oneof = [] - for (let opt of schema.oneOf) { + for (let opt of schema.oneOf || []) { schema.prop_oneof.push({ schema: { type: "object", From 58f555e372d4653c887eb12c1d64f169a84c57e1 Mon Sep 17 00:00:00 2001 From: aidosmf Date: Mon, 23 Sep 2024 18:26:50 +0500 Subject: [PATCH 24/24] refactor(pages/validator): destroy validator on unmount --- src/pages/validator.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/validator.js b/src/pages/validator.js index ea19035..c596bcb 100644 --- a/src/pages/validator.js +++ b/src/pages/validator.js @@ -190,6 +190,10 @@ const ValidatorPage = () => { setLoading(true) setValidator(new Validator(Ajv.Ajv2020, lottieSchema)) setLoading(false) + + return () => { + setValidator(null) + } }, []) useEffect(() => { @@ -203,8 +207,6 @@ const ValidatorPage = () => { let hasError = false - console.log(result) - result.forEach(item => { if (!warnUnknownProps && item.warning === "property") return if (!warnUnknownObjTypes && item.warning === "type") return
Path
{path || ""} {namedPath || ""} {type || ""}