From fc5a7cc27244f293a9a50acd785f7edcdaaa96ea Mon Sep 17 00:00:00 2001 From: "Ahmed T. Ali" Date: Thu, 9 Dec 2021 18:36:25 +0100 Subject: [PATCH] feat(rich-text-types): expose RT validation helper (#292) --- package-lock.json | 546 ++++++++++++- packages/rich-text-types/package.json | 3 + .../src/__test__/schemaConstraints.test.ts | 2 +- .../src/__test__/validation.test.ts | 720 ++++++++++++++++++ packages/rich-text-types/src/blocks.ts | 4 +- packages/rich-text-types/src/emptyDocument.ts | 2 +- packages/rich-text-types/src/helpers.ts | 4 +- packages/rich-text-types/src/index.ts | 5 +- packages/rich-text-types/src/inlines.ts | 4 +- packages/rich-text-types/src/nodeTypes.ts | 4 +- .../rich-text-types/src/schemaConstraints.ts | 4 +- .../src/schemas/__test__/schemas.test.ts | 4 +- packages/rich-text-types/src/types.ts | 4 +- packages/rich-text-types/src/validation.ts | 146 ++++ 14 files changed, 1410 insertions(+), 42 deletions(-) create mode 100644 packages/rich-text-types/src/__test__/validation.test.ts create mode 100644 packages/rich-text-types/src/validation.ts diff --git a/package-lock.json b/package-lock.json index f9e81944..e4524fb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@types/react": "^16.8.15", "@types/react-dom": "^16.8.4", "@types/rollup-plugin-json": "^3.0.3", + "ajv": "^8.8.2", "colors": "^1.1.2", "core-js": "^3.17.2", "coveralls": "^3.0.0", @@ -5691,9 +5692,9 @@ "dev": true }, "node_modules/@types/lodash": { - "version": "4.14.176", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.176.tgz", - "integrity": "sha512-xZmuPTa3rlZoIbtDUyJKZQimJV3bxCmzMIO2c9Pz9afyDro6kr7R79GwcB6mRhuoPmV2p1Vb66WOJH7F886WKQ==" + "version": "4.14.178", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", + "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==" }, "node_modules/@types/lodash.clonedeep": { "version": "4.5.6", @@ -6453,14 +6454,18 @@ } }, "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.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", "dependencies": { "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.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/ajv-errors": { @@ -12598,6 +12603,22 @@ "node": ">=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==", + "dev": true, + "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/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -12869,6 +12890,12 @@ "node": ">=8" } }, + "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==", + "dev": true + }, "node_modules/eslint/node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -13759,6 +13786,28 @@ "webpack": "^2.0.0 || ^3.0.0 || ^4.0.0" } }, + "node_modules/file-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==", + "dev": true, + "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/file-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==", + "dev": true + }, "node_modules/file-loader/node_modules/schema-utils": { "version": "0.4.7", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", @@ -17884,6 +17933,26 @@ "node": ">=6" } }, + "node_modules/har-validator/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/har-validator/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/hard-rejection": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", @@ -22409,9 +22478,9 @@ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "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": { "version": "1.0.1", @@ -24676,6 +24745,28 @@ "webpack": "^4.4.0 || ^5.0.0" } }, + "node_modules/mini-css-extract-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==", + "dev": true, + "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/mini-css-extract-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==", + "dev": true + }, "node_modules/mini-css-extract-plugin/node_modules/normalize-url": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", @@ -25617,6 +25708,28 @@ "webpack": "^4.3.0" } }, + "node_modules/null-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==", + "dev": true, + "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/null-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==", + "dev": true + }, "node_modules/null-loader/node_modules/schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -26988,6 +27101,28 @@ "node": ">= 6" } }, + "node_modules/postcss-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==", + "dev": true, + "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/postcss-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==", + "dev": true + }, "node_modules/postcss-loader/node_modules/schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -29443,6 +29578,14 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -29935,6 +30078,28 @@ "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==", + "dev": true, + "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/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==", + "dev": true + }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -31395,6 +31560,28 @@ "node": ">= 0.12.0" } }, + "node_modules/style-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==", + "dev": true, + "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/style-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==", + "dev": true + }, "node_modules/style-loader/node_modules/schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -31681,6 +31868,22 @@ "node": ">=6.0.0" } }, + "node_modules/table/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "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/table/node_modules/ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", @@ -31690,6 +31893,12 @@ "node": ">=6" } }, + "node_modules/table/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==", + "dev": true + }, "node_modules/table/node_modules/slice-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", @@ -33619,6 +33828,28 @@ "webpack": "^3.0.0 || ^4.0.0" } }, + "node_modules/url-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==", + "dev": true, + "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/url-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==", + "dev": true + }, "node_modules/url-loader/node_modules/schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -34308,6 +34539,22 @@ } } }, + "node_modules/webpack-dev-server/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "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/webpack-dev-server/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -34538,6 +34785,12 @@ "node": ">=4" } }, + "node_modules/webpack-dev-server/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==", + "dev": true + }, "node_modules/webpack-dev-server/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -34837,6 +35090,22 @@ "node": ">=0.4.0" } }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "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/webpack/node_modules/eslint-scope": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", @@ -34882,6 +35151,12 @@ "node": ">=4" } }, + "node_modules/webpack/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==", + "dev": true + }, "node_modules/webpack/node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -36343,6 +36618,9 @@ "name": "@contentful/rich-text-types", "version": "15.7.0", "license": "MIT", + "dependencies": { + "ajv": "^8.8.2" + }, "devDependencies": { "@types/jest": "^27.0.1", "@types/node": "^14.17.14", @@ -38175,6 +38453,7 @@ "requires": { "@types/jest": "^27.0.1", "@types/node": "^14.17.14", + "ajv": "^8.8.2", "core-js": "^3.17.2", "faker": "^4.1.0", "jest": "^27.1.0", @@ -41228,9 +41507,9 @@ "dev": true }, "@types/lodash": { - "version": "4.14.176", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.176.tgz", - "integrity": "sha512-xZmuPTa3rlZoIbtDUyJKZQimJV3bxCmzMIO2c9Pz9afyDro6kr7R79GwcB6mRhuoPmV2p1Vb66WOJH7F886WKQ==" + "version": "4.14.178", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", + "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==" }, "@types/lodash.clonedeep": { "version": "4.5.6", @@ -41881,13 +42160,13 @@ } }, "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", "requires": { "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, @@ -46526,6 +46805,18 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "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" + } + }, "ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -46715,6 +47006,12 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, + "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==", + "dev": true + }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -47744,6 +48041,24 @@ "schema-utils": "^0.4.5" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "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" + } + }, + "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==", + "dev": true + }, "schema-utils": { "version": "0.4.7", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", @@ -50870,6 +51185,24 @@ "requires": { "ajv": "^6.12.3", "har-schema": "^2.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" + } + }, + "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==" + } } }, "hard-rejection": { @@ -54268,9 +54601,9 @@ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "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": { "version": "1.0.1", @@ -55956,6 +56289,24 @@ "webpack-sources": "^1.1.0" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "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" + } + }, + "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==", + "dev": true + }, "normalize-url": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", @@ -56759,6 +57110,24 @@ "schema-utils": "^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==", + "dev": true, + "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" + } + }, + "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==", + "dev": true + }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -57858,6 +58227,24 @@ "schema-utils": "^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==", + "dev": true, + "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" + } + }, + "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==", + "dev": true + }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -59885,6 +60272,11 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -60294,6 +60686,26 @@ "@types/json-schema": "^7.0.5", "ajv": "^6.12.4", "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==", + "dev": true, + "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" + } + }, + "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==", + "dev": true + } } }, "select-hose": { @@ -61502,6 +61914,24 @@ "schema-utils": "^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==", + "dev": true, + "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" + } + }, + "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==", + "dev": true + }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -61743,12 +62173,30 @@ "string-width": "^3.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==", + "dev": true, + "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" + } + }, "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, + "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==", + "dev": true + }, "slice-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", @@ -63225,6 +63673,24 @@ "schema-utils": "^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==", + "dev": true, + "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" + } + }, + "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==", + "dev": true + }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -63729,6 +64195,18 @@ "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", "dev": true }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "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" + } + }, "eslint-scope": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", @@ -63762,6 +64240,12 @@ "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", "dev": true }, + "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==", + "dev": true + }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -63871,6 +64355,18 @@ "yargs": "^13.3.2" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "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" + } + }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -64049,6 +64545,12 @@ "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", "dev": true }, + "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==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/packages/rich-text-types/package.json b/packages/rich-text-types/package.json index 25eefbb1..ab3befde 100644 --- a/packages/rich-text-types/package.json +++ b/packages/rich-text-types/package.json @@ -24,6 +24,9 @@ "lint": "tslint -t codeFrame '@(src|bin)/*.ts'", "generate-json-schema": "ts-node -O '{\"module\": \"commonjs\"}' ./tools/jsonSchemaGen" }, + "dependencies": { + "ajv": "^8.8.2" + }, "devDependencies": { "@types/jest": "^27.0.1", "@types/node": "^14.17.14", diff --git a/packages/rich-text-types/src/__test__/schemaConstraints.test.ts b/packages/rich-text-types/src/__test__/schemaConstraints.test.ts index c58efc6a..013badb4 100644 --- a/packages/rich-text-types/src/__test__/schemaConstraints.test.ts +++ b/packages/rich-text-types/src/__test__/schemaConstraints.test.ts @@ -1,4 +1,4 @@ -import BLOCKS from '../blocks'; +import { BLOCKS } from '../blocks'; import { VOID_BLOCKS, CONTAINERS, TEXT_CONTAINERS } from '../schemaConstraints'; const allKnownBlocks = Object.values(BLOCKS); diff --git a/packages/rich-text-types/src/__test__/validation.test.ts b/packages/rich-text-types/src/__test__/validation.test.ts new file mode 100644 index 00000000..f4089906 --- /dev/null +++ b/packages/rich-text-types/src/__test__/validation.test.ts @@ -0,0 +1,720 @@ +import { BLOCKS } from '../blocks'; +import { INLINES } from '../inlines'; +import { validateRichTextDocument } from '../validation'; + +const document = (args: any, ...content: any) => ({ + nodeType: BLOCKS.DOCUMENT, + data: {}, + content, + ...args, +}); + +const node = (nodeType: string, args?: any, ...content: any) => ({ + nodeType, + data: {}, + content, + ...args, +}); + +const text = (value = '', args?: any) => ({ + nodeType: 'text', + data: {}, + marks: [], + value, + ...args, +}); + +const topLevelBlocks = [ + BLOCKS.EMBEDDED_ASSET, + BLOCKS.EMBEDDED_ENTRY, + BLOCKS.HEADING_1, + BLOCKS.HEADING_2, + BLOCKS.HEADING_3, + BLOCKS.HEADING_4, + BLOCKS.HEADING_5, + BLOCKS.HEADING_6, + BLOCKS.HR, + BLOCKS.OL_LIST, + BLOCKS.PARAGRAPH, + BLOCKS.QUOTE, + BLOCKS.TABLE, + BLOCKS.UL_LIST, +].sort(); + +const listBlocks = [ + BLOCKS.EMBEDDED_ASSET, + BLOCKS.EMBEDDED_ENTRY, + BLOCKS.HEADING_1, + BLOCKS.HEADING_2, + BLOCKS.HEADING_3, + BLOCKS.HEADING_4, + BLOCKS.HEADING_5, + BLOCKS.HEADING_6, + BLOCKS.HR, + BLOCKS.OL_LIST, + BLOCKS.PARAGRAPH, + BLOCKS.QUOTE, + BLOCKS.UL_LIST, +].sort(); + +describe('validateRichTextDocument', () => { + describe('root node', () => { + it('fails if it is not document node', () => { + const value = node(BLOCKS.PARAGRAPH); + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'enum', + instancePath: '/nodeType', + message: 'must be equal to one of the allowed values', + params: { + allowedValues: ['document'], + }, + data: BLOCKS.PARAGRAPH, + }), + ]); + }); + + it('does not allow invalid root document shape', () => { + const value: any = { nodeType: BLOCKS.DOCUMENT }; + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'required', + instancePath: '', + message: "must have required property 'content'", + }), + expect.objectContaining({ + keyword: 'required', + instancePath: '', + message: "must have required property 'data'", + }), + ]); + }); + + it('does not allow nested documents', () => { + const value = document( + {}, + node(BLOCKS.PARAGRAPH), + node(BLOCKS.UL_LIST, {}, node(BLOCKS.LIST_ITEM, {}, node(BLOCKS.DOCUMENT))), + ); + + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + message: 'must be equal to one of the allowed values', + keyword: 'enum', + instancePath: '/content/0/nodeType', + params: { + allowedValues: listBlocks, + }, + data: 'document', + }), + ]); + }); + + it('does not allow custom nodeTypes', () => { + const value = document( + {}, + node(BLOCKS.PARAGRAPH, {}, node('custom-type', {}, node(BLOCKS.PARAGRAPH))), + ); + + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'enum', + instancePath: '/content/0/nodeType', + message: 'must be equal to one of the allowed values', + params: { + allowedValues: Object.values(INLINES).sort(), + }, + data: 'custom-type', + }), + ]); + }); + }); + + describe('direct children of root node', () => { + it('validate with blocks as direct children of the root node', () => { + const value = document({}, node(BLOCKS.PARAGRAPH, {})); + const errorsResult = validateRichTextDocument(value); + expect(errorsResult).toEqual([]); + }); + + it(`fails with ${BLOCKS.LIST_ITEM} as immediate child of root node`, () => { + const value = document({}, node(BLOCKS.LIST_ITEM, {}, node(BLOCKS.PARAGRAPH, {}, text('')))); + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'enum', + message: 'must be equal to one of the allowed values', + instancePath: '/content/0/nodeType', + params: { + allowedValues: topLevelBlocks, + }, + data: BLOCKS.LIST_ITEM, + }), + ]); + }); + + for (const dependentNode of [BLOCKS.TABLE_ROW, BLOCKS.TABLE_CELL, BLOCKS.TABLE_HEADER_CELL]) { + it(`fails with ${dependentNode} as immediate child of root node`, () => { + const value = document({}, node(dependentNode, {}, node(BLOCKS.PARAGRAPH, {}, text('')))); + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'enum', + message: 'must be equal to one of the allowed values', + instancePath: '/content/0/nodeType', + params: { + allowedValues: topLevelBlocks, + }, + data: dependentNode, + }), + ]); + }); + } + + it('fail with inlines as direct children', () => { + const value = document({}, node(INLINES.HYPERLINK, { data: { uri: '' } })); + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'enum', + instancePath: '/content/0/nodeType', + message: 'must be equal to one of the allowed values', + params: { + allowedValues: topLevelBlocks, + }, + data: INLINES.HYPERLINK, + }), + ]); + }); + + it('fail with text as direct children', () => { + const value = document({}, text()); + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'enum', + instancePath: '/content/0/nodeType', + message: `must be equal to one of the allowed values`, + params: { + allowedValues: topLevelBlocks, + }, + data: 'text', + }), + ]); + }); + + it('fail with text and inline as direct children', () => { + const value = document( + {}, + text(), + node(BLOCKS.PARAGRAPH), + node(INLINES.ASSET_HYPERLINK, { data: { target: {} } }), + ); + + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'enum', + instancePath: '/content/0/nodeType', + message: `must be equal to one of the allowed values`, + params: { + allowedValues: topLevelBlocks, + }, + data: 'text', + }), + ]); + }); + }); + + describe('children constraints', () => { + for (const listNode of [BLOCKS.UL_LIST, BLOCKS.OL_LIST]) { + it(`allows only ${BLOCKS.LIST_ITEM} as immediate children of list nodes (${listNode})`, () => { + const value = document({}, node(listNode, {}, node(BLOCKS.PARAGRAPH, {}, text('')))); + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'enum', + instancePath: '/content/0/nodeType', + message: `must be equal to one of the allowed values`, + params: { + allowedValues: [BLOCKS.LIST_ITEM], + }, + data: BLOCKS.PARAGRAPH, + }), + ]); + }); + } + + it(`allows only ${BLOCKS.TABLE_ROW} as immediate children of table nodes`, () => { + const value = document({}, node(BLOCKS.TABLE, {}, node(BLOCKS.PARAGRAPH, {}, text('')))); + + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'enum', + instancePath: '/content/0/nodeType', + message: `must be equal to one of the allowed values`, + params: { + allowedValues: [BLOCKS.TABLE_ROW], + }, + data: BLOCKS.PARAGRAPH, + }), + ]); + }); + + it(`allows only table cell nodes as immediate children of a ${BLOCKS.TABLE_ROW} nodes`, () => { + const value = document( + {}, + node(BLOCKS.TABLE, {}, node(BLOCKS.TABLE_ROW, {}, node(BLOCKS.PARAGRAPH, {}, text('')))), + ); + + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'enum', + instancePath: '/content/0/nodeType', + message: `must be equal to one of the allowed values`, + params: { + allowedValues: [BLOCKS.TABLE_CELL, BLOCKS.TABLE_HEADER_CELL], + }, + data: BLOCKS.PARAGRAPH, + }), + ]); + }); + + it(`allows only block nodes as direct children of ${BLOCKS.LIST_ITEM} nodes`, () => { + const value = document({}, node(BLOCKS.UL_LIST, {}, node(BLOCKS.LIST_ITEM, {}, text('')))); + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'enum', + instancePath: '/content/0/nodeType', + message: `must be equal to one of the allowed values`, + params: { + allowedValues: listBlocks, + }, + data: 'text', + }), + ]); + }); + + it(`allows only paragraphs as direct children of ${BLOCKS.TABLE_CELL} nodes`, () => { + const value = document( + {}, + node(BLOCKS.TABLE, {}, node(BLOCKS.TABLE_ROW, {}, node(BLOCKS.TABLE_CELL, {}, text('')))), + ); + + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'enum', + instancePath: '/content/0/nodeType', + message: `must be equal to one of the allowed values`, + params: { + allowedValues: ['paragraph'], + }, + data: 'text', + }), + ]); + }); + + it('allows inlines to contain only inline or text nodes', () => { + const value = document( + {}, + node( + BLOCKS.PARAGRAPH, + {}, + node(INLINES.HYPERLINK, { data: { uri: '' } }, node(BLOCKS.PARAGRAPH, {}, text(''))), + ), + ); + + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'enum', + instancePath: '/content/0/nodeType', + message: `must be equal to one of the allowed values`, + params: { + allowedValues: ['text'], + }, + data: BLOCKS.PARAGRAPH, + }), + ]); + }); + + it(`allows only ${BLOCKS.PARAGRAPH} as children of ${BLOCKS.QUOTE}`, () => { + const value = document( + {}, + node(BLOCKS.QUOTE, {}, node(INLINES.HYPERLINK, { data: { uri: '' } })), + ); + + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'enum', + message: `must be equal to one of the allowed values`, + instancePath: '/content/0/nodeType', + data: INLINES.HYPERLINK, + params: { + allowedValues: [BLOCKS.PARAGRAPH], + }, + }), + ]); + }); + }); + + describe('node properties', () => { + describe('blocks and inlines', () => { + it('validate with required properties', () => { + const value: any = { + nodeType: BLOCKS.DOCUMENT, + data: {}, + content: [], + }; + + const errorsResult = validateRichTextDocument(value); + expect(errorsResult).toEqual([]); + }); + + it('fail without required `nodeType` property', () => { + const value = document({}, node(BLOCKS.PARAGRAPH, { data: {}, nodeType: null }, text(''))); + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'enum', + instancePath: '/content/0/nodeType', + message: 'must be equal to one of the allowed values', + params: { + allowedValues: topLevelBlocks, + }, + data: null, + }), + ]); + }); + + it('fail without required `content` property', () => { + const value = document( + {}, + node( + BLOCKS.OL_LIST, + {}, + node(BLOCKS.LIST_ITEM, {}, { nodeType: BLOCKS.PARAGRAPH, data: {} }), + ), + ); + + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'required', + instancePath: '', + message: "must have required property 'content'", + }), + ]); + }); + + it('fail with invalid `content` property', () => { + // We already test `undefined` value above (that would throw a "required" error) + // that's why it's not included in the list. + ['hello', 123, true, null].forEach(content => { + const value: any = { + nodeType: 'document', + data: {}, + content, + }; + + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'type', + instancePath: '/content', + message: 'must be array', + schema: 'array', + data: content, + }), + ]); + }); + }); + + it('fail with unknown/custom properties', () => { + const value = document({ + data: {}, + content: [], + customProp: 1, + }); + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'additionalProperties', + instancePath: '', + params: { + additionalProperty: 'customProp', + }, + message: 'must NOT have additional properties', + }), + ]); + }); + + it('fail with missing & unknown/custom properties', () => { + const value = document({ + data: {}, + customProp: 1, + content: null, + }); + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'additionalProperties', + instancePath: '', + params: { + additionalProperty: 'customProp', + }, + message: `must NOT have additional properties`, + }), + expect.objectContaining({ + keyword: 'type', + instancePath: '/content', + message: 'must be array', + data: null, + schema: 'array', + }), + ]); + }); + }); + + describe('text nodes', () => { + it('validate with required properties', () => { + const value = document({}, node(BLOCKS.PARAGRAPH, {}, text(''))); + const errorResults = validateRichTextDocument(value); + expect(errorResults).toEqual([]); + }); + + it('fail without required properties', () => { + const value = document({}, node(BLOCKS.PARAGRAPH, {}, text('', { data: null }))); + const errorsResult = validateRichTextDocument(value); + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'type', + instancePath: '/data', + message: 'must be object', + schema: 'object', + data: null, + }), + ]); + }); + + it('fail without required `value` property', () => { + const value = document({}, node(BLOCKS.PARAGRAPH, {}, text(null))); + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'type', + instancePath: '/value', + message: 'must be string', + schema: 'string', + data: null, + }), + ]); + }); + + it('fail with unknown/custom properties', () => { + const value = document({}, node(BLOCKS.PARAGRAPH, {}, text('', { customProp: true }))); + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'additionalProperties', + instancePath: '', + params: { + additionalProperty: 'customProp', + }, + message: `must NOT have additional properties`, + }), + ]); + }); + + it('fail with missing & unknown/custom properties', () => { + const value = document({}, node(BLOCKS.PARAGRAPH, {}, text(null))); + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'type', + instancePath: '/value', + message: 'must be string', + schema: 'string', + data: null, + }), + ]); + }); + }); + }); + + describe('properties shape', () => { + describe('`data` property', () => { + it('fails with missing `data` property', () => { + const value = document( + {}, + node(BLOCKS.PARAGRAPH, {}, { nodeType: INLINES.HYPERLINK, content: [] }), + ); + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'required', + message: "must have required property 'data'", + }), + ]); + }); + + it(`fails with invalid properties for ${INLINES.HYPERLINK} nodeTypes`, () => { + const value = document( + {}, + node( + BLOCKS.PARAGRAPH, + {}, + { + nodeType: INLINES.HYPERLINK, + data: {}, + content: [text('')], + }, + ), + ); + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'required', + instancePath: '/data', + message: "must have required property 'uri'", + }), + ]); + }); + + it(`fails with invalid properties for ${INLINES.ASSET_HYPERLINK} nodeTypes`, () => { + const value = document( + {}, + node( + BLOCKS.PARAGRAPH, + {}, + { nodeType: INLINES.ASSET_HYPERLINK, data: {}, content: [text()] }, + ), + ); + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'required', + instancePath: '/data', + message: "must have required property 'target'", + }), + ]); + }); + + it(`fails with invalid properties for ${INLINES.ENTRY_HYPERLINK} nodeTypes`, () => { + const value = document( + {}, + node( + BLOCKS.PARAGRAPH, + {}, + { nodeType: INLINES.ENTRY_HYPERLINK, data: {}, content: [text()] }, + ), + ); + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'required', + instancePath: '/data', + message: "must have required property 'target'", + }), + ]); + }); + + it(`fails with invalid properties of ${INLINES.EMBEDDED_ENTRY} nodeTypes`, () => { + const value = document( + {}, + node( + BLOCKS.PARAGRAPH, + {}, + { nodeType: INLINES.EMBEDDED_ENTRY, data: {}, content: [text()] }, + ), + ); + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'required', + instancePath: '/data', + message: "must have required property 'target'", + }), + expect.objectContaining({ + keyword: 'maxItems', + instancePath: '/content', + message: 'must NOT have more than 0 items', + data: [ + { + data: {}, + marks: [], + nodeType: 'text', + value: '', + }, + ], + }), + ]); + }); + + it('fails with unknown/custom properties', () => { + const value = document( + {}, + node( + BLOCKS.PARAGRAPH, + {}, + { + nodeType: INLINES.HYPERLINK, + data: { foo: true, uri: 'https://world.com' }, + content: [text()], + }, + ), + ); + const errorsResult = validateRichTextDocument(value); + + expect(errorsResult).toEqual([ + expect.objectContaining({ + keyword: 'additionalProperties', + instancePath: '/data', + params: { + additionalProperty: 'foo', + }, + message: 'must NOT have additional properties', + }), + ]); + }); + }); + }); +}); diff --git a/packages/rich-text-types/src/blocks.ts b/packages/rich-text-types/src/blocks.ts index f75dcf31..69842daa 100644 --- a/packages/rich-text-types/src/blocks.ts +++ b/packages/rich-text-types/src/blocks.ts @@ -1,7 +1,7 @@ /** * Map of all Contentful block types. Blocks contain inline or block nodes. */ -enum BLOCKS { +export enum BLOCKS { DOCUMENT = 'document', PARAGRAPH = 'paragraph', @@ -27,5 +27,3 @@ enum BLOCKS { TABLE_CELL = 'table-cell', TABLE_HEADER_CELL = 'table-header-cell', } - -export default BLOCKS; diff --git a/packages/rich-text-types/src/emptyDocument.ts b/packages/rich-text-types/src/emptyDocument.ts index ff7d6a57..65d6f5a9 100644 --- a/packages/rich-text-types/src/emptyDocument.ts +++ b/packages/rich-text-types/src/emptyDocument.ts @@ -1,5 +1,5 @@ import { Document } from './types'; -import BLOCKS from './blocks'; +import { BLOCKS } from './blocks'; /** * A rich text document considered to be empty. diff --git a/packages/rich-text-types/src/helpers.ts b/packages/rich-text-types/src/helpers.ts index 30904895..7fab56b4 100644 --- a/packages/rich-text-types/src/helpers.ts +++ b/packages/rich-text-types/src/helpers.ts @@ -1,6 +1,6 @@ import { Node, Block, Inline, Text } from './types'; -import BLOCKS from './blocks'; -import INLINES from './inlines'; +import { BLOCKS } from './blocks'; +import { INLINES } from './inlines'; /** * Checks if the node is an instance of Inline. diff --git a/packages/rich-text-types/src/index.ts b/packages/rich-text-types/src/index.ts index 1d2bef47..d7b63c02 100644 --- a/packages/rich-text-types/src/index.ts +++ b/packages/rich-text-types/src/index.ts @@ -1,8 +1,8 @@ import 'core-js/features/object/values'; import 'core-js/features/array/includes'; -export { default as BLOCKS } from './blocks'; -export { default as INLINES } from './inlines'; +export { BLOCKS } from './blocks'; +export { INLINES } from './inlines'; export { default as MARKS } from './marks'; export * from './schemaConstraints'; @@ -14,3 +14,4 @@ export { default as EMPTY_DOCUMENT } from './emptyDocument'; import * as helpers from './helpers'; export { helpers }; +export { validateRichTextDocument } from './validation'; diff --git a/packages/rich-text-types/src/inlines.ts b/packages/rich-text-types/src/inlines.ts index 045c6f2a..fce50916 100644 --- a/packages/rich-text-types/src/inlines.ts +++ b/packages/rich-text-types/src/inlines.ts @@ -1,11 +1,9 @@ /** * Map of all Contentful inline types. Inline contain inline or text nodes. */ -enum INLINES { +export enum INLINES { HYPERLINK = 'hyperlink', ENTRY_HYPERLINK = 'entry-hyperlink', ASSET_HYPERLINK = 'asset-hyperlink', EMBEDDED_ENTRY = 'embedded-entry-inline', } - -export default INLINES; diff --git a/packages/rich-text-types/src/nodeTypes.ts b/packages/rich-text-types/src/nodeTypes.ts index bb8c0f4c..c3b04063 100644 --- a/packages/rich-text-types/src/nodeTypes.ts +++ b/packages/rich-text-types/src/nodeTypes.ts @@ -1,5 +1,5 @@ -import BLOCKS from './blocks'; -import INLINES from './inlines'; +import { BLOCKS } from './blocks'; +import { INLINES } from './inlines'; import { Block, Inline, Text, ListItemBlock } from './types'; type EmptyNodeData = {}; diff --git a/packages/rich-text-types/src/schemaConstraints.ts b/packages/rich-text-types/src/schemaConstraints.ts index e693c117..b3e6bd33 100644 --- a/packages/rich-text-types/src/schemaConstraints.ts +++ b/packages/rich-text-types/src/schemaConstraints.ts @@ -1,5 +1,5 @@ -import BLOCKS from './blocks'; -import INLINES from './inlines'; +import { BLOCKS } from './blocks'; +import { INLINES } from './inlines'; export type TopLevelBlockEnum = | BLOCKS.PARAGRAPH diff --git a/packages/rich-text-types/src/schemas/__test__/schemas.test.ts b/packages/rich-text-types/src/schemas/__test__/schemas.test.ts index 5935b280..54278530 100644 --- a/packages/rich-text-types/src/schemas/__test__/schemas.test.ts +++ b/packages/rich-text-types/src/schemas/__test__/schemas.test.ts @@ -1,5 +1,5 @@ -import BLOCKS from '../../blocks'; -import INLINES from '../../inlines'; +import { BLOCKS } from '../../blocks'; +import { INLINES } from '../../inlines'; import { getSchemaWithNodeType } from '../index'; const matchesSnapshot = (nodeType: string): void => { diff --git a/packages/rich-text-types/src/types.ts b/packages/rich-text-types/src/types.ts index b7b6b083..3c9ace0c 100644 --- a/packages/rich-text-types/src/types.ts +++ b/packages/rich-text-types/src/types.ts @@ -1,5 +1,5 @@ -import BLOCKS from './blocks'; -import INLINES from './inlines'; +import { BLOCKS } from './blocks'; +import { INLINES } from './inlines'; import { TopLevelBlockEnum, ListItemBlockEnum } from './schemaConstraints'; /** diff --git a/packages/rich-text-types/src/validation.ts b/packages/rich-text-types/src/validation.ts new file mode 100644 index 00000000..9aca7859 --- /dev/null +++ b/packages/rich-text-types/src/validation.ts @@ -0,0 +1,146 @@ +import Ajv, { ErrorObject, ValidateFunction } from 'ajv'; + +import { Node } from './types'; +import { BLOCKS } from './blocks'; +import { isText } from './helpers'; +import { getSchemaWithNodeType } from './schemas'; + +const ajv = new Ajv({ allErrors: true, verbose: true }); + +type AnyNode = Node & { + content?: AnyNode[]; + value?: string; + marks?: string[]; +}; + +type Path = (string | number)[]; + +/** + * Validates a rich text document against our JSON schemas using AJV. + * + * We need to reduce the validation scope to keep AJV from returning error + * messages with obscure code paths. + * + * Example: + * + * Given a node that accepts children which should match one of multiple + * schemas, having an invalid child node (e.g., with a missing property), AJV + * tries to validate the child node against one of the other schemas. This + * results in misleading / cryptic error messages. + * + * This function runs AJV validations against nodes whose children have had + * their properties reset, so that AJV validates only against properties of the + * parent node's nodeType. This is the reasoning behind the `removeChildNodes` + * and `removeGrandChildNodes` helpers. + */ +export function validateRichTextDocument(document: AnyNode): ErrorObject[] { + const validateRootNode = getValidator(BLOCKS.DOCUMENT); + const rootNodeIsValid = validateRootNode(removeGrandChildNodes(document)); + + /** + * Note that this is not the most beautiful / functional implementation + * possible, but since we are validating what could potentially be a + * substantially lengthy (hence: computationally complex) tree, we need to + * constrain both space _and_ memory usage. This is the reasoning behind using + * imperative logic with passed references and in-line mutation. + */ + const errors: ErrorObject[] = []; + + if (rootNodeIsValid) { + validateChildNodes(document, ['content'], errors); + } else { + buildSchemaErrors(validateRootNode, [], errors); + } + + return errors; +} + +/** + * Validates each child of a root node, continually (recursively) passing down + * the path from the originating root node. + */ +function validateChildNodes(node: AnyNode, path: Path, errors: ErrorObject[]) { + for (let i = 0; i < node.content.length; i++) { + validateNode(node.content[i], [...path, i], errors); + } +} + +function validateNode(node: AnyNode, path: Path, errors: ErrorObject[]) { + const validateSchema = getValidator(node.nodeType); + const isValid = validateSchema(removeGrandChildNodes(resetChildNodes(node))); + if (!isValid) { + buildSchemaErrors(validateSchema, path, errors); + return; + } + + if (!isLeafNode(node)) { + validateChildNodes(node, [...path, 'content'], errors); + } +} + +/** + * Gets the validating function for the schema from the AJV instance. Note that + * AJV caches the schema under the hood, while `getSchemaWithNodeType` is + * returning JSON objects from a Webpack-ified dictionary object, so there is no + * way to further optimize here (even though it may look otherwise). + */ +function getValidator(nodeType: string): ValidateFunction { + const schema = getSchemaWithNodeType(nodeType); + + return ajv.compile(schema); +} + +function isConstraintError(error: ErrorObject) { + return error.keyword === 'enum' || error.keyword === 'anyOf'; +} + +function buildSchemaErrors(validateSchema: ValidateFunction, path: Path, errors: ErrorObject[]) { + const schemaErrors = validateSchema.errors; + const constraintError = schemaErrors.find(isConstraintError); + + if (constraintError) { + errors.push(constraintError); + return; + } + + errors.push(...schemaErrors); +} + +function resetChildNodes(node: AnyNode) { + const { content } = node; + if (isLeafNode(node)) { + return node; + } + + return Object.assign({}, node, { content: content.map(resetNode) }); +} + +function resetNode(node: AnyNode): AnyNode { + const { nodeType } = node; + if (isText(node)) { + return { nodeType, data: {}, value: '', marks: [] }; + } + + return { nodeType, data: {}, content: [] }; +} + +function removeGrandChildNodes(node: AnyNode) { + const { content } = node; + if (isLeafNode(node)) { + return node; + } + + return Object.assign({}, node, { content: content.map(removeChildNodes) }); +} + +function removeChildNodes(node: AnyNode) { + if (isText(node)) { + return node; + } + + return Object.assign({}, node, { content: [] }); +} + +function isLeafNode(node: AnyNode) { + return isText(node) || !Array.isArray(node.content); +}