From f38fe21c2525fdc329e804ad6f49f58837b27d67 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Fri, 22 Mar 2024 14:23:17 +0100 Subject: [PATCH 01/43] feat(plate): add as new widget, basic marks --- dev-test/config.yml | 2 +- package-lock.json | 1413 ++++++++++++++++- packages/decap-cms-app/src/extensions.js | 2 + .../decap-cms-widget-markdown/package.json | 7 +- packages/decap-cms-widget-richtext/README.md | 9 + .../decap-cms-widget-richtext/package.json | 44 + .../src/RichtextControl.js | 30 + .../src/RichtextControl/VisualEditor.js | 108 ++ .../RichtextControl/components/BoldLeaf.js | 12 + .../RichtextControl/components/CodeLeaf.js | 21 + .../src/RichtextControl/components/Editor.js | 26 + .../RichtextControl/components/ItalicLeaf.js | 12 + .../components/MarkToolbarButton.js | 14 + .../components/ParagraphElement.js | 10 + .../src/RichtextControl/components/Toolbar.js | 120 ++ .../components/ToolbarButton.js | 49 + .../src/RichtextPreview.js | 5 + .../decap-cms-widget-richtext/src/index.js | 16 + .../decap-cms-widget-richtext/src/schema.js | 35 + .../decap-cms-widget-richtext/src/styles.js | 13 + 20 files changed, 1856 insertions(+), 92 deletions(-) create mode 100644 packages/decap-cms-widget-richtext/README.md create mode 100644 packages/decap-cms-widget-richtext/package.json create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/BoldLeaf.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/CodeLeaf.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/Editor.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/ItalicLeaf.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/MarkToolbarButton.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/ParagraphElement.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/ToolbarButton.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextPreview.js create mode 100644 packages/decap-cms-widget-richtext/src/index.js create mode 100644 packages/decap-cms-widget-richtext/src/schema.js create mode 100644 packages/decap-cms-widget-richtext/src/styles.js diff --git a/dev-test/config.yml b/dev-test/config.yml index 31c7f12ae97a..059201b3fcad 100644 --- a/dev-test/config.yml +++ b/dev-test/config.yml @@ -50,7 +50,7 @@ collections: # A list of collections the CMS should be able to edit required: false tagname: '' - - { label: 'Body', name: 'body', widget: 'markdown', hint: 'Main content goes here.' } + - { label: 'Body', name: 'body', widget: 'richtext', hint: 'Main content goes here.' } - name: 'restaurants' # Used in routes, ie.: /admin/collections/:slug/edit label: 'Restaurants' # Used in the UI diff --git a/package-lock.json b/package-lock.json index 397f417bd711..74573e98ca49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3071,7 +3071,6 @@ }, "node_modules/@floating-ui/core": { "version": "1.5.0", - "dev": true, "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.1.3" @@ -3079,13 +3078,26 @@ }, "node_modules/@floating-ui/dom": { "version": "1.5.3", - "dev": true, "license": "MIT", "dependencies": { "@floating-ui/core": "^1.4.2", "@floating-ui/utils": "^0.1.3" } }, + "node_modules/@floating-ui/react": { + "version": "0.22.3", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.22.3.tgz", + "integrity": "sha512-RlF+7yU3/abTZcUez44IHoEH89yDHHonkYzZocynTWbl6J6MiMINMbyZSmSKdRKdadrC+MwQLdEexu++irvZhQ==", + "dependencies": { + "@floating-ui/react-dom": "^1.3.0", + "aria-hidden": "^1.1.3", + "tabbable": "^6.0.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@floating-ui/react-dom": { "version": "2.0.2", "dev": true, @@ -3098,9 +3110,20 @@ "react-dom": ">=16.8.0" } }, + "node_modules/@floating-ui/react/node_modules/@floating-ui/react-dom": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.3.0.tgz", + "integrity": "sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==", + "dependencies": { + "@floating-ui/dom": "^1.2.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@floating-ui/utils": { "version": "0.1.6", - "dev": true, "license": "MIT" }, "node_modules/@hapi/address": { @@ -5094,7 +5117,6 @@ }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.0.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -5407,7 +5429,6 @@ }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -7663,66 +7684,975 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": { - "version": "11.1.0", - "dev": true, - "license": "MIT", + "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@udecode/cn": { + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/@udecode/cn/-/cn-29.0.1.tgz", + "integrity": "sha512-U41vXvTBKU+06CiQivy4pIWB7RzfaB3DlqkQMNv8UNK164pJhM3v6P0D45kFpbU2uOSOCGpYRSo4kMp9y8RtcQ==", + "dependencies": { + "@udecode/react-utils": "29.0.1" + }, + "peerDependencies": { + "class-variance-authority": ">=0.7.0", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "tailwind-merge": ">=2.2.0" + } + }, + "node_modules/@udecode/plate": { + "version": "30.9.4", + "resolved": "https://registry.npmjs.org/@udecode/plate/-/plate-30.9.4.tgz", + "integrity": "sha512-HSI27P/sqU+4mrK7YsRGDusmDLpMjT/Gp+Mnuj70/7q1kYKBXP4yxjgEa2ikCnoNXRMsDYhUKS2EOSHfSYy9wA==", + "dependencies": { + "@udecode/plate-alignment": "30.5.3", + "@udecode/plate-autoformat": "30.5.3", + "@udecode/plate-basic-elements": "30.7.0", + "@udecode/plate-basic-marks": "30.5.3", + "@udecode/plate-block-quote": "30.5.3", + "@udecode/plate-break": "30.5.3", + "@udecode/plate-code-block": "30.7.0", + "@udecode/plate-combobox": "30.5.3", + "@udecode/plate-comments": "30.5.3", + "@udecode/plate-common": "30.4.5", + "@udecode/plate-diff": "30.9.0", + "@udecode/plate-find-replace": "30.5.3", + "@udecode/plate-floating": "30.5.3", + "@udecode/plate-font": "30.5.3", + "@udecode/plate-heading": "30.5.3", + "@udecode/plate-highlight": "30.5.3", + "@udecode/plate-horizontal-rule": "30.5.3", + "@udecode/plate-indent": "30.5.3", + "@udecode/plate-indent-list": "30.5.3", + "@udecode/plate-kbd": "30.5.3", + "@udecode/plate-line-height": "30.5.3", + "@udecode/plate-link": "30.9.4", + "@udecode/plate-list": "30.5.3", + "@udecode/plate-media": "30.5.3", + "@udecode/plate-mention": "30.5.3", + "@udecode/plate-node-id": "30.5.3", + "@udecode/plate-normalizers": "30.5.3", + "@udecode/plate-paragraph": "30.5.3", + "@udecode/plate-reset-node": "30.5.3", + "@udecode/plate-resizable": "30.5.3", + "@udecode/plate-select": "30.5.3", + "@udecode/plate-serializer-csv": "30.9.4", + "@udecode/plate-serializer-docx": "30.9.4", + "@udecode/plate-serializer-html": "30.5.3", + "@udecode/plate-serializer-md": "30.9.4", + "@udecode/plate-suggestion": "30.9.0", + "@udecode/plate-tabbable": "30.5.3", + "@udecode/plate-table": "30.9.4", + "@udecode/plate-toggle": "30.9.2", + "@udecode/plate-trailing-block": "30.5.3" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-alignment": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-alignment/-/plate-alignment-30.5.3.tgz", + "integrity": "sha512-zQK2pA5lhUsxCJtNP0eDytfAHjUzqd1znhQh6CzRgoZeALtFkEKee/v329iEv7ZdVFWGtooQoaNraq8PF8+7yg==", + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-autoformat": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-autoformat/-/plate-autoformat-30.5.3.tgz", + "integrity": "sha512-yvRS9Fw3eMr9djRExW8sf6BSc+BYMp0G6tCXHIXvTOfne55+WU4D8eFlrcjy94EhGGddCUS45Q/YvDALYAsTpA==", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-basic-elements": { + "version": "30.7.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-basic-elements/-/plate-basic-elements-30.7.0.tgz", + "integrity": "sha512-FYriqwvthx+3BPljePD/eVBszDyi1g1i2RMSs1PlXxqcDlXBuaUtWurWpnViJLK0hzfGagTzaOz6soEMlB5qZw==", + "dependencies": { + "@udecode/plate-block-quote": "30.5.3", + "@udecode/plate-code-block": "30.7.0", + "@udecode/plate-heading": "30.5.3", + "@udecode/plate-paragraph": "30.5.3" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-basic-marks": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-basic-marks/-/plate-basic-marks-30.5.3.tgz", + "integrity": "sha512-/p5WVEz20mWVg+HNrMemDLJ/n0AM2e0GZwn5NTQULXa5i9DcqqcZOXlOayXhxjG4P9/KV9nPdOttQtxti/Sr3g==", + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-block-quote": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-block-quote/-/plate-block-quote-30.5.3.tgz", + "integrity": "sha512-InFQ/IaS2BFj74CaDU4V/hlbcefXG3joRBw2cH8QJgbB1t4GSBTW8ZoMDDA6L6N9edBUu8R3vQWQgfZhp303Ig==", + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-break": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-break/-/plate-break-30.5.3.tgz", + "integrity": "sha512-MwJrHw+1qFIs6HOBoaHGV/jaj6JXj0+BzrxT7FOLvdTK5vVFWC21a+WwgOJWZBZ3NVVRpxp0PxxhwpaqtEkKRw==", + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-code-block": { + "version": "30.7.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-code-block/-/plate-code-block-30.7.0.tgz", + "integrity": "sha512-/wodH5+SH9eALLIiUAkcwRE2EO4eIBIe5bIoCYMToe3dwaDF4MVHwBU5jZLzi6cy9osar396CQfPmW1j63MJLQ==", + "dependencies": { + "prismjs": "^1.29.0" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-combobox": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-combobox/-/plate-combobox-30.5.3.tgz", + "integrity": "sha512-bAq3jWFEPwFwsm0NYoBz+vg6w/8NBKk3Az2x1/HOZ4vwoQNhbI6JLLPWnpTabPDDaF1hIvj9kqV+rOIbSS241g==", + "dependencies": { + "downshift": "^6.1.12" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-comments": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-comments/-/plate-comments-30.5.3.tgz", + "integrity": "sha512-DiepkcQ4G+TNTA86fvbuue5sva3XmETkZjYDMsBin/NMn/foiBA2O+SorK3nZ5kCCSZsFgG07UhufnRXFbkN6w==", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-common": { + "version": "30.4.5", + "resolved": "https://registry.npmjs.org/@udecode/plate-common/-/plate-common-30.4.5.tgz", + "integrity": "sha512-p/hF7rvuEqyrxvsfgjaBswv82C/Z1/S5vNj+m33UG91cnPs5sLHbofd5qh7vRgKKfZ/uk028mNpUgemo1bFgbA==", + "dependencies": { + "@udecode/plate-core": "30.4.5", + "@udecode/plate-utils": "30.4.5", + "@udecode/react-utils": "29.0.1", + "@udecode/slate": "25.0.0", + "@udecode/slate-react": "29.0.1", + "@udecode/slate-utils": "25.0.0", + "@udecode/utils": "24.3.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-core": { + "version": "30.4.5", + "resolved": "https://registry.npmjs.org/@udecode/plate-core/-/plate-core-30.4.5.tgz", + "integrity": "sha512-x/X0dCLoWFyC7wEI9hTcVMR8C/xiTkF0w9I5fyhCMg1mXz/y4DB0CMute+hYT0Wz7rqgj9DYT4v8ryrB9fEu9A==", + "dependencies": { + "@udecode/slate": "25.0.0", + "@udecode/slate-react": "29.0.1", + "@udecode/slate-utils": "25.0.0", + "@udecode/utils": "24.3.0", + "clsx": "^1.2.1", + "is-hotkey": "^0.2.0", + "jotai": "^2.6.0", + "jotai-optics": "0.3.1", + "jotai-x": "^1.2.2", + "lodash": "^4.17.21", + "nanoid": "^3.3.6", + "optics-ts": "2.4.1", + "react-hotkeys-hook": "^4.4.1", + "use-deep-compare": "^1.1.0", + "zustand": "^4.4.7", + "zustand-x": "^3.0.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-diff": { + "version": "30.9.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-diff/-/plate-diff-30.9.0.tgz", + "integrity": "sha512-FiChegLUmW4T4iFDMxBCUjExn0C1rgi4rZM57HtJ6phN3EyotPWHXgn2R7cfGhL6vBAY+4E44CS69dVyeKzkcg==", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-find-replace": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-find-replace/-/plate-find-replace-30.5.3.tgz", + "integrity": "sha512-xmv373CgN+fuZR3ESaLS56PkNblNoXSrt5w53MV7I3ISy3vUYU2TZnuvTtn1v59HHSWIZdlIlyTySrsJT0YxdA==", + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-floating": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-floating/-/plate-floating-30.5.3.tgz", + "integrity": "sha512-9KxpZdKLy45a3Z+MJqSGmuJKQrl7CrNsLyUdjKD4Iqd1DIdBwl65dGqTmgI1EycF2jUsWIrgGE3W71f7E5/JdA==", + "dependencies": { + "@floating-ui/core": "^1.3.1", + "@floating-ui/react": "^0.22.3" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-font": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-font/-/plate-font-30.5.3.tgz", + "integrity": "sha512-S40ES4ihWBHF9/BscZGkQCV0b/wNtQcKdaHS6DvQ3JxnzY73XEwwvaJ19FBrPJ2U/CD+i+9SIF5do1bgW5jQuw==", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-heading": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-heading/-/plate-heading-30.5.3.tgz", + "integrity": "sha512-F0SRJSXQtIw6N4AXcENyR01KNSZdflExsQnsEyjDGHZfF0x4bjCt7AeMr79ZDJ+ZAFTrOUKGR53+z2CV2G5ixg==", + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-highlight": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-highlight/-/plate-highlight-30.5.3.tgz", + "integrity": "sha512-20RlkxTNkJTJH+c1fE3D0wwt2WfUBKJMp97I7S0cVkWE7jv/HVzFsXsfdzQeS2Yms4tEA/ZiZQCXoRmqbHvkOw==", + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-horizontal-rule": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-horizontal-rule/-/plate-horizontal-rule-30.5.3.tgz", + "integrity": "sha512-qsAnS9eW/REH+fXXWUy8O27VhYOEFRMhMlXIp83dIDKP2BtXeR2JeVHdM2wa5oEo+3G7o7Qy2DS5Yg51A3wu/Q==", + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-indent": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-indent/-/plate-indent-30.5.3.tgz", + "integrity": "sha512-39V7egkg0Gk0z5nKveAh6gipeH12MQdtSzGphGtpLQqyMF+d+eOLsaZDJsO0XfZlkDTeonA8cDr5ZWERc9SHXQ==", + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-indent-list": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-indent-list/-/plate-indent-list-30.5.3.tgz", + "integrity": "sha512-j9UYOdGf8Qif3X6uMaZrV1yildC9CJSrN4jpscFy5T+80g0ysnb7g6cv4R1FNBzTT6tth3FM7cXacbIuyK7DSQ==", + "dependencies": { + "@udecode/plate-indent": "30.5.3", + "@udecode/plate-list": "30.5.3", + "clsx": "^1.2.1" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-kbd": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-kbd/-/plate-kbd-30.5.3.tgz", + "integrity": "sha512-pPpNbrBAIF6mSTRzR1/WcT0xUS4g/9MmGufAdm8FpFogR6N8CVGZRylqCdx+i0IDHjKHy4mkMLDUP/HVUuWnmQ==", + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-line-height": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-line-height/-/plate-line-height-30.5.3.tgz", + "integrity": "sha512-8y329FUhLcEJQf11+JDs26YCAgH6qhMAEVHwvtQuZKfonHRSFabzkt13f61b2uRZ7piqz8fYWJCEXwGLaPYevA==", + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-link": { + "version": "30.9.4", + "resolved": "https://registry.npmjs.org/@udecode/plate-link/-/plate-link-30.9.4.tgz", + "integrity": "sha512-aBVOPNeI62nzzDFSCxj3NDdpn1XsmsOpcdAleG4ZrtB9o4ndTXDTN1m0NumComk7KPRFAod7NWOuo7KwNMK6JQ==", + "dependencies": { + "@udecode/plate-floating": "30.5.3", + "@udecode/plate-normalizers": "30.5.3" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-list": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-list/-/plate-list-30.5.3.tgz", + "integrity": "sha512-Q6c1hE4oAZp3OkJzoeRIp+ULKcugsNx0Eh4o/yKyWJAx/DzZNPJyuuAyClA9nZMdWv96UAjvEZ75Em3BcFtTwg==", + "dependencies": { + "@udecode/plate-reset-node": "30.5.3", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-media": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-media/-/plate-media-30.5.3.tgz", + "integrity": "sha512-cO4o+257oDMqOtgLMgFxUbFLWov+HUi8GXpd6NbUxPkoGUw24vo3or6Wni+X3DlUJQF0Do5/g9bwZlQcT1IZGw==", + "dependencies": { + "js-video-url-parser": "^0.5.1" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-mention": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-mention/-/plate-mention-30.5.3.tgz", + "integrity": "sha512-OvlgSaHT39dxSRgjS9NsEvbduNbuTWG4KLoEHFJpFgJmGwrVmcPR/AuCpHLiTlDixHVWHJjisp+/imF1eJolQQ==", + "dependencies": { + "@udecode/plate-combobox": "30.5.3" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-node-id": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-node-id/-/plate-node-id-30.5.3.tgz", + "integrity": "sha512-4Dho3kuW/SZWBA5kKzuAaupJbRVMKq8Et0LXWbV3qO5/XtCGsOVZtOaeeIldplLXvYYQp3GN/NqGF6GGdwDziQ==", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-normalizers": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-normalizers/-/plate-normalizers-30.5.3.tgz", + "integrity": "sha512-jf8H5OPPLEYgaoQ0pyHZfSXwzZBxI959BxHy83Y1wvhB5Yykgc8NflNGme3ds/rMED3z90E7QOCL2h1waHNtNQ==", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-paragraph": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-paragraph/-/plate-paragraph-30.5.3.tgz", + "integrity": "sha512-vqvN6Gex1aj189C3ohuq85g6reajYqJMFb4CETGqUTifmKw0ReeJ6a8OYhNqX7v2xE+4gEBm+Z8qO3Z3CnoHqw==", + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-reset-node": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-reset-node/-/plate-reset-node-30.5.3.tgz", + "integrity": "sha512-bBUnE3uMw+jp7zAaZtagCRB9WpBZxJfLdhc1YdqwU1Hmqqy4l0GaH4/oq2QtnN8DtZnOV/PkJlus8tgsP3yzjg==", + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-resizable": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-resizable/-/plate-resizable-30.5.3.tgz", + "integrity": "sha512-fBsWIA8JHDCH8Q7NHkhu300rVTMM8ELcAT/MAD+FyILiLdtYNWA/o9nncjc7HRpyLvsrEKDN2PPCTizKUeaf2Q==", + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-select": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-select/-/plate-select-30.5.3.tgz", + "integrity": "sha512-cVWqikhiwMOBI7IQOCL/vi8HzR4lDfGnlGfeRl4nE6XqbCEmydtKsXzKbsUmxDIv0AhTPgAhWoRjYEv/S7Yoag==", + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-serializer-csv": { + "version": "30.9.4", + "resolved": "https://registry.npmjs.org/@udecode/plate-serializer-csv/-/plate-serializer-csv-30.9.4.tgz", + "integrity": "sha512-kPiHT84/HzUsqZMLajOWYNUPnKremtvGlHrsl5vt9KwMjih7WPPHxsjimfFyE1m0vc97lPdGqDBaxFRD+lxWFA==", + "dependencies": { + "@udecode/plate-table": "30.9.4", + "papaparse": "^5.4.1" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-serializer-docx": { + "version": "30.9.4", + "resolved": "https://registry.npmjs.org/@udecode/plate-serializer-docx/-/plate-serializer-docx-30.9.4.tgz", + "integrity": "sha512-pKNc/HVOO4sOhRlSmG8Ukvyd7A2mG+3NGDGHcTtjnJOitDIb3809aMeMWZvMSCIE7bAljYu3RjAO2righw9l/Q==", + "dependencies": { + "@udecode/plate-heading": "30.5.3", + "@udecode/plate-indent": "30.5.3", + "@udecode/plate-indent-list": "30.5.3", + "@udecode/plate-media": "30.5.3", + "@udecode/plate-paragraph": "30.5.3", + "@udecode/plate-table": "30.9.4", + "validator": "^13.9.0" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-serializer-html": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-serializer-html/-/plate-serializer-html-30.5.3.tgz", + "integrity": "sha512-RESODsZPLiv5efaoOfWph+4+1JPIwNJzhSpqPK7L9TX/wO4M5W8CY9F9IXtdftpn/Xt+WOX1ApPt+7xEWeCQLA==", + "dependencies": { + "html-entities": "^2.4.0" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-serializer-md": { + "version": "30.9.4", + "resolved": "https://registry.npmjs.org/@udecode/plate-serializer-md/-/plate-serializer-md-30.9.4.tgz", + "integrity": "sha512-ah6ccXZ9oTlJ7uUeJEFeS5LCBNG1thCnrPvTmwDjoSqaFFFm/ifHAafRqsREN1vW4yqTRILCOuDVDG2ciccNXg==", + "dependencies": { + "@udecode/plate-basic-marks": "30.5.3", + "@udecode/plate-block-quote": "30.5.3", + "@udecode/plate-code-block": "30.7.0", + "@udecode/plate-heading": "30.5.3", + "@udecode/plate-horizontal-rule": "30.5.3", + "@udecode/plate-link": "30.9.4", + "@udecode/plate-list": "30.5.3", + "@udecode/plate-media": "30.5.3", + "@udecode/plate-paragraph": "30.5.3", + "remark-parse": "^9.0.0", + "unified": "^9.2.2" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-serializer-md/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/@udecode/plate-serializer-md/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@udecode/plate-serializer-md/node_modules/remark-parse": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", + "integrity": "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==", + "dependencies": { + "mdast-util-from-markdown": "^0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@udecode/plate-serializer-md/node_modules/unified": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", + "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", + "dependencies": { + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^2.0.0", + "trough": "^1.0.0", + "vfile": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@udecode/plate-suggestion": { + "version": "30.9.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-suggestion/-/plate-suggestion-30.9.0.tgz", + "integrity": "sha512-Ca1el7ei+pvm1SAh6nIgUQbH4cNpoi3nfkQFvAg96V2G+Yn9dBBcHIJ6w/971hgJ7Lp7tzGaUdlwfCjjdO1J5Q==", + "dependencies": { + "@udecode/plate-diff": "30.9.0", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-tabbable": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-tabbable/-/plate-tabbable-30.5.3.tgz", + "integrity": "sha512-NJaVPOjjG20MPjbvpPmPdYZKkzMF2f7xZKdjpOPMd6SpgvtvV4Cs7iIRDONT6i/i/zyG7tmdSekEOm9Ifxg40w==", + "dependencies": { + "tabbable": "^6.2.0" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-table": { + "version": "30.9.4", + "resolved": "https://registry.npmjs.org/@udecode/plate-table/-/plate-table-30.9.4.tgz", + "integrity": "sha512-53Y2Iu4QbKuvUkuvijmG+758x+gylwdvv0g04w++k061MEjOc0JkVBbsz6Otv4h5vWTbXX2tLooTZK05NyX/Jw==", + "dependencies": { + "@udecode/plate-resizable": "30.5.3", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-toggle": { + "version": "30.9.2", + "resolved": "https://registry.npmjs.org/@udecode/plate-toggle/-/plate-toggle-30.9.2.tgz", + "integrity": "sha512-yY/JtDN+P66T9QjLoYJxIfITe3QEpjkSBCowRc0dhOIBTXlcownktmGpiSSQfbFSHAo5XSzO4JJoDodTkW/xGg==", + "dependencies": { + "@udecode/plate-indent": "30.5.3", + "@udecode/plate-node-id": "30.5.3", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-trailing-block": { + "version": "30.5.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-trailing-block/-/plate-trailing-block-30.5.3.tgz", + "integrity": "sha512-0Vzt2cVXFlFy8gwoFxnTTW8knd7cKt4LFhETXUlVOzPQk0crRZ4s+I/oENwzUQ9v/iF5dsUQ5Tn2MTUJjCn1NQ==", + "peerDependencies": { + "@udecode/plate-common": ">=30.4.5 < 31", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/plate-utils": { + "version": "30.4.5", + "resolved": "https://registry.npmjs.org/@udecode/plate-utils/-/plate-utils-30.4.5.tgz", + "integrity": "sha512-cJ0auswNFxhv/qF9yqrIbgPa3mqxWtLtBQ/N+1zqMfEM3vzWE+4WlHpMJb/SdAC/Dvuc5zzfB26/t2IyhrZp5w==", + "dependencies": { + "@udecode/plate-core": "30.4.5", + "@udecode/react-utils": "29.0.1", + "@udecode/slate": "25.0.0", + "@udecode/slate-react": "29.0.1", + "@udecode/slate-utils": "25.0.0", + "@udecode/utils": "24.3.0", + "clsx": "^1.2.1", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + } + }, + "node_modules/@udecode/react-utils": { + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/@udecode/react-utils/-/react-utils-29.0.1.tgz", + "integrity": "sha512-+bFJFTDsWArFaC4AZFap0VdCvEbu5ZA16avj4xjjdBBho4TiHOZ7RMDliwTUetA3DOm5LG02dmZ1U4ORNC0m3w==", + "dependencies": { + "@radix-ui/react-slot": "^1.0.2", + "@udecode/utils": "24.3.0", + "clsx": "^1.2.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@udecode/slate": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@udecode/slate/-/slate-25.0.0.tgz", + "integrity": "sha512-mGb9nMDwIygLqERwJ8kTOfo3wIxMQ0xLJEPKn09jrshEIxUCyO3mYj8y/5vOMcrzj6yexOsgQ6VNX8ylS3lnIQ==", "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" + "@udecode/utils": "24.3.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "slate": ">=0.94.0", + "slate-history": ">=0.93.0" } }, - "node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "dev": true, - "license": "MIT", + "node_modules/@udecode/slate-react": { + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/@udecode/slate-react/-/slate-react-29.0.1.tgz", + "integrity": "sha512-DOiGXxfL43tVyNg0LneTQGQBW/HkF2srwIM8b0Al/x082HHfo2PP2WkFqPqTh1uGUAa2RBRh9xFKmNkKeuyWSw==", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "@udecode/react-utils": "29.0.1", + "@udecode/slate": "25.0.0", + "@udecode/utils": "24.3.0" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-react": ">=0.99.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "dev": true, - "license": "MIT", + "node_modules/@udecode/slate-utils": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@udecode/slate-utils/-/slate-utils-25.0.0.tgz", + "integrity": "sha512-H8dECl5Tu44Nt946rkSXCJ1yzsc2R9GXSoA9oNIBmcyNo3jTHZOyG/Ocn3RGgfzAK996A43GBD/keNabJEPtQg==", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "@udecode/slate": "25.0.0", + "@udecode/utils": "24.3.0", + "lodash": "^4.17.21" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependencies": { + "slate": ">=0.94.0", + "slate-history": ">=0.93.0" } }, + "node_modules/@udecode/utils": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-24.3.0.tgz", + "integrity": "sha512-/Y2lh/Ih1wx4zN35Ky2Z1G1/5f7cSAS7F6dkhrcbJUnDF0srTidoEIRabK+og/yIK/MCEFfOsQGetoV7Ert5hg==" + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "dev": true, @@ -8526,7 +9456,6 @@ }, "node_modules/aria-hidden": { "version": "1.2.3", - "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -10458,6 +11387,25 @@ "node": ">=0.10.0" } }, + "node_modules/class-variance-authority": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz", + "integrity": "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==", + "dependencies": { + "clsx": "2.0.0" + }, + "funding": { + "url": "https://joebell.co.uk" + } + }, + "node_modules/class-variance-authority/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/clean-regexp": { "version": "1.0.0", "dev": true, @@ -10884,7 +11832,8 @@ }, "node_modules/compute-scroll-into-view": { "version": "1.0.20", - "license": "MIT" + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", + "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==" }, "node_modules/concat-map": { "version": "0.0.1", @@ -12576,6 +13525,10 @@ "resolved": "packages/decap-cms-widget-relation", "link": true }, + "node_modules/decap-cms-widget-richtext": { + "resolved": "packages/decap-cms-widget-richtext", + "link": true + }, "node_modules/decap-cms-widget-select": { "resolved": "packages/decap-cms-widget-select", "link": true @@ -12845,7 +13798,6 @@ }, "node_modules/dequal": { "version": "2.0.3", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -13135,6 +14087,26 @@ "node": ">=12" } }, + "node_modules/downshift": { + "version": "6.1.12", + "resolved": "https://registry.npmjs.org/downshift/-/downshift-6.1.12.tgz", + "integrity": "sha512-7XB/iaSJVS4T8wGFT3WRXmSF1UlBHAA40DshZtkrIscIN+VC+Lh363skLxFTvJwtNgHxAMDGEHT4xsyQFWL+UA==", + "dependencies": { + "@babel/runtime": "^7.14.8", + "compute-scroll-into-view": "^1.0.17", + "prop-types": "^15.7.2", + "react-is": "^17.0.2", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.12.0" + } + }, + "node_modules/downshift/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, "node_modules/duplexer": { "version": "0.1.2", "license": "MIT" @@ -15885,7 +16857,6 @@ }, "node_modules/html-entities": { "version": "2.4.0", - "dev": true, "funding": [ { "type": "github", @@ -17863,6 +18834,53 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/jotai": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.7.1.tgz", + "integrity": "sha512-bsaTPn02nFgWNP6cBtg/htZhCu4s0wxqoklRHePp6l/vlsypR9eLn7diRliwXYWMXDpPvW/LLA2afI8vwgFFaw==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=17.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/jotai-optics": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/jotai-optics/-/jotai-optics-0.3.1.tgz", + "integrity": "sha512-KibUx9IneM2hGWGIYGs/v0KCxU985lg7W2c6dt5RodJCB2XPbmok8rkkLmdVk9+fKsn2shkPMi+AG8XzHgB3+w==", + "peerDependencies": { + "jotai": ">=1.11.0", + "optics-ts": "*" + } + }, + "node_modules/jotai-x": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/jotai-x/-/jotai-x-1.2.2.tgz", + "integrity": "sha512-HaFl3O4aKdBdeTyuzzcvnBWvicXkxl0DBINsqasqWrL7mZov4AAuXUSAsAY817UDwMe1+k77uBazUCFlaiyU3A==", + "peerDependencies": { + "@types/react": ">=17.0.0", + "jotai": ">=2.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/jquery": { "version": "3.7.1", "license": "MIT" @@ -17879,6 +18897,11 @@ "version": "4.0.0", "license": "MIT" }, + "node_modules/js-video-url-parser": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/js-video-url-parser/-/js-video-url-parser-0.5.1.tgz", + "integrity": "sha512-/vwqT67k0AyIGMHAvSOt+n4JfrZWF7cPKgKswDO35yr27GfW4HtjpQVlTx6JLF45QuPm8mkzFHkZgFVnFm4x/w==" + }, "node_modules/js-yaml": { "version": "4.1.0", "license": "MIT", @@ -19214,6 +20237,11 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "dev": true, @@ -19331,6 +20359,14 @@ "node": ">=10" } }, + "node_modules/lucide-react": { + "version": "0.331.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.331.0.tgz", + "integrity": "sha512-CHFJ0ve9vaZ7bB2VRAl27SlX1ELh6pfNC0jS96qGpPEEzLkLDGq4pDBFU8RhOoRMqsjXqTzLm9U6bZ1OcIHq7Q==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/lz-string": { "version": "1.5.0", "dev": true, @@ -20572,7 +21608,6 @@ }, "node_modules/nanoid": { "version": "3.3.6", - "dev": true, "funding": [ { "type": "github", @@ -25196,6 +26231,11 @@ "opener": "bin/opener-bin.js" } }, + "node_modules/optics-ts": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/optics-ts/-/optics-ts-2.4.1.tgz", + "integrity": "sha512-HaYzMHvC80r7U/LqAd4hQyopDezC60PO2qF5GuIwALut2cl5rK1VWHsqTp0oqoJJWjiv6uXKqsO+Q2OO0C3MmQ==" + }, "node_modules/optimism": { "version": "0.10.3", "license": "MIT", @@ -25746,6 +26786,11 @@ "version": "1.0.11", "license": "(MIT AND Zlib)" }, + "node_modules/papaparse": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + }, "node_modules/parent-module": { "version": "1.0.1", "license": "MIT", @@ -26433,6 +27478,14 @@ "node": ">= 0.8" } }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/proc-log": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", @@ -26555,6 +27608,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-compare": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.6.0.tgz", + "integrity": "sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw==" + }, "node_modules/proxy-from-env": { "version": "1.0.0", "dev": true, @@ -26863,6 +27921,15 @@ "react-dom": ">= 16.3" } }, + "node_modules/react-hotkeys-hook": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.5.0.tgz", + "integrity": "sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==", + "peerDependencies": { + "react": ">=16.8.1", + "react-dom": ">=16.8.1" + } + }, "node_modules/react-immutable-proptypes": { "version": "2.2.0", "license": "MIT", @@ -27345,6 +28412,29 @@ "react": ">=16.8.0" } }, + "node_modules/react-tracked": { + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/react-tracked/-/react-tracked-1.7.14.tgz", + "integrity": "sha512-6UMlgQeRAGA+uyYzuQGm7kZB6ZQYFhc7sntgP7Oxwwd6M0Ud/POyb4K3QWT1eXvoifSa80nrAWnXWFGpOvbwkw==", + "dependencies": { + "proxy-compare": "2.6.0", + "use-context-selector": "1.4.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": "*", + "react-native": "*", + "scheduler": ">=0.19.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "license": "BSD-3-Clause", @@ -28491,12 +29581,18 @@ } }, "node_modules/scroll-into-view-if-needed": { - "version": "2.2.31", - "license": "MIT", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", "dependencies": { - "compute-scroll-into-view": "^1.0.20" + "compute-scroll-into-view": "^3.0.2" } }, + "node_modules/scroll-into-view-if-needed/node_modules/compute-scroll-into-view": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz", + "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==" + }, "node_modules/section-matter": { "version": "1.0.0", "license": "MIT", @@ -29039,10 +30135,11 @@ } }, "node_modules/slate": { - "version": "0.91.4", - "license": "MIT", + "version": "0.102.0", + "resolved": "https://registry.npmjs.org/slate/-/slate-0.102.0.tgz", + "integrity": "sha512-RT+tHgqOyZVB1oFV9Pv99ajwh4OUCN9p28QWdnDTIzaN/kZxMsHeQN39UNAgtkZTVVVygFqeg7/R2jiptCvfyA==", "dependencies": { - "immer": "^9.0.6", + "immer": "^10.0.3", "is-plain-object": "^5.0.0", "tiny-warning": "^1.0.3" } @@ -29058,8 +30155,9 @@ } }, "node_modules/slate-history": { - "version": "0.93.0", - "license": "MIT", + "version": "0.100.0", + "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.100.0.tgz", + "integrity": "sha512-x5rUuWLNtH97hs9PrFovGgt3Qc5zkTm/5mcUB+0NR/TK923eLax4HsL6xACLHMs245nI6aJElyM1y6hN0y5W/Q==", "dependencies": { "is-plain-object": "^5.0.0" }, @@ -29087,33 +30185,26 @@ } }, "node_modules/slate-react": { - "version": "0.91.11", - "license": "MIT", + "version": "0.102.0", + "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.102.0.tgz", + "integrity": "sha512-SAcFsK5qaOxXjm0hr/t2pvIxfRv6HJGzmWkG58TdH4LdJCsgKS1n6hQOakHPlRVCwPgwvngB6R+t3pPjv8MqwA==", "dependencies": { "@juggle/resize-observer": "^3.4.0", - "@types/is-hotkey": "^0.1.1", - "@types/lodash": "^4.14.149", - "direction": "^1.0.3", - "is-hotkey": "^0.1.6", + "@types/is-hotkey": "^0.1.8", + "@types/lodash": "^4.14.200", + "direction": "^1.0.4", + "is-hotkey": "^0.2.0", "is-plain-object": "^5.0.0", - "lodash": "^4.17.4", - "scroll-into-view-if-needed": "^2.2.20", - "tiny-invariant": "1.0.6" + "lodash": "^4.17.21", + "scroll-into-view-if-needed": "^3.1.0", + "tiny-invariant": "1.3.1" }, "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.65.3" + "react": ">=18.2.0", + "react-dom": ">=18.2.0", + "slate": ">=0.99.0" } }, - "node_modules/slate-react/node_modules/is-hotkey": { - "version": "0.1.8", - "license": "MIT" - }, - "node_modules/slate-react/node_modules/tiny-invariant": { - "version": "1.0.6", - "license": "MIT" - }, "node_modules/slate-soft-break": { "version": "0.9.0", "license": "MIT", @@ -29122,6 +30213,15 @@ "slate-react": ">=0.19.3" } }, + "node_modules/slate/node_modules/immer": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.4.tgz", + "integrity": "sha512-cuBuGK40P/sk5IzWa9QPUaAdvPHjkk1c+xYsd9oZw+YQQEV+10G0P5uMpGctZZKnyQ+ibRO08bD25nWLmYi2pw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/slice-ansi": { "version": "4.0.0", "dev": true, @@ -30346,6 +31446,11 @@ "node": ">=4" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "node_modules/table": { "version": "6.8.1", "dev": true, @@ -31856,6 +32961,36 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/use-context-selector": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/use-context-selector/-/use-context-selector-1.4.4.tgz", + "integrity": "sha512-pS790zwGxxe59GoBha3QYOwk8AFGp4DN6DOtH+eoqVmgBBRXVx4IlPDhJmmMiNQAgUaLlP+58aqRC3A4rdaSjg==", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": "*", + "react-native": "*", + "scheduler": ">=0.19.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/use-deep-compare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-deep-compare/-/use-deep-compare-1.2.1.tgz", + "integrity": "sha512-JTnOZAr0fq1ix6CQ4XANoWIh03xAiMFlP/lVAYDdAOZwur6nqBSdATn1/Q9PLIGIW+C7xmFZBCcaA4KLDcQJtg==", + "dependencies": { + "dequal": "2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/use-isomorphic-layout-effect": { "version": "1.1.2", "license": "MIT", @@ -31916,6 +33051,14 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/utf-8-validate": { "version": "5.0.10", "dev": true, @@ -32011,6 +33154,14 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/value-equal": { "version": "1.0.1", "license": "MIT" @@ -33197,6 +34348,55 @@ "version": "1.14.1", "license": "0BSD" }, + "node_modules/zustand": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", + "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/zustand-x": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/zustand-x/-/zustand-x-3.0.2.tgz", + "integrity": "sha512-tb4qMWbmgrWEdemb+LlrJiHI1ZMxwlQNz7jDHN5iA/vmU8xlpAX80MQZ2FNLP2KejBFEnsA1RWRAO/0D5O0rPw==", + "dependencies": { + "immer": "^10.0.3", + "lodash.mapvalues": "^4.6.0", + "react-tracked": "^1.7.11" + }, + "peerDependencies": { + "zustand": ">=4.3.9" + } + }, + "node_modules/zustand-x/node_modules/immer": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.4.tgz", + "integrity": "sha512-cuBuGK40P/sk5IzWa9QPUaAdvPHjkk1c+xYsd9oZw+YQQEV+10G0P5uMpGctZZKnyQ+ibRO08bD25nWLmYi2pw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/zwitch": { "version": "1.0.5", "license": "MIT", @@ -33807,11 +35007,12 @@ "remark-slate": "^1.8.6", "remark-slate-transformer": "^0.7.4", "remark-stringify": "^6.0.4", - "slate": "^0.91.1", + "slate": "^0.102.0", "slate-base64-serializer": "^0.2.107", - "slate-history": "^0.93.0", + "slate-history": "^0.100.0", + "slate-hyperscript": "^0.100.0", "slate-plain-serializer": "^0.7.1", - "slate-react": "^0.91.2", + "slate-react": "^0.102.0", "slate-soft-break": "^0.9.0", "unified": "^9.2.0", "unist-builder": "^1.0.3", @@ -33971,6 +35172,42 @@ "uuid": "^8.3.2" } }, + "packages/decap-cms-widget-richtext": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "@udecode/cn": "^29.0.1", + "@udecode/plate": "^30.5.0", + "class-variance-authority": "^0.7.0", + "lucide-react": "^0.331.0", + "slate": "^0.102.0", + "slate-history": "^0.100.0", + "slate-hyperscript": "^0.100.0", + "slate-react": "^0.102.0" + }, + "peerDependencies": { + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "decap-cms-ui-default": "^3.0.0", + "immutable": "^3.7.6", + "lodash": "^4.17.11", + "prop-types": "^15.7.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-immutable-proptypes": "^2.1.0" + } + }, + "packages/decap-cms-widget-richtext/node_modules/slate-hyperscript": { + "version": "0.100.0", + "resolved": "https://registry.npmjs.org/slate-hyperscript/-/slate-hyperscript-0.100.0.tgz", + "integrity": "sha512-fb2KdAYg6RkrQGlqaIi4wdqz3oa0S4zKNBJlbnJbNOwa23+9FLD6oPVx9zUGqCSIpy+HIpOeqXrg0Kzwh/Ii4A==", + "dependencies": { + "is-plain-object": "^5.0.0" + }, + "peerDependencies": { + "slate": ">=0.65.3" + } + }, "packages/decap-cms-widget-select": { "version": "3.1.1", "license": "MIT", diff --git a/packages/decap-cms-app/src/extensions.js b/packages/decap-cms-app/src/extensions.js index eef9ebd77063..988bb226b1a2 100644 --- a/packages/decap-cms-app/src/extensions.js +++ b/packages/decap-cms-app/src/extensions.js @@ -18,6 +18,7 @@ import DecapCmsWidgetImage from 'decap-cms-widget-image'; import DecapCmsWidgetFile from 'decap-cms-widget-file'; import DecapCmsWidgetSelect from 'decap-cms-widget-select'; import DecapCmsWidgetMarkdown from 'decap-cms-widget-markdown'; +import DecapCmsWidgetRichtext from 'decap-cms-widget-richtext'; import DecapCmsWidgetList from 'decap-cms-widget-list'; import DecapCmsWidgetObject from 'decap-cms-widget-object'; import DecapCmsWidgetRelation from 'decap-cms-widget-relation'; @@ -49,6 +50,7 @@ CMS.registerWidget([ DecapCmsWidgetFile.Widget(), DecapCmsWidgetSelect.Widget(), DecapCmsWidgetMarkdown.Widget(), + DecapCmsWidgetRichtext.Widget(), DecapCmsWidgetList.Widget(), DecapCmsWidgetObject.Widget(), DecapCmsWidgetRelation.Widget(), diff --git a/packages/decap-cms-widget-markdown/package.json b/packages/decap-cms-widget-markdown/package.json index 9ee577ac96fd..6685cd84bc80 100644 --- a/packages/decap-cms-widget-markdown/package.json +++ b/packages/decap-cms-widget-markdown/package.json @@ -34,11 +34,12 @@ "remark-slate": "^1.8.6", "remark-slate-transformer": "^0.7.4", "remark-stringify": "^6.0.4", - "slate": "^0.91.1", + "slate": "^0.102.0", + "slate-hyperscript": "^0.100.0", "slate-base64-serializer": "^0.2.107", - "slate-history": "^0.93.0", + "slate-history": "^0.100.0", "slate-plain-serializer": "^0.7.1", - "slate-react": "^0.91.2", + "slate-react": "^0.102.0", "slate-soft-break": "^0.9.0", "unified": "^9.2.0", "unist-builder": "^1.0.3", diff --git a/packages/decap-cms-widget-richtext/README.md b/packages/decap-cms-widget-richtext/README.md new file mode 100644 index 000000000000..0ef8211e104f --- /dev/null +++ b/packages/decap-cms-widget-richtext/README.md @@ -0,0 +1,9 @@ +# Docs coming soon! + +Decap CMS was converted from a single npm package to a "monorepo" of over 20 packages. +We haven't created a README for this package yet, but you can: + +1. Check out the [main readme](https://github.com/decaporg/decap-cms/#readme) or the [documentation + site](https://www.decapcms.org) for more info. +2. Reach out to the [community chat](https://decapcms.org/chat/) if you need help. +3. Help out and [write the readme yourself](https://github.com/decaporg/decap-cms/edit/master/packages/decap-cms-widget-markdown/README.md)! diff --git a/packages/decap-cms-widget-richtext/package.json b/packages/decap-cms-widget-richtext/package.json new file mode 100644 index 000000000000..d70fe69792ff --- /dev/null +++ b/packages/decap-cms-widget-richtext/package.json @@ -0,0 +1,44 @@ +{ + "name": "decap-cms-widget-richtext", + "description": "Widget for editing richtext in Decap CMS.", + "version": "3.1.0", + "homepage": "https://www.decapcms.org/docs/widgets/#richtext", + "repository": "https://github.com/decaporg/decap-cms/tree/master/packages/decap-cms-widget-richtext", + "bugs": "https://github.com/decaporg/decap-cms/issues", + "module": "dist/esm/index.js", + "main": "dist/decap-cms-widget-richtext.js", + "license": "MIT", + "keywords": [ + "decap-cms", + "widget", + "richtext", + "editor" + ], + "sideEffects": false, + "scripts": { + "develop": "npm run build:esm -- --watch", + "build": "cross-env NODE_ENV=production webpack", + "build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward --copy-files --extensions \".js,.jsx,.ts,.tsx\"" + }, + "dependencies": { + "@udecode/cn": "^29.0.1", + "@udecode/plate": "^30.5.0", + "class-variance-authority": "^0.7.0", + "lucide-react": "^0.331.0", + "slate": "^0.102.0", + "slate-history": "^0.100.0", + "slate-hyperscript": "^0.100.0", + "slate-react": "^0.102.0" + }, + "peerDependencies": { + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "decap-cms-ui-default": "^3.0.0", + "immutable": "^3.7.6", + "lodash": "^4.17.11", + "prop-types": "^15.7.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-immutable-proptypes": "^2.1.0" + } +} diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl.js b/packages/decap-cms-widget-richtext/src/RichtextControl.js new file mode 100644 index 000000000000..43bac1e063ac --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl.js @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; + +import VisualEditor from './RichtextControl/VisualEditor'; + +export default class MarkdownControl extends React.Component { + static propTypes = { + onChange: PropTypes.func.isRequired, + onAddAsset: PropTypes.func.isRequired, + getAsset: PropTypes.func.isRequired, + classNameWrapper: PropTypes.string.isRequired, + editorControl: PropTypes.elementType.isRequired, + value: PropTypes.string, + field: ImmutablePropTypes.map.isRequired, + getEditorComponents: PropTypes.func, + t: PropTypes.func.isRequired, + isDisabled: PropTypes.bool, + }; + + render() { + const { classNameWrapper, field, t, isDisabled } = this.props; + const visualEditor = ( +
+ +
+ ); + return visualEditor; + } +} diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js new file mode 100644 index 000000000000..93619538a493 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -0,0 +1,108 @@ +import React from 'react'; +import { createPlugins, Plate } from '@udecode/plate-common'; +import { createParagraphPlugin, ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph'; +import { createBoldPlugin, MARK_BOLD, createItalicPlugin, MARK_ITALIC, createCodePlugin, MARK_CODE } from '@udecode/plate-basic-marks'; +import { ClassNames } from '@emotion/react'; +import { fonts, lengths, zIndex } from 'decap-cms-ui-default'; + +import Editor from './components/Editor'; +import Toolbar from './components/Toolbar'; +import { editorStyleVars } from '../styles'; +import CodeLeaf from './components/CodeLeaf'; +import ParagraphElement from './components/ParagraphElement'; +import BoldLeaf from './components/BoldLeaf'; +import ItalicLeaf from './components/ItalicLeaf'; + +function visualEditorStyles({ minimal }) { + return ` + position: relative; + overflow: auto; + font-family: ${fonts.primary}; + min-height: ${minimal ? 'auto' : lengths.richTextEditorMinHeight}; + margin-top: -${editorStyleVars.stickyDistanceBottom}; + padding: 0; + display: flex; + flex-direction: column; + z-index: ${zIndex.zIndex100}; +`; +} + +const initialValue = [ + { + id: '1', + type: 'p', + children: [{ text: 'Hello, World!' }], + }, +]; + +export default function VisualEditor(props) { + const { t, field, className, isDisabled } = props; + + const plugins = createPlugins([ + createParagraphPlugin(), + createBoldPlugin(), + createItalicPlugin(), + createCodePlugin(), + ], + { + components: { + [MARK_BOLD]: BoldLeaf, + [MARK_CODE]: CodeLeaf, + [MARK_ITALIC]: ItalicLeaf, + [ELEMENT_PARAGRAPH]: ParagraphElement, + }, + }); + + function handleBlockClick() { + console.log('handleBlockClick'); + } + + function handleLinkClick() { + console.log('handleLinkClick'); + } + + function handleToggleMode() { + console.log('handleToggleMode'); + } + + function handleChange(data) { + console.log('handleChange', data); + } + + return ( + + {({ css, cx }) => ( +
+ + false} + getAsset={() => false} + hasInline={() => false} + hasBlock={() => false} + hasQuote={() => false} + hasListItems={() => false} + isShowModeToggle={() => false} + onChange={() => false} + t={t} + disabled={isDisabled} + /> + + +
+ )} +
+ ); +} diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/BoldLeaf.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/BoldLeaf.js new file mode 100644 index 000000000000..4884ca6b20f5 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/BoldLeaf.js @@ -0,0 +1,12 @@ +import React from 'react'; +import { PlateLeaf } from '@udecode/plate-common'; + +function BoldLeaf({ children, ...props }) { + return ( + + {children} + + ); +} + +export default BoldLeaf; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/CodeLeaf.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/CodeLeaf.js new file mode 100644 index 000000000000..6a4e01a0b5ff --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/CodeLeaf.js @@ -0,0 +1,21 @@ +import React from 'react'; +import { PlateLeaf } from '@udecode/plate-common'; +import styled from '@emotion/styled'; +import { colors, lengths } from 'decap-cms-ui-default'; + +const StyledCode = styled.code` + background-color: ${colors.background}; + border-radius: ${lengths.borderRadius}; + padding: 0 2px; + font-size: 85%; +`; + +function CodeLeaf({ children, ...props }) { + return ( + + {children} + + ); +} + +export default CodeLeaf; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Editor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Editor.js new file mode 100644 index 000000000000..8e2ee07eec11 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Editor.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { PlateContent } from '@udecode/plate-common'; +import { ClassNames } from '@emotion/react'; + +function Editor(props) { + const { isDisabled } = props; + + return ( + + {({ css }) => ( + + )} + + ); +} + +export default Editor; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/ItalicLeaf.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/ItalicLeaf.js new file mode 100644 index 000000000000..62944c6eb374 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/ItalicLeaf.js @@ -0,0 +1,12 @@ +import React from 'react'; +import { PlateLeaf } from '@udecode/plate-common'; + +function ItalicLeaf({ children, ...props }) { + return ( + + {children} + + ); +} + +export default ItalicLeaf; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/MarkToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/MarkToolbarButton.js new file mode 100644 index 000000000000..af08b7949d92 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/MarkToolbarButton.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { useMarkToolbarButton, useMarkToolbarButtonState } from '@udecode/plate-common'; + +import ToolbarButton from './ToolbarButton'; + +function MarkToolbarButton({ clear, nodeType, ...rest }) { + const state = useMarkToolbarButtonState({ clear, nodeType }); + const { + props: { pressed, onClick }, + } = useMarkToolbarButton(state); + return ; +} + +export default MarkToolbarButton; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/ParagraphElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/ParagraphElement.js new file mode 100644 index 000000000000..ee863d4261ee --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/ParagraphElement.js @@ -0,0 +1,10 @@ +import styled from '@emotion/styled'; +import { PlateElement } from '@udecode/plate-common'; + +const bottomMargin = '12px'; + +const ParagraphElement = styled(PlateElement)` + margin-bottom: ${bottomMargin}; +`; + +export default ParagraphElement diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar.js new file mode 100644 index 000000000000..69995cf444cf --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar.js @@ -0,0 +1,120 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from '@emotion/styled'; +// import { css } from '@emotion/react'; +import { List } from 'immutable'; +import { + // Toggle, + // Dropdown, + // DropdownItem, + // DropdownButton, + colors, + transitions, +} from 'decap-cms-ui-default'; +import { MARK_BOLD, MARK_CODE, MARK_ITALIC } from '@udecode/plate-basic-marks'; + +// import ToolbarButton from './ToolbarButton'; +import MarkToolbarButton from './MarkToolbarButton'; + +// import ToolbarButton from './ToolbarButton'; + +const ToolbarContainer = styled.div` + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + padding: 11px 14px; + min-height: 58px; + transition: background-color ${transitions.main}, color ${transitions.main}; + color: ${colors.text}; +`; + +// const ToolbarDropdownWrapper = styled.div` +// display: inline-block; +// position: relative; +// `; + +// const ToolbarToggle = styled.div` +// flex-shrink: 0; +// display: flex; +// align-items: center; +// font-size: 14px; +// margin: 0 10px; +// `; + +// const StyledToggle = ToolbarToggle.withComponent(Toggle); + +// const ToolbarToggleLabel = styled.span` +// display: inline-block; +// text-align: center; +// white-space: nowrap; +// line-height: 20px; +// min-width: ${props => (props.offPosition ? '62px' : '70px')}; + +// ${props => +// props.isActive && +// css` +// font-weight: 600; +// color: ${colors.active}; +// `}; +// `; + +function Toolbar(props) { + const { disabled, t } = props; + + function isVisible(button) { + const { buttons } = props; + return !List.isList(buttons) || buttons.includes(button); + } + + return ( + +
+ {isVisible('bold') && ( + + )} + {isVisible('italic') && ( + + )} + {isVisible('code') && ( + + )} +
+
+ ); +} + +Toolbar.propTypes = { + // onAddAsset: PropTypes.func.isRequired, + // getAsset: PropTypes.func.isRequired, + // onChange: PropTypes.func.isRequired, + // onMode: PropTypes.func.isRequired, + // className: PropTypes.string.isRequired, + // value: PropTypes.string, + // field: ImmutablePropTypes.map.isRequired, + // getEditorComponents: PropTypes.func.isRequired, + // getRemarkPlugins: PropTypes.func.isRequired, + // isShowModeToggle: PropTypes.bool.isRequired, + disabled: PropTypes.bool, + t: PropTypes.func.isRequired, +}; + +export default Toolbar; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/ToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/ToolbarButton.js new file mode 100644 index 000000000000..50d4501889b7 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/ToolbarButton.js @@ -0,0 +1,49 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from '@emotion/styled'; +import { Icon, buttons } from 'decap-cms-ui-default'; + +const StyledToolbarButton = styled.button` + ${buttons.button}; + display: inline-block; + padding: 6px; + border: none; + background-color: transparent; + font-size: 16px; + color: ${props => (props.isActive ? '#1e2532' : 'inherit')}; + cursor: pointer; + + &:disabled { + cursor: auto; + opacity: 0.5; + } + + ${Icon} { + display: block; + } +`; + +function ToolbarButton({ type, label, icon, onClick, isActive, disabled }) { + return ( + onClick && onClick(e, type)} + onMouseDown={e => e.preventDefault()} + title={label} + disabled={disabled} + > + {icon ? : label} + + ); +} + +ToolbarButton.propTypes = { + type: PropTypes.string, + label: PropTypes.string.isRequired, + icon: PropTypes.string, + onClick: PropTypes.func, + isActive: PropTypes.bool, + disabled: PropTypes.bool, +}; + +export default ToolbarButton; diff --git a/packages/decap-cms-widget-richtext/src/RichtextPreview.js b/packages/decap-cms-widget-richtext/src/RichtextPreview.js new file mode 100644 index 000000000000..0c5deb3c6e29 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextPreview.js @@ -0,0 +1,5 @@ +import React from "react"; + +export default function RichtextPreview() { + return <>Richtext preview; +} diff --git a/packages/decap-cms-widget-richtext/src/index.js b/packages/decap-cms-widget-richtext/src/index.js new file mode 100644 index 000000000000..1d06f360be96 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/index.js @@ -0,0 +1,16 @@ +import controlComponent from './RichtextControl'; +import previewComponent from './RichtextPreview'; +import schema from './schema'; + +function Widget(opts = {}) { + return { + name: 'richtext', + controlComponent, + previewComponent, + schema, + ...opts, + }; +} + +export const DecapCmsWidgetMarkdown = { Widget, controlComponent, previewComponent }; +export default DecapCmsWidgetMarkdown; diff --git a/packages/decap-cms-widget-richtext/src/schema.js b/packages/decap-cms-widget-richtext/src/schema.js new file mode 100644 index 000000000000..19fda27d797f --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/schema.js @@ -0,0 +1,35 @@ +export default { + properties: { + minimal: { type: 'boolean' }, + buttons: { + type: 'array', + items: { + type: 'string', + enum: [ + 'bold', + 'italic', + 'code', + 'link', + 'heading-one', + 'heading-two', + 'heading-three', + 'heading-four', + 'heading-five', + 'heading-six', + 'quote', + 'bulleted-list', + 'numbered-list', + ], + }, + }, + editor_components: { type: 'array', items: { type: 'string' } }, + modes: { + type: 'array', + items: { + type: 'string', + enum: ['raw', 'rich_text'], + }, + minItems: 1, + }, + }, +}; diff --git a/packages/decap-cms-widget-richtext/src/styles.js b/packages/decap-cms-widget-richtext/src/styles.js new file mode 100644 index 000000000000..79698f537ecf --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/styles.js @@ -0,0 +1,13 @@ +import styled from '@emotion/styled'; +import { zIndex } from 'decap-cms-ui-default'; + +export const editorStyleVars = { + stickyDistanceBottom: '0', +}; + +export const EditorControlBar = styled.div` + z-index: ${zIndex.zIndex200}; + position: sticky; + top: 0; + margin-bottom: ${editorStyleVars.stickyDistanceBottom}; +`; From 4ec49836a377be91a2d9cdb2d7df8bbce46fdd5f Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Wed, 27 Mar 2024 08:17:15 +0100 Subject: [PATCH 02/43] feat(richtext): add paragraphs and headings, refactor some --- .../src/RichtextControl/VisualEditor.js | 84 +++++++++++++--- .../RichtextControl/components/BoldLeaf.js | 12 --- .../components/HeadingElement.js | 51 ++++++++++ .../RichtextControl/components/ItalicLeaf.js | 12 --- .../components/ParagraphElement.js | 2 +- .../Toolbar/HeadingToolbarButton.js | 99 +++++++++++++++++++ .../{ => Toolbar}/MarkToolbarButton.js | 0 .../components/{ => Toolbar}/Toolbar.js | 56 +---------- .../components/{ => Toolbar}/ToolbarButton.js | 4 +- .../components/Toolbar/index.js | 3 + .../src/RichtextControl/withProps.js | 11 +++ tsconfig.json | 3 +- 12 files changed, 243 insertions(+), 94 deletions(-) delete mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/BoldLeaf.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/HeadingElement.js delete mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/ItalicLeaf.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js rename packages/decap-cms-widget-richtext/src/RichtextControl/components/{ => Toolbar}/MarkToolbarButton.js (100%) rename packages/decap-cms-widget-richtext/src/RichtextControl/components/{ => Toolbar}/Toolbar.js (55%) rename packages/decap-cms-widget-richtext/src/RichtextControl/components/{ => Toolbar}/ToolbarButton.js (87%) create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/index.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/withProps.js diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index 93619538a493..ea9ba09416dc 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -1,7 +1,25 @@ import React from 'react'; -import { createPlugins, Plate } from '@udecode/plate-common'; +import { createPlugins, Plate, PlateLeaf } from '@udecode/plate-common'; import { createParagraphPlugin, ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph'; -import { createBoldPlugin, MARK_BOLD, createItalicPlugin, MARK_ITALIC, createCodePlugin, MARK_CODE } from '@udecode/plate-basic-marks'; +import { + createBoldPlugin, + MARK_BOLD, + createItalicPlugin, + MARK_ITALIC, + createCodePlugin, + MARK_CODE, +} from '@udecode/plate-basic-marks'; +import { + createHeadingPlugin, + ELEMENT_H1, + ELEMENT_H2, + ELEMENT_H3, + ELEMENT_H4, + ELEMENT_H5, + ELEMENT_H6, + KEYS_HEADING, +} from '@udecode/plate-heading'; +import { createSoftBreakPlugin, createExitBreakPlugin } from '@udecode/plate-break'; import { ClassNames } from '@emotion/react'; import { fonts, lengths, zIndex } from 'decap-cms-ui-default'; @@ -10,8 +28,8 @@ import Toolbar from './components/Toolbar'; import { editorStyleVars } from '../styles'; import CodeLeaf from './components/CodeLeaf'; import ParagraphElement from './components/ParagraphElement'; -import BoldLeaf from './components/BoldLeaf'; -import ItalicLeaf from './components/ItalicLeaf'; +import HeadingElement from './components/HeadingElement'; +import withProps from './withProps'; function visualEditorStyles({ minimal }) { return ` @@ -24,6 +42,7 @@ function visualEditorStyles({ minimal }) { display: flex; flex-direction: column; z-index: ${zIndex.zIndex100}; + white-space: pre-wrap; `; } @@ -38,20 +57,57 @@ const initialValue = [ export default function VisualEditor(props) { const { t, field, className, isDisabled } = props; - const plugins = createPlugins([ - createParagraphPlugin(), - createBoldPlugin(), - createItalicPlugin(), - createCodePlugin(), - ], - { + const plugins = createPlugins( + [ + createParagraphPlugin(), + createHeadingPlugin(), + createBoldPlugin(), + createItalicPlugin(), + createCodePlugin(), + createSoftBreakPlugin({ + options: { + rules: [{ hotkey: 'shift+enter' }], + }, + }), + createExitBreakPlugin({ + options: { + rules: [ + { + hotkey: 'mod+enter', + }, + { + hotkey: 'mod+shift+enter', + before: true, + }, + { + hotkey: 'enter', + query: { + start: true, + end: true, + allow: KEYS_HEADING, + }, + relative: true, + level: 1, + }, + ], + }, + }), + ], + { components: { - [MARK_BOLD]: BoldLeaf, + [MARK_BOLD]: withProps(PlateLeaf, { as: 'b' }), [MARK_CODE]: CodeLeaf, - [MARK_ITALIC]: ItalicLeaf, + [MARK_ITALIC]: withProps(PlateLeaf, { as: 'em'}), [ELEMENT_PARAGRAPH]: ParagraphElement, + [ELEMENT_H1]: withProps(HeadingElement, { variant: 'h1' }), + [ELEMENT_H2]: withProps(HeadingElement, { variant: 'h2' }), + [ELEMENT_H3]: withProps(HeadingElement, { variant: 'h3' }), + [ELEMENT_H4]: withProps(HeadingElement, { variant: 'h4' }), + [ELEMENT_H5]: withProps(HeadingElement, { variant: 'h5' }), + [ELEMENT_H6]: withProps(HeadingElement, { variant: 'h6' }), }, - }); + }, + ); function handleBlockClick() { console.log('handleBlockClick'); diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/BoldLeaf.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/BoldLeaf.js deleted file mode 100644 index 4884ca6b20f5..000000000000 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/BoldLeaf.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { PlateLeaf } from '@udecode/plate-common'; - -function BoldLeaf({ children, ...props }) { - return ( - - {children} - - ); -} - -export default BoldLeaf; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/HeadingElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/HeadingElement.js new file mode 100644 index 000000000000..bdeafeb6e73f --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/HeadingElement.js @@ -0,0 +1,51 @@ +import React from 'react'; +import { PlateElement } from '@udecode/plate-common'; +import styled from '@emotion/styled'; + +const headingVariants = { + h1: { + fontSize: '32px', + marginTop: '16px', + }, + h2: { + fontSize: '24px', + marginTop: '12px', + }, + h3: { + fontSize: '20px', + }, + h4: { + fontSize: '18px', + marginTop: '8px', + }, + h5: { + fontSize: '16px', + marginTop: '8px', + }, + h6: { + fontSize: '16px', + marginTop: '8px', + }, +}; + +const StyledHeading = styled(PlateElement)` + font-weight: 700; + line-height: 1; + margin-top: ${props => (props.isFirstBlock ? '0' : headingVariants[props.variant].marginTop)}; + font-size: ${props => headingVariants[props.variant].fontSize}; +`; + +function HeadingElement({ variant = 'h1', children, ...props }) { + const { element, editor } = props; + const isFirstBlock = element === editor.children[0]; + + const Element = StyledHeading.withComponent(variant); + + return ( + + {children} + + ); +} + +export default HeadingElement; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/ItalicLeaf.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/ItalicLeaf.js deleted file mode 100644 index 62944c6eb374..000000000000 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/ItalicLeaf.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { PlateLeaf } from '@udecode/plate-common'; - -function ItalicLeaf({ children, ...props }) { - return ( - - {children} - - ); -} - -export default ItalicLeaf; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/ParagraphElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/ParagraphElement.js index ee863d4261ee..c9260dba5f8d 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/ParagraphElement.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/ParagraphElement.js @@ -7,4 +7,4 @@ const ParagraphElement = styled(PlateElement)` margin-bottom: ${bottomMargin}; `; -export default ParagraphElement +export default ParagraphElement; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js new file mode 100644 index 000000000000..c9ae59b801d1 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js @@ -0,0 +1,99 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + findNode, + focusEditor, + isBlock, + isSelectionExpanded, + toggleNodeType, + useEditorRef, + useEditorSelector, +} from '@udecode/plate-common'; +import styled from '@emotion/styled'; +import { ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph'; +import { Dropdown, DropdownButton, DropdownItem } from 'decap-cms-ui-default'; + +import ToolbarButton from './ToolbarButton'; + +const ToolbarDropdownWrapper = styled.div` + display: inline-block; + position: relative; +`; + +function HeadingToolbarButton({ disabled, isVisible, t }) { + const headingOptions = { + h1: t('editor.editorWidgets.headingOptions.headingOne'), + h2: t('editor.editorWidgets.headingOptions.headingTwo'), + h3: t('editor.editorWidgets.headingOptions.headingThree'), + h4: t('editor.editorWidgets.headingOptions.headingFour'), + h5: t('editor.editorWidgets.headingOptions.headingFive'), + h6: t('editor.editorWidgets.headingOptions.headingSix'), + }; + + const editor = useEditorRef(); + + const value = useEditorSelector(editor => { + if (!isSelectionExpanded(editor)) { + const entry = findNode(editor, { + match: n => isBlock(editor, n), + }); + + if (entry) { + return entry[0].type; + } + } + + return ELEMENT_PARAGRAPH; + }, []); + + function handleChange(optionKey) { + toggleNodeType(editor, { activeType: optionKey }); + focusEditor(editor); + } + + return ( + <> + {Object.keys(headingOptions).some(isVisible) && ( + + ( + + key == value)} + /> + + )} + > + {!disabled && + Object.keys(headingOptions).map( + (optionKey, idx) => + isVisible(optionKey) && ( + e.preventDefault()} + onClick={() => handleChange(optionKey)} + /> + ), + )} + + + )} + + ); +} + +HeadingToolbarButton.propTypes = { + isVisible: PropTypes.func.isRequired, + disabled: PropTypes.bool, + t: PropTypes.func.isRequired, +}; + +export default HeadingToolbarButton; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/MarkToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/MarkToolbarButton.js similarity index 100% rename from packages/decap-cms-widget-richtext/src/RichtextControl/components/MarkToolbarButton.js rename to packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/MarkToolbarButton.js diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js similarity index 55% rename from packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar.js rename to packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js index 69995cf444cf..2680b00997b0 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js @@ -1,22 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled from '@emotion/styled'; -// import { css } from '@emotion/react'; import { List } from 'immutable'; -import { - // Toggle, - // Dropdown, - // DropdownItem, - // DropdownButton, - colors, - transitions, -} from 'decap-cms-ui-default'; +import { colors, transitions } from 'decap-cms-ui-default'; import { MARK_BOLD, MARK_CODE, MARK_ITALIC } from '@udecode/plate-basic-marks'; -// import ToolbarButton from './ToolbarButton'; import MarkToolbarButton from './MarkToolbarButton'; - -// import ToolbarButton from './ToolbarButton'; +import HeadingToolbarButton from './HeadingToolbarButton'; const ToolbarContainer = styled.div` position: relative; @@ -29,36 +19,6 @@ const ToolbarContainer = styled.div` color: ${colors.text}; `; -// const ToolbarDropdownWrapper = styled.div` -// display: inline-block; -// position: relative; -// `; - -// const ToolbarToggle = styled.div` -// flex-shrink: 0; -// display: flex; -// align-items: center; -// font-size: 14px; -// margin: 0 10px; -// `; - -// const StyledToggle = ToolbarToggle.withComponent(Toggle); - -// const ToolbarToggleLabel = styled.span` -// display: inline-block; -// text-align: center; -// white-space: nowrap; -// line-height: 20px; -// min-width: ${props => (props.offPosition ? '62px' : '70px')}; - -// ${props => -// props.isActive && -// css` -// font-weight: 600; -// color: ${colors.active}; -// `}; -// `; - function Toolbar(props) { const { disabled, t } = props; @@ -97,22 +57,14 @@ function Toolbar(props) { disabled={disabled} /> )} + ); } Toolbar.propTypes = { - // onAddAsset: PropTypes.func.isRequired, - // getAsset: PropTypes.func.isRequired, - // onChange: PropTypes.func.isRequired, - // onMode: PropTypes.func.isRequired, - // className: PropTypes.string.isRequired, - // value: PropTypes.string, - // field: ImmutablePropTypes.map.isRequired, - // getEditorComponents: PropTypes.func.isRequired, - // getRemarkPlugins: PropTypes.func.isRequired, - // isShowModeToggle: PropTypes.bool.isRequired, + buttons: PropTypes.array, disabled: PropTypes.bool, t: PropTypes.func.isRequired, }; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/ToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/ToolbarButton.js similarity index 87% rename from packages/decap-cms-widget-richtext/src/RichtextControl/components/ToolbarButton.js rename to packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/ToolbarButton.js index 50d4501889b7..0fb6707ef0f7 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/ToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/ToolbarButton.js @@ -8,9 +8,9 @@ const StyledToolbarButton = styled.button` display: inline-block; padding: 6px; border: none; - background-color: transparent; + background-color: ${props => (props.isActive ? '#e8f5fe' : 'transparent')}; font-size: 16px; - color: ${props => (props.isActive ? '#1e2532' : 'inherit')}; + color: ${props => (props.isActive ? '#3a69c7' : 'inherit')}; cursor: pointer; &:disabled { diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/index.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/index.js new file mode 100644 index 000000000000..b801595640cb --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/index.js @@ -0,0 +1,3 @@ +import Toolbar from "./Toolbar"; + +export default Toolbar; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/withProps.js b/packages/decap-cms-widget-richtext/src/RichtextControl/withProps.js new file mode 100644 index 000000000000..f3bdc5a2f2f4 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/withProps.js @@ -0,0 +1,11 @@ +import React from 'react'; + +function withProps(Component, defaultProps) { + const ComponentWithClassName = Component; + + return React.forwardRef(function ExtendComponent(props, ref) { + return ; + }); +} + +export default withProps; diff --git a/tsconfig.json b/tsconfig.json index 732806af3e16..e2aa43627a14 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,8 @@ "decap-cms-backend-github": ["packages/decap-cms-backend-github/src"], "decap-cms-backend-gitlab": ["packages/decap-cms-backend-gitlab/src"], "decap-cms-lib-util": ["packages/decap-cms-lib-util/src"], - "decap-cms-lib-widgets": ["packages/decap-cms-lib-widgets/src"] + "decap-cms-lib-widgets": ["packages/decap-cms-lib-widgets/src"], + "decap-cms-ui-default": ["packages/decap-cms-ui-default/src"], } }, "include": ["**/src/**/*"], From 899990b4cbd832bb99b6d68d500832a369f562ab Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Wed, 27 Mar 2024 10:43:48 +0100 Subject: [PATCH 03/43] feat(richtext): add basic listsr --- .../src/RichtextControl/VisualEditor.js | 16 +++++++---- .../{ => Element}/HeadingElement.js | 0 .../components/Element/ListElement.js | 27 +++++++++++++++++++ .../{ => Element}/ParagraphElement.js | 2 +- .../components/{ => Leaf}/CodeLeaf.js | 0 .../Toolbar/HeadingToolbarButton.js | 2 ++ .../components/Toolbar/ListToolbarButton.js | 24 +++++++++++++++++ .../components/Toolbar/Toolbar.js | 13 +++++++++ 8 files changed, 78 insertions(+), 6 deletions(-) rename packages/decap-cms-widget-richtext/src/RichtextControl/components/{ => Element}/HeadingElement.js (100%) create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ListElement.js rename packages/decap-cms-widget-richtext/src/RichtextControl/components/{ => Element}/ParagraphElement.js (87%) rename packages/decap-cms-widget-richtext/src/RichtextControl/components/{ => Leaf}/CodeLeaf.js (100%) create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/ListToolbarButton.js diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index ea9ba09416dc..6f36044b692e 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -20,16 +20,18 @@ import { KEYS_HEADING, } from '@udecode/plate-heading'; import { createSoftBreakPlugin, createExitBreakPlugin } from '@udecode/plate-break'; +import { createListPlugin, ELEMENT_UL, ELEMENT_OL, ELEMENT_LI } from '@udecode/plate-list'; import { ClassNames } from '@emotion/react'; import { fonts, lengths, zIndex } from 'decap-cms-ui-default'; -import Editor from './components/Editor'; -import Toolbar from './components/Toolbar'; import { editorStyleVars } from '../styles'; -import CodeLeaf from './components/CodeLeaf'; -import ParagraphElement from './components/ParagraphElement'; -import HeadingElement from './components/HeadingElement'; import withProps from './withProps'; +import Editor from './components/Editor'; +import Toolbar from './components/Toolbar'; +import CodeLeaf from './components/Leaf/CodeLeaf'; +import ParagraphElement from './components/Element/ParagraphElement'; +import HeadingElement from './components/Element/HeadingElement'; +import ListElement from './components/Element/ListElement'; function visualEditorStyles({ minimal }) { return ` @@ -64,6 +66,7 @@ export default function VisualEditor(props) { createBoldPlugin(), createItalicPlugin(), createCodePlugin(), + createListPlugin(), createSoftBreakPlugin({ options: { rules: [{ hotkey: 'shift+enter' }], @@ -105,6 +108,9 @@ export default function VisualEditor(props) { [ELEMENT_H4]: withProps(HeadingElement, { variant: 'h4' }), [ELEMENT_H5]: withProps(HeadingElement, { variant: 'h5' }), [ELEMENT_H6]: withProps(HeadingElement, { variant: 'h6' }), + [ELEMENT_UL]: withProps(ListElement, { variant: 'ul' }), + [ELEMENT_OL]: withProps(ListElement, { variant: 'ol' }), + [ELEMENT_LI]: withProps(ListElement, { variant: 'li' }), }, }, ); diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/HeadingElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/HeadingElement.js similarity index 100% rename from packages/decap-cms-widget-richtext/src/RichtextControl/components/HeadingElement.js rename to packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/HeadingElement.js diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ListElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ListElement.js new file mode 100644 index 000000000000..9acfb0658a35 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ListElement.js @@ -0,0 +1,27 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { PlateElement } from '@udecode/plate-common'; + +const bottomMargin = '16px'; + +const StyledList = styled(PlateElement)` + margin-bottom: ${bottomMargin}; + padding-left: 30px; +`; + +const StyledListElement = styled(PlateElement)` + margin-top: 8px; + margin-bottom: 8px; +`; + +function ListElement({ children, variant, ...props }) { + const Element = (variant == 'li' ? StyledListElement : StyledList).withComponent(variant); + + return ( + + {children} + + ); +} + +export default ListElement; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/ParagraphElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ParagraphElement.js similarity index 87% rename from packages/decap-cms-widget-richtext/src/RichtextControl/components/ParagraphElement.js rename to packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ParagraphElement.js index c9260dba5f8d..35d545de20b2 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/ParagraphElement.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ParagraphElement.js @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import { PlateElement } from '@udecode/plate-common'; -const bottomMargin = '12px'; +const bottomMargin = '16px'; const ParagraphElement = styled(PlateElement)` margin-bottom: ${bottomMargin}; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/CodeLeaf.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Leaf/CodeLeaf.js similarity index 100% rename from packages/decap-cms-widget-richtext/src/RichtextControl/components/CodeLeaf.js rename to packages/decap-cms-widget-richtext/src/RichtextControl/components/Leaf/CodeLeaf.js diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js index c9ae59b801d1..d211871266f2 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js @@ -11,6 +11,7 @@ import { } from '@udecode/plate-common'; import styled from '@emotion/styled'; import { ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph'; +import { unwrapList } from '@udecode/plate-list'; import { Dropdown, DropdownButton, DropdownItem } from 'decap-cms-ui-default'; import ToolbarButton from './ToolbarButton'; @@ -47,6 +48,7 @@ function HeadingToolbarButton({ disabled, isVisible, t }) { }, []); function handleChange(optionKey) { + unwrapList(editor); toggleNodeType(editor, { activeType: optionKey }); focusEditor(editor); } diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/ListToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/ListToolbarButton.js new file mode 100644 index 000000000000..42901cf1c256 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/ListToolbarButton.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { useListToolbarButton, useListToolbarButtonState } from '@udecode/plate-list'; + +import ToolbarButton from './ToolbarButton'; + +function ListToolbarButton({ label, icon, type, disabled }) { + const state = useListToolbarButtonState({ nodeType: type }); + + const { + props: { pressed, onClick }, + } = useListToolbarButton(state); + + return ( + + ); +} + +export default ListToolbarButton; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js index 2680b00997b0..09983ea77b3e 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js @@ -7,6 +7,7 @@ import { MARK_BOLD, MARK_CODE, MARK_ITALIC } from '@udecode/plate-basic-marks'; import MarkToolbarButton from './MarkToolbarButton'; import HeadingToolbarButton from './HeadingToolbarButton'; +import ListToolbarButton from './ListToolbarButton'; const ToolbarContainer = styled.div` position: relative; @@ -58,6 +59,18 @@ function Toolbar(props) { /> )} + + ); From e7e110b3664c2a07a1c9e815aa21ad399f21ba0c Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Wed, 27 Mar 2024 13:16:07 +0100 Subject: [PATCH 04/43] feat(richtext): add serializers, update tests --- dev-test/config.yml | 1 + .../src/MarkdownControl/VisualEditor.js | 2 +- .../src/RichtextControl.js | 11 +- .../src/RichtextControl/VisualEditor.js | 20 +- .../components/Toolbar/ToolbarButton.js | 3 +- .../components/Toolbar/index.js | 2 +- .../src/RichtextPreview.js | 29 +- .../__fixtures__/commonmarkExpected.json | 625 ++++++++++++++++++ .../duplicate_marks_github_issue_3280.md | 1 + .../serializers/__tests__/commonmark.spec.js | 110 +++ .../src/serializers/__tests__/index.spec.js | 52 ++ .../__tests__/remarkAllowHtmlEntities.spec.js | 25 + .../__tests__/remarkAssertParents.spec.js | 171 +++++ .../remarkEscapeMarkdownEntities.spec.js | 84 +++ .../__tests__/remarkPaddedLinks.spec.js | 43 ++ .../__tests__/remarkPlugins.spec.js | 299 +++++++++ .../__tests__/remarkShortcodes.spec.js | 106 +++ .../serializers/__tests__/remarkSlate.spec.js | 67 ++ .../remarkStripTrailingBreaks.spec.js | 23 + .../src/serializers/__tests__/slate.spec.js | 300 +++++++++ .../src/serializers/index.js | 227 +++++++ .../src/serializers/regexHelper.js | 137 ++++ .../src/serializers/rehypePaperEmoji.js | 16 + .../serializers/remarkAllowHtmlEntities.js | 58 ++ .../src/serializers/remarkAssertParents.js | 83 +++ .../remarkEscapeMarkdownEntities.js | 269 ++++++++ .../src/serializers/remarkImagesToText.js | 26 + .../src/serializers/remarkPaddedLinks.js | 120 ++++ .../src/serializers/remarkRehypeShortcodes.js | 67 ++ .../src/serializers/remarkShortcodes.js | 106 +++ .../src/serializers/remarkSlate.js | 424 ++++++++++++ .../src/serializers/remarkSquashReferences.js | 73 ++ .../serializers/remarkStripTrailingBreaks.js | 56 ++ .../src/serializers/remarkWrapHtml.js | 20 + .../src/serializers/slateRemark.js | 467 +++++++++++++ .../test-helpers/h.js | 32 + 36 files changed, 4139 insertions(+), 16 deletions(-) create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/commonmarkExpected.json create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/duplicate_marks_github_issue_3280.md create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/commonmark.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/index.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAllowHtmlEntities.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAssertParents.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkEscapeMarkdownEntities.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPaddedLinks.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPlugins.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkShortcodes.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkSlate.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkStripTrailingBreaks.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/slate.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/index.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/regexHelper.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/rehypePaperEmoji.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/remarkAllowHtmlEntities.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/remarkAssertParents.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/remarkEscapeMarkdownEntities.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/remarkImagesToText.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/remarkPaddedLinks.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/remarkRehypeShortcodes.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/remarkShortcodes.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/remarkSlate.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/remarkSquashReferences.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/remarkStripTrailingBreaks.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/remarkWrapHtml.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/slateRemark.js create mode 100644 packages/decap-cms-widget-richtext/test-helpers/h.js diff --git a/dev-test/config.yml b/dev-test/config.yml index 059201b3fcad..c3a0fd5e70f4 100644 --- a/dev-test/config.yml +++ b/dev-test/config.yml @@ -51,6 +51,7 @@ collections: # A list of collections the CMS should be able to edit tagname: '' - { label: 'Body', name: 'body', widget: 'richtext', hint: 'Main content goes here.' } + - { label: 'Body', name: 'bodyold', widget: 'markdown', hint: 'Main content goes here.' } - name: 'restaurants' # Used in routes, ie.: /admin/collections/:slug/edit label: 'Restaurants' # Used in the UI diff --git a/packages/decap-cms-widget-markdown/src/MarkdownControl/VisualEditor.js b/packages/decap-cms-widget-markdown/src/MarkdownControl/VisualEditor.js index c8c2ba7d123e..4baf2b5a1ea7 100644 --- a/packages/decap-cms-widget-markdown/src/MarkdownControl/VisualEditor.js +++ b/packages/decap-cms-widget-markdown/src/MarkdownControl/VisualEditor.js @@ -218,7 +218,7 @@ function Editor(props) { position: relative; `} > - + { - + ); return visualEditor; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index 6f36044b692e..894ce52226ce 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -32,6 +32,7 @@ import CodeLeaf from './components/Leaf/CodeLeaf'; import ParagraphElement from './components/Element/ParagraphElement'; import HeadingElement from './components/Element/HeadingElement'; import ListElement from './components/Element/ListElement'; +import { markdownToSlate, slateToMarkdown } from '../serializers'; function visualEditorStyles({ minimal }) { return ` @@ -48,17 +49,15 @@ function visualEditorStyles({ minimal }) { `; } -const initialValue = [ +const emptyValue = [ { id: '1', type: 'p', - children: [{ text: 'Hello, World!' }], + children: [{ text: '' }], }, ]; -export default function VisualEditor(props) { - const { t, field, className, isDisabled } = props; - +export default function VisualEditor({ t, field, className, isDisabled, onChange, ...props }) { const plugins = createPlugins( [ createParagraphPlugin(), @@ -100,7 +99,7 @@ export default function VisualEditor(props) { components: { [MARK_BOLD]: withProps(PlateLeaf, { as: 'b' }), [MARK_CODE]: CodeLeaf, - [MARK_ITALIC]: withProps(PlateLeaf, { as: 'em'}), + [MARK_ITALIC]: withProps(PlateLeaf, { as: 'em' }), [ELEMENT_PARAGRAPH]: ParagraphElement, [ELEMENT_H1]: withProps(HeadingElement, { variant: 'h1' }), [ELEMENT_H2]: withProps(HeadingElement, { variant: 'h2' }), @@ -127,10 +126,15 @@ export default function VisualEditor(props) { console.log('handleToggleMode'); } - function handleChange(data) { - console.log('handleChange', data); + function handleChange(value) { + const mdValue = slateToMarkdown(value, {}); + onChange(mdValue); } + const initialValue = props.value + ? markdownToSlate(props.value, {}) + : emptyValue; + return ( {({ css, cx }) => ( diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/ToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/ToolbarButton.js index 0fb6707ef0f7..8db168b01ed2 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/ToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/ToolbarButton.js @@ -6,7 +6,8 @@ import { Icon, buttons } from 'decap-cms-ui-default'; const StyledToolbarButton = styled.button` ${buttons.button}; display: inline-block; - padding: 6px; + padding: 4px; + margin: 2px; border: none; background-color: ${props => (props.isActive ? '#e8f5fe' : 'transparent')}; font-size: 16px; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/index.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/index.js index b801595640cb..e0203f055c43 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/index.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/index.js @@ -1,3 +1,3 @@ -import Toolbar from "./Toolbar"; +import Toolbar from './Toolbar'; export default Toolbar; diff --git a/packages/decap-cms-widget-richtext/src/RichtextPreview.js b/packages/decap-cms-widget-richtext/src/RichtextPreview.js index 0c5deb3c6e29..6d0402fb57b2 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextPreview.js +++ b/packages/decap-cms-widget-richtext/src/RichtextPreview.js @@ -1,5 +1,28 @@ -import React from "react"; +import React from 'react'; +import PropTypes from 'prop-types'; +import { WidgetPreviewContainer } from 'decap-cms-ui-default'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import DOMPurify from 'dompurify'; -export default function RichtextPreview() { - return <>Richtext preview; +import { markdownToHtml } from './serializers'; + +function RichtextPreview({ value, getAsset, resolveWidget, field, getRemarkPlugins }) { + if (value === null) { + return null; + } + + const html = markdownToHtml(value, { getAsset, resolveWidget }, getRemarkPlugins?.()); + const toRender = field?.get('sanitize_preview', false) ? DOMPurify.sanitize(html) : html; + + return ; } + +RichtextPreview.propTypes = { + value: PropTypes.string, + getAsset: PropTypes.func.isRequired, + resolveWidget: PropTypes.func.isRequired, + field: ImmutablePropTypes.map.isRequired, + getRemarkPlugins: PropTypes.func, +}; + +export default RichtextPreview; diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/commonmarkExpected.json b/packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/commonmarkExpected.json new file mode 100644 index 000000000000..2e74df1471fe --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/commonmarkExpected.json @@ -0,0 +1,625 @@ +{ + "\tfoo\tbaz\t\tbim\n": "NOT_TO_EQUAL", + " \tfoo\tbaz\t\tbim\n": "NOT_TO_EQUAL", + " a\ta\n ὐ\ta\n": "NOT_TO_EQUAL", + " - foo\n\n\tbar\n": "NOT_TO_EQUAL", + "- foo\n\n\t\tbar\n": "NOT_TO_EQUAL", + ">\t\tfoo\n": "NOT_TO_EQUAL", + "-\t\tfoo\n": "NOT_TO_EQUAL", + " foo\n\tbar\n": "TO_EQUAL", + " - foo\n - bar\n\t - baz\n": "NOT_TO_EQUAL", + "#\tFoo\n": "TO_EQUAL", + "*\t*\t*\t\n": "TO_ERROR", + "- `one\n- two`\n": "TO_EQUAL", + "***\n---\n___\n": "NOT_TO_EQUAL", + "+++\n": "TO_EQUAL", + "===\n": "TO_EQUAL", + "--\n**\n__\n": "TO_EQUAL", + " ***\n ***\n ***\n": "NOT_TO_EQUAL", + " ***\n": "TO_EQUAL", + "Foo\n ***\n": "TO_EQUAL", + "_____________________________________\n": "NOT_TO_EQUAL", + " - - -\n": "NOT_TO_EQUAL", + " ** * ** * ** * **\n": "NOT_TO_EQUAL", + "- - - -\n": "NOT_TO_EQUAL", + "- - - - \n": "NOT_TO_EQUAL", + "_ _ _ _ a\n\na------\n\n---a---\n": "TO_EQUAL", + " *-*\n": "NOT_TO_EQUAL", + "- foo\n***\n- bar\n": "NOT_TO_EQUAL", + "Foo\n***\nbar\n": "NOT_TO_EQUAL", + "Foo\n---\nbar\n": "TO_EQUAL", + "* Foo\n* * *\n* Bar\n": "NOT_TO_EQUAL", + "- Foo\n- * * *\n": "NOT_TO_EQUAL", + "# foo\n## foo\n### foo\n#### foo\n##### foo\n###### foo\n": "TO_EQUAL", + "####### foo\n": "TO_EQUAL", + "#5 bolt\n\n#hashtag\n": "TO_EQUAL", + "\\## foo\n": "TO_EQUAL", + "# foo *bar* \\*baz\\*\n": "NOT_TO_EQUAL", + "# foo \n": "TO_EQUAL", + " ### foo\n ## foo\n # foo\n": "TO_EQUAL", + " # foo\n": "TO_EQUAL", + "foo\n # bar\n": "TO_EQUAL", + "## foo ##\n ### bar ###\n": "TO_EQUAL", + "# foo ##################################\n##### foo ##\n": "TO_EQUAL", + "### foo ### \n": "TO_EQUAL", + "### foo ### b\n": "TO_EQUAL", + "# foo#\n": "NOT_TO_EQUAL", + "### foo \\###\n## foo #\\##\n# foo \\#\n": "NOT_TO_EQUAL", + "****\n## foo\n****\n": "NOT_TO_EQUAL", + "Foo bar\n# baz\nBar foo\n": "TO_EQUAL", + "## \n#\n### ###\n": "TO_ERROR", + "Foo *bar*\n=========\n\nFoo *bar*\n---------\n": "TO_EQUAL", + "Foo *bar\nbaz*\n====\n": "NOT_TO_EQUAL", + "Foo\n-------------------------\n\nFoo\n=\n": "TO_EQUAL", + " Foo\n---\n\n Foo\n-----\n\n Foo\n ===\n": "NOT_TO_EQUAL", + " Foo\n ---\n\n Foo\n---\n": "NOT_TO_EQUAL", + "Foo\n ---- \n": "NOT_TO_EQUAL", + "Foo\n ---\n": "TO_EQUAL", + "Foo\n= =\n\nFoo\n--- -\n": "NOT_TO_EQUAL", + "Foo \n-----\n": "TO_EQUAL", + "Foo\\\n----\n": "TO_EQUAL", + "`Foo\n----\n`\n\n\n": "NOT_TO_EQUAL", + "> Foo\n---\n": "NOT_TO_EQUAL", + "> foo\nbar\n===\n": "NOT_TO_EQUAL", + "- Foo\n---\n": "NOT_TO_EQUAL", + "Foo\nBar\n---\n": "NOT_TO_EQUAL", + "---\nFoo\n---\nBar\n---\nBaz\n": "NOT_TO_EQUAL", + "\n====\n": "TO_EQUAL", + "---\n---\n": "TO_ERROR", + "- foo\n-----\n": "NOT_TO_EQUAL", + " foo\n---\n": "NOT_TO_EQUAL", + "> foo\n-----\n": "NOT_TO_EQUAL", + "\\> foo\n------\n": "NOT_TO_EQUAL", + "Foo\n\nbar\n---\nbaz\n": "TO_EQUAL", + "Foo\nbar\n\n---\n\nbaz\n": "NOT_TO_EQUAL", + "Foo\nbar\n* * *\nbaz\n": "NOT_TO_EQUAL", + "Foo\nbar\n\\---\nbaz\n": "NOT_TO_EQUAL", + " a simple\n indented code block\n": "TO_EQUAL", + " - foo\n\n bar\n": "NOT_TO_EQUAL", + "1. foo\n\n - bar\n": "TO_EQUAL", + " \n *hi*\n\n - one\n": "NOT_TO_EQUAL", + " chunk1\n\n chunk2\n \n \n \n chunk3\n": "TO_EQUAL", + " chunk1\n \n chunk2\n": "TO_EQUAL", + "Foo\n bar\n\n": "TO_EQUAL", + " foo\nbar\n": "TO_EQUAL", + "# Heading\n foo\nHeading\n------\n foo\n----\n": "NOT_TO_EQUAL", + " foo\n bar\n": "TO_EQUAL", + "\n \n foo\n \n\n": "TO_EQUAL", + " foo \n": "TO_EQUAL", + "```\n<\n >\n```\n": "NOT_TO_EQUAL", + "~~~\n<\n >\n~~~\n": "NOT_TO_EQUAL", + "``\nfoo\n``\n": "TO_EQUAL", + "```\naaa\n~~~\n```\n": "NOT_TO_EQUAL", + "~~~\naaa\n```\n~~~\n": "NOT_TO_EQUAL", + "````\naaa\n```\n``````\n": "NOT_TO_EQUAL", + "~~~~\naaa\n~~~\n~~~~\n": "NOT_TO_EQUAL", + "```\n": "TO_EQUAL", + "`````\n\n```\naaa\n": "NOT_TO_EQUAL", + "> ```\n> aaa\n\nbbb\n": "TO_EQUAL", + "```\n\n \n```\n": "NOT_TO_EQUAL", + "```\n```\n": "TO_EQUAL", + " ```\n aaa\naaa\n```\n": "TO_EQUAL", + " ```\naaa\n aaa\naaa\n ```\n": "TO_EQUAL", + " ```\n aaa\n aaa\n aaa\n ```\n": "TO_EQUAL", + " ```\n aaa\n ```\n": "NOT_TO_EQUAL", + "```\naaa\n ```\n": "TO_EQUAL", + " ```\naaa\n ```\n": "TO_EQUAL", + "```\naaa\n ```\n": "NOT_TO_EQUAL", + "``` ```\naaa\n": "TO_EQUAL", + "~~~~~~\naaa\n~~~ ~~\n": "NOT_TO_EQUAL", + "foo\n```\nbar\n```\nbaz\n": "TO_EQUAL", + "foo\n---\n~~~\nbar\n~~~\n# baz\n": "TO_EQUAL", + "```ruby\ndef foo(x)\n return 3\nend\n```\n": "TO_EQUAL", + "~~~~ ruby startline=3 $%@#$\ndef foo(x)\n return 3\nend\n~~~~~~~\n": "TO_EQUAL", + "````;\n````\n": "TO_EQUAL", + "``` aa ```\nfoo\n": "TO_EQUAL", + "```\n``` aaa\n```\n": "NOT_TO_EQUAL", + "
\n
\n**Hello**,\n\n_world_.\n
\n
\n": "NOT_TO_EQUAL", + "\n \n \n \n
\n hi\n
\n\nokay.\n": "TO_EQUAL", + "
\n*foo*\n": "NOT_TO_EQUAL", + "
\n\n*Markdown*\n\n
\n": "TO_EQUAL", + "
\n
\n": "TO_EQUAL", + "
\n
\n": "TO_EQUAL", + "
\n*foo*\n\n*bar*\n": "NOT_TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "
\nfoo\n
\n": "TO_EQUAL", + "
\n``` c\nint x = 33;\n```\n": "NOT_TO_EQUAL", + "\n*bar*\n\n": "NOT_TO_EQUAL", + "\n*bar*\n\n": "NOT_TO_EQUAL", + "\n*bar*\n\n": "NOT_TO_EQUAL", + "\n*bar*\n": "NOT_TO_EQUAL", + "\n*foo*\n\n": "NOT_TO_EQUAL", + "\n\n*foo*\n\n\n": "TO_EQUAL", + "*foo*\n": "TO_EQUAL", + "
\nimport Text.HTML.TagSoup\n\nmain :: IO ()\nmain = print $ parseTags tags\n
\nokay\n": "TO_EQUAL", + "\nokay\n": "TO_EQUAL", + "\nh1 {color:red;}\n\np {color:blue;}\n\nokay\n": "TO_EQUAL", + "\n\nfoo\n": "TO_EQUAL", + ">
\n> foo\n\nbar\n": "TO_EQUAL", + "-
\n- foo\n": "TO_EQUAL", + "\n*foo*\n": "TO_EQUAL", + "*bar*\n*baz*\n": "NOT_TO_EQUAL", + "1. *bar*\n": "NOT_TO_EQUAL", + "\nokay\n": "TO_EQUAL", + "';\n\n?>\nokay\n": "TO_EQUAL", + "\n": "TO_EQUAL", + "\nokay\n": "NOT_TO_EQUAL", + " \n\n \n": "NOT_TO_EQUAL", + "
\n\n
\n": "NOT_TO_EQUAL", + "Foo\n
\nbar\n
\n": "TO_EQUAL", + "
\nbar\n
\n*foo*\n": "NOT_TO_EQUAL", + "Foo\n\nbaz\n": "TO_EQUAL", + "
\n\n*Emphasized* text.\n\n
\n": "TO_EQUAL", + "
\n*Emphasized* text.\n
\n": "NOT_TO_EQUAL", + "\n\n\n\n\n\n\n\n
\nHi\n
\n": "TO_EQUAL", + "\n\n \n\n \n\n \n\n
\n Hi\n
\n": "NOT_TO_EQUAL", + "[foo]: /url \"title\"\n\n[foo]\n": "TO_EQUAL", + " [foo]: \n /url \n 'the title' \n\n[foo]\n": "TO_EQUAL", + "[Foo*bar\\]]:my_(url) 'title (with parens)'\n\n[Foo*bar\\]]\n": "NOT_TO_EQUAL", + "[Foo bar]:\n\n'title'\n\n[Foo bar]\n": "TO_EQUAL", + "[foo]: /url '\ntitle\nline1\nline2\n'\n\n[foo]\n": "NOT_TO_EQUAL", + "[foo]: /url 'title\n\nwith blank line'\n\n[foo]\n": "TO_EQUAL", + "[foo]:\n/url\n\n[foo]\n": "TO_EQUAL", + "[foo]:\n\n[foo]\n": "TO_EQUAL", + "[foo]: /url\\bar\\*baz \"foo\\\"bar\\baz\"\n\n[foo]\n": "NOT_TO_EQUAL", + "[foo]\n\n[foo]: url\n": "TO_EQUAL", + "[foo]\n\n[foo]: first\n[foo]: second\n": "NOT_TO_EQUAL", + "[FOO]: /url\n\n[Foo]\n": "TO_EQUAL", + "[ΑΓΩ]: /φου\n\n[αγω]\n": "TO_EQUAL", + "[foo]: /url\n": "TO_ERROR", + "[\nfoo\n]: /url\nbar\n": "TO_EQUAL", + "[foo]: /url \"title\" ok\n": "NOT_TO_EQUAL", + "[foo]: /url\n\"title\" ok\n": "NOT_TO_EQUAL", + " [foo]: /url \"title\"\n\n[foo]\n": "NOT_TO_EQUAL", + "```\n[foo]: /url\n```\n\n[foo]\n": "TO_EQUAL", + "Foo\n[bar]: /baz\n\n[bar]\n": "TO_EQUAL", + "# [Foo]\n[foo]: /url\n> bar\n": "TO_EQUAL", + "[foo]: /foo-url \"foo\"\n[bar]: /bar-url\n \"bar\"\n[baz]: /baz-url\n\n[foo],\n[bar],\n[baz]\n": "TO_EQUAL", + "[foo]\n\n> [foo]: /url\n": "NOT_TO_EQUAL", + "aaa\n\nbbb\n": "TO_EQUAL", + "aaa\nbbb\n\nccc\nddd\n": "TO_EQUAL", + "aaa\n\n\nbbb\n": "TO_EQUAL", + " aaa\n bbb\n": "NOT_TO_EQUAL", + "aaa\n bbb\n ccc\n": "TO_EQUAL", + " aaa\nbbb\n": "NOT_TO_EQUAL", + " aaa\nbbb\n": "TO_EQUAL", + "aaa \nbbb \n": "NOT_TO_EQUAL", + " \n\naaa\n \n\n# aaa\n\n \n": "TO_EQUAL", + "> # Foo\n> bar\n> baz\n": "TO_EQUAL", + "># Foo\n>bar\n> baz\n": "TO_EQUAL", + " > # Foo\n > bar\n > baz\n": "TO_EQUAL", + " > # Foo\n > bar\n > baz\n": "NOT_TO_EQUAL", + "> # Foo\n> bar\nbaz\n": "TO_EQUAL", + "> bar\nbaz\n> foo\n": "TO_EQUAL", + "> foo\n---\n": "NOT_TO_EQUAL", + "> - foo\n- bar\n": "TO_EQUAL", + "> foo\n bar\n": "TO_EQUAL", + "> ```\nfoo\n```\n": "NOT_TO_EQUAL", + "> foo\n - bar\n": "NOT_TO_EQUAL", + ">\n": "TO_ERROR", + ">\n> \n> \n": "TO_ERROR", + ">\n> foo\n> \n": "TO_EQUAL", + "> foo\n\n> bar\n": "NOT_TO_EQUAL", + "> foo\n> bar\n": "TO_EQUAL", + "> foo\n>\n> bar\n": "TO_EQUAL", + "foo\n> bar\n": "TO_EQUAL", + "> aaa\n***\n> bbb\n": "NOT_TO_EQUAL", + "> bar\nbaz\n": "TO_EQUAL", + "> bar\n\nbaz\n": "TO_EQUAL", + "> bar\n>\nbaz\n": "NOT_TO_EQUAL", + "> > > foo\nbar\n": "TO_EQUAL", + ">>> foo\n> bar\n>>baz\n": "TO_EQUAL", + "> code\n\n> not code\n": "NOT_TO_EQUAL", + "A paragraph\nwith two lines.\n\n indented code\n\n> A block quote.\n": "TO_EQUAL", + "1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "TO_EQUAL", + "- one\n\n two\n": "NOT_TO_EQUAL", + "- one\n\n two\n": "NOT_TO_EQUAL", + " - one\n\n two\n": "NOT_TO_EQUAL", + " - one\n\n two\n": "NOT_TO_EQUAL", + " > > 1. one\n>>\n>> two\n": "NOT_TO_EQUAL", + ">>- one\n>>\n > > two\n": "TO_EQUAL", + "-one\n\n2.two\n": "TO_EQUAL", + "- foo\n\n\n bar\n": "TO_EQUAL", + "1. foo\n\n ```\n bar\n ```\n\n baz\n\n > bam\n": "TO_EQUAL", + "- Foo\n\n bar\n\n\n baz\n": "NOT_TO_EQUAL", + "123456789. ok\n": "TO_EQUAL", + "1234567890. not ok\n": "NOT_TO_EQUAL", + "0. ok\n": "NOT_TO_EQUAL", + "003. ok\n": "TO_EQUAL", + "-1. not ok\n": "TO_EQUAL", + "- foo\n\n bar\n": "TO_EQUAL", + " 10. foo\n\n bar\n": "TO_EQUAL", + " indented code\n\nparagraph\n\n more code\n": "TO_EQUAL", + "1. indented code\n\n paragraph\n\n more code\n": "TO_EQUAL", + "1. indented code\n\n paragraph\n\n more code\n": "TO_EQUAL", + " foo\n\nbar\n": "NOT_TO_EQUAL", + "- foo\n\n bar\n": "NOT_TO_EQUAL", + "- foo\n\n bar\n": "NOT_TO_EQUAL", + "-\n foo\n-\n ```\n bar\n ```\n-\n baz\n": "NOT_TO_EQUAL", + "- \n foo\n": "TO_ERROR", + "-\n\n foo\n": "NOT_TO_EQUAL", + "- foo\n-\n- bar\n": "TO_ERROR", + "- foo\n- \n- bar\n": "TO_ERROR", + "1. foo\n2.\n3. bar\n": "TO_ERROR", + "*\n": "NOT_TO_EQUAL", + "foo\n*\n\nfoo\n1.\n": "TO_EQUAL", + " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "TO_EQUAL", + " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "TO_EQUAL", + " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "TO_EQUAL", + " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "NOT_TO_EQUAL", + " 1. A paragraph\nwith two lines.\n\n indented code\n\n > A block quote.\n": "NOT_TO_EQUAL", + " 1. A paragraph\n with two lines.\n": "TO_EQUAL", + "> 1. > Blockquote\ncontinued here.\n": "TO_EQUAL", + "> 1. > Blockquote\n> continued here.\n": "TO_EQUAL", + "- foo\n - bar\n - baz\n - boo\n": "NOT_TO_EQUAL", + "- foo\n - bar\n - baz\n - boo\n": "TO_EQUAL", + "10) foo\n - bar\n": "NOT_TO_EQUAL", + "10) foo\n - bar\n": "TO_EQUAL", + "- - foo\n": "TO_EQUAL", + "1. - 2. foo\n": "TO_EQUAL", + "- # Foo\n- Bar\n ---\n baz\n": "NOT_TO_EQUAL", + "- foo\n- bar\n+ baz\n": "TO_EQUAL", + "1. foo\n2. bar\n3) baz\n": "TO_EQUAL", + "Foo\n- bar\n- baz\n": "TO_EQUAL", + "The number of windows in my house is\n14. The number of doors is 6.\n": "NOT_TO_EQUAL", + "The number of windows in my house is\n1. The number of doors is 6.\n": "TO_EQUAL", + "- foo\n\n- bar\n\n\n- baz\n": "NOT_TO_EQUAL", + "- foo\n - bar\n - baz\n\n\n bim\n": "NOT_TO_EQUAL", + "- foo\n- bar\n\n\n\n- baz\n- bim\n": "TO_EQUAL", + "- foo\n\n notcode\n\n- foo\n\n\n\n code\n": "NOT_TO_EQUAL", + "- a\n - b\n - c\n - d\n - e\n - f\n - g\n - h\n- i\n": "NOT_TO_EQUAL", + "1. a\n\n 2. b\n\n 3. c\n": "NOT_TO_EQUAL", + "- a\n- b\n\n- c\n": "NOT_TO_EQUAL", + "* a\n*\n\n* c\n": "TO_ERROR", + "- a\n- b\n\n c\n- d\n": "NOT_TO_EQUAL", + "- a\n- b\n\n [ref]: /url\n- d\n": "NOT_TO_EQUAL", + "- a\n- ```\n b\n\n\n ```\n- c\n": "NOT_TO_EQUAL", + "- a\n - b\n\n c\n- d\n": "NOT_TO_EQUAL", + "* a\n > b\n >\n* c\n": "NOT_TO_EQUAL", + "- a\n > b\n ```\n c\n ```\n- d\n": "NOT_TO_EQUAL", + "- a\n": "TO_EQUAL", + "- a\n - b\n": "NOT_TO_EQUAL", + "1. ```\n foo\n ```\n\n bar\n": "TO_EQUAL", + "* foo\n * bar\n\n baz\n": "NOT_TO_EQUAL", + "- a\n - b\n - c\n\n- d\n - e\n - f\n": "TO_EQUAL", + "`hi`lo`\n": "TO_EQUAL", + "\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~\n": "NOT_TO_EQUAL", + "\\\t\\A\\a\\ \\3\\φ\\«\n": "TO_EQUAL", + "\\*not emphasized*\n\\
not a tag\n\\[not a link](/foo)\n\\`not code`\n1\\. not a list\n\\* not a list\n\\# not a heading\n\\[foo]: /url \"not a reference\"\n": "NOT_TO_EQUAL", + "\\\\*emphasis*\n": "NOT_TO_EQUAL", + "foo\\\nbar\n": "NOT_TO_EQUAL", + "`` \\[\\` ``\n": "TO_EQUAL", + " \\[\\]\n": "TO_EQUAL", + "~~~\n\\[\\]\n~~~\n": "TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "
\n": "TO_EQUAL", + "[foo](/bar\\* \"ti\\*tle\")\n": "TO_EQUAL", + "[foo]\n\n[foo]: /bar\\* \"ti\\*tle\"\n": "TO_EQUAL", + "``` foo\\+bar\nfoo\n```\n": "TO_EQUAL", + "  & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸\n": "NOT_TO_EQUAL", + "# Ӓ Ϡ � �\n": "NOT_TO_EQUAL", + "" ആ ಫ\n": "NOT_TO_EQUAL", + "  &x; &#; &#x;\n&ThisIsNotDefined; &hi?;\n": "NOT_TO_EQUAL", + "©\n": "NOT_TO_EQUAL", + "&MadeUpEntity;\n": "NOT_TO_EQUAL", + "\n": "TO_EQUAL", + "[foo](/föö \"föö\")\n": "TO_EQUAL", + "[foo]\n\n[foo]: /föö \"föö\"\n": "TO_EQUAL", + "``` föö\nfoo\n```\n": "TO_EQUAL", + "`föö`\n": "NOT_TO_EQUAL", + " föfö\n": "NOT_TO_EQUAL", + "`foo`\n": "TO_EQUAL", + "`` foo ` bar ``\n": "TO_EQUAL", + "` `` `\n": "TO_EQUAL", + "`foo bar\n baz`\n": "TO_EQUAL", + "`a b`\n": "NOT_TO_EQUAL", + "`foo `` bar`\n": "TO_EQUAL", + "`foo\\`bar`\n": "TO_EQUAL", + "*foo`*`\n": "NOT_TO_EQUAL", + "[not a `link](/foo`)\n": "NOT_TO_EQUAL", + "``\n": "NOT_TO_EQUAL", + "`\n": "TO_EQUAL", + "``\n": "NOT_TO_EQUAL", + "`\n": "TO_EQUAL", + "```foo``\n": "NOT_TO_EQUAL", + "`foo\n": "TO_EQUAL", + "`foo``bar``\n": "NOT_TO_EQUAL", + "*foo bar*\n": "TO_EQUAL", + "a * foo bar*\n": "NOT_TO_EQUAL", + "a*\"foo\"*\n": "NOT_TO_EQUAL", + "* a *\n": "NOT_TO_EQUAL", + "foo*bar*\n": "TO_EQUAL", + "5*6*78\n": "NOT_TO_EQUAL", + "_foo bar_\n": "TO_EQUAL", + "_ foo bar_\n": "NOT_TO_EQUAL", + "a_\"foo\"_\n": "NOT_TO_EQUAL", + "foo_bar_\n": "NOT_TO_EQUAL", + "5_6_78\n": "TO_EQUAL", + "пристаням_стремятся_\n": "NOT_TO_EQUAL", + "aa_\"bb\"_cc\n": "NOT_TO_EQUAL", + "foo-_(bar)_\n": "TO_EQUAL", + "_foo*\n": "TO_EQUAL", + "*foo bar *\n": "NOT_TO_EQUAL", + "*foo bar\n*\n": "NOT_TO_EQUAL", + "*(*foo)\n": "NOT_TO_EQUAL", + "*(*foo*)*\n": "NOT_TO_EQUAL", + "*foo*bar\n": "NOT_TO_EQUAL", + "_foo bar _\n": "NOT_TO_EQUAL", + "_(_foo)\n": "TO_EQUAL", + "_(_foo_)_\n": "NOT_TO_EQUAL", + "_foo_bar\n": "TO_EQUAL", + "_пристаням_стремятся\n": "NOT_TO_EQUAL", + "_foo_bar_baz_\n": "TO_EQUAL", + "_(bar)_.\n": "TO_EQUAL", + "**foo bar**\n": "TO_EQUAL", + "** foo bar**\n": "NOT_TO_EQUAL", + "a**\"foo\"**\n": "NOT_TO_EQUAL", + "foo**bar**\n": "TO_EQUAL", + "__foo bar__\n": "TO_EQUAL", + "__ foo bar__\n": "NOT_TO_EQUAL", + "__\nfoo bar__\n": "NOT_TO_EQUAL", + "a__\"foo\"__\n": "NOT_TO_EQUAL", + "foo__bar__\n": "NOT_TO_EQUAL", + "5__6__78\n": "NOT_TO_EQUAL", + "пристаням__стремятся__\n": "NOT_TO_EQUAL", + "__foo, __bar__, baz__\n": "NOT_TO_EQUAL", + "foo-__(bar)__\n": "TO_EQUAL", + "**foo bar **\n": "NOT_TO_EQUAL", + "**(**foo)\n": "NOT_TO_EQUAL", + "*(**foo**)*\n": "TO_EQUAL", + "**Gomphocarpus (*Gomphocarpus physocarpus*, syn.\n*Asclepias physocarpa*)**\n": "TO_EQUAL", + "**foo \"*bar*\" foo**\n": "NOT_TO_EQUAL", + "**foo**bar\n": "TO_EQUAL", + "__foo bar __\n": "NOT_TO_EQUAL", + "__(__foo)\n": "NOT_TO_EQUAL", + "_(__foo__)_\n": "TO_EQUAL", + "__foo__bar\n": "NOT_TO_EQUAL", + "__пристаням__стремятся\n": "NOT_TO_EQUAL", + "__foo__bar__baz__\n": "NOT_TO_EQUAL", + "__(bar)__.\n": "TO_EQUAL", + "*foo [bar](/url)*\n": "TO_EQUAL", + "*foo\nbar*\n": "TO_EQUAL", + "_foo __bar__ baz_\n": "TO_EQUAL", + "_foo _bar_ baz_\n": "NOT_TO_EQUAL", + "__foo_ bar_\n": "NOT_TO_EQUAL", + "*foo *bar**\n": "NOT_TO_EQUAL", + "*foo **bar** baz*\n": "TO_EQUAL", + "*foo**bar**baz*\n": "TO_EQUAL", + "***foo** bar*\n": "NOT_TO_EQUAL", + "*foo **bar***\n": "NOT_TO_EQUAL", + "*foo**bar***\n": "NOT_TO_EQUAL", + "*foo **bar *baz* bim** bop*\n": "NOT_TO_EQUAL", + "*foo [*bar*](/url)*\n": "NOT_TO_EQUAL", + "** is not an empty emphasis\n": "TO_EQUAL", + "**** is not an empty strong emphasis\n": "TO_EQUAL", + "**foo [bar](/url)**\n": "TO_EQUAL", + "**foo\nbar**\n": "TO_EQUAL", + "__foo _bar_ baz__\n": "TO_EQUAL", + "__foo __bar__ baz__\n": "NOT_TO_EQUAL", + "____foo__ bar__\n": "NOT_TO_EQUAL", + "**foo **bar****\n": "NOT_TO_EQUAL", + "**foo *bar* baz**\n": "TO_EQUAL", + "**foo*bar*baz**\n": "NOT_TO_EQUAL", + "***foo* bar**\n": "TO_EQUAL", + "**foo *bar***\n": "TO_EQUAL", + "**foo *bar **baz**\nbim* bop**\n": "NOT_TO_EQUAL", + "**foo [*bar*](/url)**\n": "TO_EQUAL", + "__ is not an empty emphasis\n": "TO_EQUAL", + "____ is not an empty strong emphasis\n": "TO_EQUAL", + "foo ***\n": "TO_EQUAL", + "foo *\\**\n": "NOT_TO_EQUAL", + "foo *_*\n": "NOT_TO_EQUAL", + "foo *****\n": "NOT_TO_EQUAL", + "foo **\\***\n": "TO_EQUAL", + "foo **_**\n": "TO_EQUAL", + "**foo*\n": "TO_EQUAL", + "*foo**\n": "NOT_TO_EQUAL", + "***foo**\n": "NOT_TO_EQUAL", + "****foo*\n": "NOT_TO_EQUAL", + "**foo***\n": "NOT_TO_EQUAL", + "*foo****\n": "NOT_TO_EQUAL", + "foo ___\n": "TO_EQUAL", + "foo _\\__\n": "NOT_TO_EQUAL", + "foo _*_\n": "TO_EQUAL", + "foo _____\n": "NOT_TO_EQUAL", + "foo __\\___\n": "TO_EQUAL", + "foo __*__\n": "TO_EQUAL", + "__foo_\n": "TO_EQUAL", + "_foo__\n": "NOT_TO_EQUAL", + "___foo__\n": "NOT_TO_EQUAL", + "____foo_\n": "NOT_TO_EQUAL", + "__foo___\n": "NOT_TO_EQUAL", + "_foo____\n": "NOT_TO_EQUAL", + "**foo**\n": "TO_EQUAL", + "*_foo_*\n": "NOT_TO_EQUAL", + "__foo__\n": "TO_EQUAL", + "_*foo*_\n": "NOT_TO_EQUAL", + "****foo****\n": "NOT_TO_EQUAL", + "____foo____\n": "NOT_TO_EQUAL", + "******foo******\n": "NOT_TO_EQUAL", + "***foo***\n": "TO_EQUAL", + "_____foo_____\n": "NOT_TO_EQUAL", + "*foo _bar* baz_\n": "TO_EQUAL", + "*foo __bar *baz bim__ bam*\n": "NOT_TO_EQUAL", + "**foo **bar baz**\n": "NOT_TO_EQUAL", + "*foo *bar baz*\n": "NOT_TO_EQUAL", + "*[bar*](/url)\n": "NOT_TO_EQUAL", + "_foo [bar_](/url)\n": "NOT_TO_EQUAL", + "*\n": "NOT_TO_EQUAL", + "**\n": "NOT_TO_EQUAL", + "__\n": "NOT_TO_EQUAL", + "*a `*`*\n": "NOT_TO_EQUAL", + "_a `_`_\n": "NOT_TO_EQUAL", + "**a\n": "NOT_TO_EQUAL", + "__a\n": "NOT_TO_EQUAL", + "[link](/uri \"title\")\n": "TO_EQUAL", + "[link](/uri)\n": "TO_EQUAL", + "[link]()\n": "TO_EQUAL", + "[link](<>)\n": "TO_EQUAL", + "[link](/my uri)\n": "TO_EQUAL", + "[link]()\n": "NOT_TO_EQUAL", + "[link](foo\nbar)\n": "TO_EQUAL", + "[link]()\n": "TO_EQUAL", + "[link](\\(foo\\))\n": "TO_EQUAL", + "[link](foo(and(bar)))\n": "TO_EQUAL", + "[link](foo\\(and\\(bar\\))\n": "TO_EQUAL", + "[link]()\n": "TO_EQUAL", + "[link](foo\\)\\:)\n": "TO_EQUAL", + "[link](#fragment)\n\n[link](http://example.com#fragment)\n\n[link](http://example.com?foo=3#frag)\n": "TO_EQUAL", + "[link](foo\\bar)\n": "TO_EQUAL", + "[link](foo%20bä)\n": "TO_EQUAL", + "[link](\"title\")\n": "TO_EQUAL", + "[link](/url \"title\")\n[link](/url 'title')\n[link](/url (title))\n": "TO_EQUAL", + "[link](/url \"title \\\""\")\n": "NOT_TO_EQUAL", + "[link](/url \"title\")\n": "NOT_TO_EQUAL", + "[link](/url \"title \"and\" title\")\n": "NOT_TO_EQUAL", + "[link](/url 'title \"and\" title')\n": "NOT_TO_EQUAL", + "[link]( /uri\n \"title\" )\n": "TO_EQUAL", + "[link] (/uri)\n": "NOT_TO_EQUAL", + "[link [foo [bar]]](/uri)\n": "NOT_TO_EQUAL", + "[link] bar](/uri)\n": "TO_EQUAL", + "[link [bar](/uri)\n": "TO_EQUAL", + "[link \\[bar](/uri)\n": "NOT_TO_EQUAL", + "[link *foo **bar** `#`*](/uri)\n": "TO_EQUAL", + "[![moon](moon.jpg)](/uri)\n": "NOT_TO_EQUAL", + "[foo [bar](/uri)](/uri)\n": "NOT_TO_EQUAL", + "[foo *[bar [baz](/uri)](/uri)*](/uri)\n": "NOT_TO_EQUAL", + "![[[foo](uri1)](uri2)](uri3)\n": "NOT_TO_EQUAL", + "*[foo*](/uri)\n": "NOT_TO_EQUAL", + "[foo *bar](baz*)\n": "TO_EQUAL", + "*foo [bar* baz]\n": "TO_EQUAL", + "[foo \n": "NOT_TO_EQUAL", + "[foo`](/uri)`\n": "NOT_TO_EQUAL", + "[foo\n": "NOT_TO_EQUAL", + "[foo][bar]\n\n[bar]: /url \"title\"\n": "TO_EQUAL", + "[link [foo [bar]]][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL", + "[link \\[bar][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL", + "[link *foo **bar** `#`*][ref]\n\n[ref]: /uri\n": "TO_EQUAL", + "[![moon](moon.jpg)][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL", + "[foo [bar](/uri)][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL", + "[foo *bar [baz][ref]*][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL", + "*[foo*][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL", + "[foo *bar][ref]\n\n[ref]: /uri\n": "TO_EQUAL", + "[foo \n\n[ref]: /uri\n": "NOT_TO_EQUAL", + "[foo`][ref]`\n\n[ref]: /uri\n": "NOT_TO_EQUAL", + "[foo\n\n[ref]: /uri\n": "NOT_TO_EQUAL", + "[foo][BaR]\n\n[bar]: /url \"title\"\n": "TO_EQUAL", + "[Толпой][Толпой] is a Russian word.\n\n[ТОЛПОЙ]: /url\n": "TO_EQUAL", + "[Foo\n bar]: /url\n\n[Baz][Foo bar]\n": "TO_EQUAL", + "[foo] [bar]\n\n[bar]: /url \"title\"\n": "NOT_TO_EQUAL", + "[foo]\n[bar]\n\n[bar]: /url \"title\"\n": "NOT_TO_EQUAL", + "[foo]: /url1\n\n[foo]: /url2\n\n[bar][foo]\n": "NOT_TO_EQUAL", + "[bar][foo\\!]\n\n[foo!]: /url\n": "NOT_TO_EQUAL", + "[foo][ref[]\n\n[ref[]: /uri\n": "NOT_TO_EQUAL", + "[foo][ref[bar]]\n\n[ref[bar]]: /uri\n": "NOT_TO_EQUAL", + "[[[foo]]]\n\n[[[foo]]]: /url\n": "TO_EQUAL", + "[foo][ref\\[]\n\n[ref\\[]: /uri\n": "TO_EQUAL", + "[bar\\\\]: /uri\n\n[bar\\\\]\n": "NOT_TO_EQUAL", + "[]\n\n[]: /uri\n": "TO_EQUAL", + "[\n ]\n\n[\n ]: /uri\n": "NOT_TO_EQUAL", + "[foo][]\n\n[foo]: /url \"title\"\n": "TO_EQUAL", + "[*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n": "TO_EQUAL", + "[Foo][]\n\n[foo]: /url \"title\"\n": "TO_EQUAL", + "[foo] \n[]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", + "[foo]\n\n[foo]: /url \"title\"\n": "TO_EQUAL", + "[*foo* bar]\n\n[*foo* bar]: /url \"title\"\n": "TO_EQUAL", + "[[*foo* bar]]\n\n[*foo* bar]: /url \"title\"\n": "TO_EQUAL", + "[[bar [foo]\n\n[foo]: /url\n": "TO_EQUAL", + "[Foo]\n\n[foo]: /url \"title\"\n": "TO_EQUAL", + "[foo] bar\n\n[foo]: /url\n": "TO_EQUAL", + "\\[foo]\n\n[foo]: /url \"title\"\n": "TO_EQUAL", + "[foo*]: /url\n\n*[foo*]\n": "NOT_TO_EQUAL", + "[foo][bar]\n\n[foo]: /url1\n[bar]: /url2\n": "TO_EQUAL", + "[foo][]\n\n[foo]: /url1\n": "TO_EQUAL", + "[foo]()\n\n[foo]: /url1\n": "TO_EQUAL", + "[foo](not a link)\n\n[foo]: /url1\n": "TO_EQUAL", + "[foo][bar][baz]\n\n[baz]: /url\n": "NOT_TO_EQUAL", + "[foo][bar][baz]\n\n[baz]: /url1\n[bar]: /url2\n": "TO_EQUAL", + "[foo][bar][baz]\n\n[baz]: /url1\n[foo]: /url2\n": "NOT_TO_EQUAL", + "![foo](/url \"title\")\n": "NOT_TO_EQUAL", + "![foo *bar*]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n": "NOT_TO_EQUAL", + "![foo ![bar](/url)](/url2)\n": "NOT_TO_EQUAL", + "![foo [bar](/url)](/url2)\n": "NOT_TO_EQUAL", + "![foo *bar*][]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n": "NOT_TO_EQUAL", + "![foo *bar*][foobar]\n\n[FOOBAR]: train.jpg \"train & tracks\"\n": "NOT_TO_EQUAL", + "![foo](train.jpg)\n": "NOT_TO_EQUAL", + "My ![foo bar](/path/to/train.jpg \"title\" )\n": "NOT_TO_EQUAL", + "![foo]()\n": "NOT_TO_EQUAL", + "![](/url)\n": "NOT_TO_EQUAL", + "![foo][bar]\n\n[bar]: /url\n": "NOT_TO_EQUAL", + "![foo][bar]\n\n[BAR]: /url\n": "NOT_TO_EQUAL", + "![foo][]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", + "![*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n": "NOT_TO_EQUAL", + "![Foo][]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", + "![foo] \n[]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", + "![foo]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", + "![*foo* bar]\n\n[*foo* bar]: /url \"title\"\n": "NOT_TO_EQUAL", + "![[foo]]\n\n[[foo]]: /url \"title\"\n": "NOT_TO_EQUAL", + "![Foo]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", + "!\\[foo]\n\n[foo]: /url \"title\"\n": "TO_EQUAL", + "\\![foo]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "<>\n": "NOT_TO_EQUAL", + "< http://foo.bar >\n": "NOT_TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "http://example.com\n": "TO_EQUAL", + "foo@bar.example.com\n": "TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "TO_EQUAL", + "Foo \n": "TO_EQUAL", + "<33> <__>\n": "NOT_TO_EQUAL", + "\n": "NOT_TO_EQUAL", + " \n": "NOT_TO_EQUAL", + "< a><\nfoo>\n": "NOT_TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "foo \n": "TO_EQUAL", + "foo \n": "NOT_TO_EQUAL", + "foo foo -->\n\nfoo \n": "NOT_TO_EQUAL", + "foo \n": "TO_EQUAL", + "foo \n": "TO_EQUAL", + "foo &<]]>\n": "NOT_TO_EQUAL", + "foo \n": "TO_EQUAL", + "foo \n": "TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "foo \nbaz\n": "NOT_TO_EQUAL", + "foo\\\nbaz\n": "NOT_TO_EQUAL", + "foo \nbaz\n": "NOT_TO_EQUAL", + "foo \n bar\n": "NOT_TO_EQUAL", + "foo\\\n bar\n": "NOT_TO_EQUAL", + "*foo \nbar*\n": "NOT_TO_EQUAL", + "*foo\\\nbar*\n": "NOT_TO_EQUAL", + "`code \nspan`\n": "TO_EQUAL", + "`code\\\nspan`\n": "TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "TO_EQUAL", + "foo\\\n": "TO_EQUAL", + "foo \n": "TO_EQUAL", + "### foo\\\n": "TO_EQUAL", + "### foo \n": "TO_EQUAL", + "foo\nbaz\n": "TO_EQUAL", + "foo \n baz\n": "TO_EQUAL", + "hello $.;'there\n": "TO_EQUAL", + "Foo χρῆν\n": "TO_EQUAL", + "Multiple spaces\n": "TO_EQUAL" +} \ No newline at end of file diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/duplicate_marks_github_issue_3280.md b/packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/duplicate_marks_github_issue_3280.md new file mode 100644 index 000000000000..2d6cad7de1d4 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/duplicate_marks_github_issue_3280.md @@ -0,0 +1 @@ +Fill to_*this*_mark, and your charge is but a penny; to_*this*_a penny more; and so on to the full glass—the Cape Horn measure, which you may gulp down for a shilling.\n\nUpon entering the place I found a number of young seamen gathered about a table, examining by a dim light divers specimens of_*skrimshander*. \ No newline at end of file diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/commonmark.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/commonmark.spec.js new file mode 100644 index 000000000000..5998821084f9 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/commonmark.spec.js @@ -0,0 +1,110 @@ +import { flow } from 'lodash'; +import { tests as commonmarkSpec } from 'commonmark-spec'; +import * as commonmark from 'commonmark'; + +import { markdownToSlate, slateToMarkdown } from '../index.js'; + +const skips = [ + { + number: [456], + reason: 'Remark ¯\\_(ツ)_/¯', + }, + { + number: [416, 417, 424, 425, 426, 431, 457, 460, 462, 464, 467], + reason: 'Remark does not support infinite (redundant) nested marks', + }, + { + number: [455, 469, 470, 471], + reason: 'Remark parses the initial set of identical nested delimiters first', + }, + { + number: [473, 476, 478, 480], + reason: 'we convert underscores to asterisks for strong/emphasis', + }, + { number: 490, reason: 'Remark strips pointy enclosing pointy brackets from link url' }, + { number: 503, reason: 'Remark allows non-breaking space between link url and title' }, + { number: 507, reason: 'Remark allows a space between link alt and url' }, + { + number: [ + 511, 516, 525, 528, 529, 530, 532, 533, 534, 540, 541, 542, 543, 546, 548, 560, 565, 567, + ], + reason: 'we convert link references to standard links, but Remark also fails these', + }, + { + number: [569, 570, 571, 572, 573, 581, 585], + reason: 'Remark does not recognize or remove marks in image alt text', + }, + { number: 589, reason: 'Remark does not honor backslash escape of image exclamation point' }, + { number: 593, reason: 'Remark removes "mailto:" from autolink text' }, + { number: 599, reason: 'Remark does not escape all expected entities' }, + { number: 602, reason: 'Remark allows autolink emails to contain backslashes' }, +]; + +const onlys = [ + // just add the spec number, eg: + // 431, +]; + +/** + * Each test receives input markdown and output html as expected for Commonmark + * compliance. To test all of our handling in one go, we serialize the markdown + * into our Slate AST, then back to raw markdown, and finally to HTML. + */ +const reader = new commonmark.Parser(); +const writer = new commonmark.HtmlRenderer(); + +function parseWithCommonmark(markdown) { + const parsed = reader.parse(markdown); + return writer.render(parsed); +} + +const parse = flow([markdownToSlate, slateToMarkdown]); + +/** + * Passing this test suite requires 100% Commonmark compliance. There are 624 + * tests, of which we're passing about 300 as of introduction of this suite. To + * work on improving Commonmark support, update __fixtures__/commonmarkExpected.json + */ +describe.skip('Commonmark support', function () { + const specs = + onlys.length > 0 + ? commonmarkSpec.filter(({ number }) => onlys.includes(number)) + : commonmarkSpec; + specs.forEach(spec => { + const skip = skips.find(({ number }) => { + return Array.isArray(number) ? number.includes(spec.number) : number === spec.number; + }); + const specUrl = `https://spec.commonmark.org/0.29/#example-${spec.number}`; + const parsed = parse(spec.markdown); + const commonmarkParsedHtml = parseWithCommonmark(parsed); + const description = ` +${spec.section} +${specUrl} + +Spec: +${JSON.stringify(spec, null, 2)} + +Markdown input: +${spec.markdown} + +Markdown parsed through Slate/Remark and back to Markdown: +${parsed} + +HTML output: +${commonmarkParsedHtml} + +Expected HTML output: +${spec.html} + `; + if (skip) { + const showMessage = Array.isArray(skip.number) ? skip.number[0] === spec.number : true; + if (showMessage) { + //console.log(`skipping spec ${skip.number}\n${skip.reason}\n${specUrl}`); + } + } + const testFn = skip ? test.skip : test; + testFn(description, () => { + expect(commonmarkParsedHtml).toEqual(spec.html); + }); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/index.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/index.spec.js new file mode 100644 index 000000000000..33a4fac515ac --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/index.spec.js @@ -0,0 +1,52 @@ +import path from 'path'; +import fs from 'fs'; + +import { markdownToSlate, htmlToSlate } from '../'; + +describe('markdownToSlate', () => { + it('should not add duplicate identical marks under the same node (GitHub Issue 3280)', () => { + const mdast = fs.readFileSync( + path.join(__dirname, '__fixtures__', 'duplicate_marks_github_issue_3280.md'), + ); + const slate = markdownToSlate(mdast); + + expect(slate).toEqual([ + { + type: 'p', + children: [ + { + text: 'Fill to', + }, + { + italic: true, + marks: [{ type: 'italic' }], + text: 'this_mark, and your charge is but a penny; tothisa penny more; and so on to the full glass—the Cape Horn measure, which you may gulp down for a shilling.\\n\\nUpon entering the place I found a number of young seamen gathered about a table, examining by a dim light divers specimens ofskrimshander', + }, + { + text: '.', + }, + ], + }, + ]); + }); +}); + +describe('htmlToSlate', () => { + it('should preserve spaces in rich html (GitHub Issue 3727)', () => { + const html = `Bold Text regular text `; + + const actual = htmlToSlate(html); + expect(actual).toEqual({ + type: 'root', + children: [ + { + type: 'p', + children: [ + { text: 'Bold Text', bold: true, marks: [{ type: 'bold' }] }, + { text: ' regular text' }, + ], + }, + ], + }); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAllowHtmlEntities.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAllowHtmlEntities.spec.js new file mode 100644 index 000000000000..844137f0a440 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAllowHtmlEntities.spec.js @@ -0,0 +1,25 @@ +import unified from 'unified'; +import markdownToRemark from 'remark-parse'; + +import remarkAllowHtmlEntities from '../remarkAllowHtmlEntities'; + +function process(markdown) { + const mdast = unified().use(markdownToRemark).use(remarkAllowHtmlEntities).parse(markdown); + + /** + * The MDAST will look like: + * + * { type: 'root', children: [ + * { type: 'paragraph', children: [ + * // results here + * ]} + * ]} + */ + return mdast.children[0].children[0].value; +} + +describe('remarkAllowHtmlEntities', () => { + it('should not decode HTML entities', () => { + expect(process('<div>')).toEqual('<div>'); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAssertParents.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAssertParents.spec.js new file mode 100644 index 000000000000..670d5c7900ef --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAssertParents.spec.js @@ -0,0 +1,171 @@ +import u from 'unist-builder'; + +import remarkAssertParents from '../remarkAssertParents'; + +const transform = remarkAssertParents(); + +describe('remarkAssertParents', () => { + it('should unnest invalidly nested blocks', () => { + const input = u('root', [ + u('paragraph', [ + u('paragraph', [u('text', 'Paragraph text.')]), + u('heading', { depth: 1 }, [u('text', 'Heading text.')]), + u('code', 'someCode()'), + u('blockquote', [u('text', 'Quote text.')]), + u('list', [u('listItem', [u('text', 'A list item.')])]), + u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]), + u('thematicBreak'), + ]), + ]); + + const output = u('root', [ + u('paragraph', [u('text', 'Paragraph text.')]), + u('heading', { depth: 1 }, [u('text', 'Heading text.')]), + u('code', 'someCode()'), + u('blockquote', [u('text', 'Quote text.')]), + u('list', [u('listItem', [u('text', 'A list item.')])]), + u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]), + u('thematicBreak'), + ]); + + expect(transform(input)).toEqual(output); + }); + + it('should unnest deeply nested blocks', () => { + const input = u('root', [ + u('paragraph', [ + u('paragraph', [ + u('paragraph', [ + u('paragraph', [u('text', 'Paragraph text.')]), + u('heading', { depth: 1 }, [u('text', 'Heading text.')]), + u('code', 'someCode()'), + u('blockquote', [ + u('paragraph', [u('strong', [u('heading', [u('text', 'Quote text.')])])]), + ]), + u('list', [u('listItem', [u('text', 'A list item.')])]), + u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]), + u('thematicBreak'), + ]), + ]), + ]), + ]); + + const output = u('root', [ + u('paragraph', [u('text', 'Paragraph text.')]), + u('heading', { depth: 1 }, [u('text', 'Heading text.')]), + u('code', 'someCode()'), + u('blockquote', [u('heading', [u('text', 'Quote text.')])]), + u('list', [u('listItem', [u('text', 'A list item.')])]), + u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]), + u('thematicBreak'), + ]); + + expect(transform(input)).toEqual(output); + }); + + it('should remove blocks that are emptied as a result of denesting', () => { + const input = u('root', [ + u('paragraph', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]), + ]); + + const output = u('root', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]); + + expect(transform(input)).toEqual(output); + }); + + it('should remove blocks that are emptied as a result of denesting', () => { + const input = u('root', [ + u('paragraph', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]), + ]); + + const output = u('root', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]); + + expect(transform(input)).toEqual(output); + }); + + it('should handle asymmetrical splits', () => { + const input = u('root', [ + u('paragraph', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]), + ]); + + const output = u('root', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]); + + expect(transform(input)).toEqual(output); + }); + + it('should nest invalidly nested blocks in the nearest valid ancestor', () => { + const input = u('root', [ + u('paragraph', [ + u('blockquote', [u('strong', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])])]), + ]), + ]); + + const output = u('root', [ + u('blockquote', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]), + ]); + + expect(transform(input)).toEqual(output); + }); + + it('should preserve validly nested siblings of invalidly nested blocks', () => { + const input = u('root', [ + u('paragraph', [ + u('blockquote', [ + u('strong', [ + u('text', 'Deep validly nested text a.'), + u('heading', { depth: 1 }, [u('text', 'Heading text.')]), + u('text', 'Deep validly nested text b.'), + ]), + ]), + u('text', 'Validly nested text.'), + ]), + ]); + + const output = u('root', [ + u('blockquote', [ + u('strong', [u('text', 'Deep validly nested text a.')]), + u('heading', { depth: 1 }, [u('text', 'Heading text.')]), + u('strong', [u('text', 'Deep validly nested text b.')]), + ]), + u('paragraph', [u('text', 'Validly nested text.')]), + ]); + + expect(transform(input)).toEqual(output); + }); + + it('should allow intermediate parents like list and table to contain required block children', () => { + const input = u('root', [ + u('blockquote', [ + u('list', [ + u('listItem', [ + u('table', [ + u('tableRow', [ + u('tableCell', [ + u('heading', { depth: 1 }, [u('text', 'Validly nested heading text.')]), + ]), + ]), + ]), + ]), + ]), + ]), + ]); + + const output = u('root', [ + u('blockquote', [ + u('list', [ + u('listItem', [ + u('table', [ + u('tableRow', [ + u('tableCell', [ + u('heading', { depth: 1 }, [u('text', 'Validly nested heading text.')]), + ]), + ]), + ]), + ]), + ]), + ]), + ]); + + expect(transform(input)).toEqual(output); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkEscapeMarkdownEntities.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkEscapeMarkdownEntities.spec.js new file mode 100644 index 000000000000..37ff0a85d36f --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkEscapeMarkdownEntities.spec.js @@ -0,0 +1,84 @@ +import unified from 'unified'; +import u from 'unist-builder'; + +import remarkEscapeMarkdownEntities from '../remarkEscapeMarkdownEntities'; + +function process(text) { + const tree = u('root', [u('text', text)]); + const escapedMdast = unified().use(remarkEscapeMarkdownEntities).runSync(tree); + + return escapedMdast.children[0].value; +} + +describe('remarkEscapeMarkdownEntities', () => { + it('should escape common markdown entities', () => { + expect(process('*a*')).toEqual('\\*a\\*'); + expect(process('**a**')).toEqual('\\*\\*a\\*\\*'); + expect(process('***a***')).toEqual('\\*\\*\\*a\\*\\*\\*'); + expect(process('_a_')).toEqual('\\_a\\_'); + expect(process('__a__')).toEqual('\\_\\_a\\_\\_'); + expect(process('~~a~~')).toEqual('\\~\\~a\\~\\~'); + expect(process('[]')).toEqual('\\[]'); + expect(process('[]()')).toEqual('\\[]()'); + expect(process('[a](b)')).toEqual('\\[a](b)'); + expect(process('[Test sentence.](https://www.example.com)')).toEqual( + '\\[Test sentence.](https://www.example.com)', + ); + expect(process('![a](b)')).toEqual('!\\[a](b)'); + }); + + it('should not escape inactive, single markdown entities', () => { + expect(process('a*b')).toEqual('a*b'); + expect(process('_')).toEqual('_'); + expect(process('~')).toEqual('~'); + expect(process('[')).toEqual('['); + }); + + it('should escape leading markdown entities', () => { + expect(process('#')).toEqual('\\#'); + expect(process('-')).toEqual('\\-'); + expect(process('*')).toEqual('\\*'); + expect(process('>')).toEqual('\\>'); + expect(process('=')).toEqual('\\='); + expect(process('|')).toEqual('\\|'); + expect(process('```')).toEqual('\\`\\``'); + expect(process(' ')).toEqual('\\ '); + }); + + it('should escape leading markdown entities preceded by whitespace', () => { + expect(process('\n #')).toEqual('\\#'); + expect(process(' \n-')).toEqual('\\-'); + }); + + it('should not escape leading markdown entities preceded by non-whitespace characters', () => { + expect(process('a# # b #')).toEqual('a# # b #'); + expect(process('a- - b -')).toEqual('a- - b -'); + }); + + it('should not escape html tags', () => { + expect(process('')).toEqual(''); + expect(process('a b e')).toEqual('a b e'); + }); + + it('should escape the contents of html blocks', () => { + expect(process('
*a*
')).toEqual('
\\*a\\*
'); + }); + + it('should not escape the contents of preformatted html blocks', () => { + expect(process('
*a*
')).toEqual('
*a*
'); + expect(process('')).toEqual(''); + expect(process('')).toEqual(''); + expect(process('
\n*a*\n
')).toEqual('
\n*a*\n
'); + expect(process('a b
*c*
d e')).toEqual('a b
*c*
d e'); + }); + + it('should not escape footnote references', () => { + expect(process('[^a]')).toEqual('[^a]'); + expect(process('[^1]')).toEqual('[^1]'); + }); + + it('should not escape footnotes', () => { + expect(process('[^a]:')).toEqual('[^a]:'); + expect(process('[^1]:')).toEqual('[^1]:'); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPaddedLinks.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPaddedLinks.spec.js new file mode 100644 index 000000000000..0d5cf4abdf22 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPaddedLinks.spec.js @@ -0,0 +1,43 @@ +import unified from 'unified'; +import markdownToRemark from 'remark-parse'; +import remarkToMarkdown from 'remark-stringify'; + +import remarkPaddedLinks from '../remarkPaddedLinks'; + +function input(markdown) { + return unified() + .use(markdownToRemark) + .use(remarkPaddedLinks) + .use(remarkToMarkdown) + .processSync(markdown).contents; +} + +function output(markdown) { + return unified().use(markdownToRemark).use(remarkToMarkdown).processSync(markdown).contents; +} + +describe('remarkPaddedLinks', () => { + it('should move leading and trailing spaces outside of a link', () => { + expect(input('[ a ](b)')).toEqual(output(' [a](b) ')); + }); + + it('should convert multiple leading or trailing spaces to a single space', () => { + expect(input('[ a ](b)')).toEqual(output(' [a](b) ')); + }); + + it('should work with only a leading space or only a trailing space', () => { + expect(input('[ a](b)[c ](d)')).toEqual(output(' [a](b)[c](d) ')); + }); + + it('should work for nested links', () => { + expect(input('* # a[ b ](c)d')).toEqual(output('* # a [b](c) d')); + }); + + it('should work for parents with multiple links that are not siblings', () => { + expect(input('# a[ b ](c)d **[ e ](f)**')).toEqual(output('# a [b](c) d ** [e](f) **')); + }); + + it('should work for links with arbitrarily nested children', () => { + expect(input('[ a __*b*__ _c_ ](d)')).toEqual(output(' [a __*b*__ _c_](d) ')); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPlugins.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPlugins.spec.js new file mode 100644 index 000000000000..83ccf907e8d9 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPlugins.spec.js @@ -0,0 +1,299 @@ +import visit from 'unist-util-visit'; + +import { markdownToRemark, remarkToMarkdown } from '..'; + +describe('registered remark plugins', () => { + function withNetlifyLinks() { + return function transformer(tree) { + visit(tree, 'link', function onLink(node) { + node.url = 'https://netlify.com'; + }); + }; + } + + it('should use remark transformer plugins when converting mdast to markdown', () => { + const plugins = [withNetlifyLinks]; + const result = remarkToMarkdown( + { + type: 'root', + children: [ + { + type: 'paragraph', + children: [ + { + type: 'text', + value: 'Some ', + }, + { + type: 'emphasis', + children: [ + { + type: 'text', + value: 'important', + }, + ], + }, + { + type: 'text', + value: ' text with ', + }, + { + type: 'link', + title: null, + url: 'https://this-value-should-be-replaced.com', + children: [ + { + type: 'text', + value: 'a link', + }, + ], + }, + { + type: 'text', + value: ' in it.', + }, + ], + }, + ], + }, + plugins, + ); + expect(result).toMatchInlineSnapshot( + `"Some *important* text with [a link](https://netlify.com) in it."`, + ); + }); + + it('should use remark transformer plugins when converting markdown to mdast', () => { + const plugins = [withNetlifyLinks]; + const result = markdownToRemark( + 'Some text with [a link](https://this-value-should-be-replaced.com) in it.', + plugins, + ); + expect(result).toMatchInlineSnapshot(` +Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [], + "position": Position { + "end": Object { + "column": 16, + "line": 1, + "offset": 15, + }, + "indent": Array [], + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "text", + "value": "Some text with ", + }, + Object { + "children": Array [ + Object { + "children": Array [], + "position": Position { + "end": Object { + "column": 23, + "line": 1, + "offset": 22, + }, + "indent": Array [], + "start": Object { + "column": 17, + "line": 1, + "offset": 16, + }, + }, + "type": "text", + "value": "a link", + }, + ], + "position": Position { + "end": Object { + "column": 67, + "line": 1, + "offset": 66, + }, + "indent": Array [], + "start": Object { + "column": 16, + "line": 1, + "offset": 15, + }, + }, + "title": null, + "type": "link", + "url": "https://netlify.com", + }, + Object { + "children": Array [], + "position": Position { + "end": Object { + "column": 74, + "line": 1, + "offset": 73, + }, + "indent": Array [], + "start": Object { + "column": 67, + "line": 1, + "offset": 66, + }, + }, + "type": "text", + "value": " in it.", + }, + ], + "position": Position { + "end": Object { + "column": 74, + "line": 1, + "offset": 73, + }, + "indent": Array [], + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "paragraph", + }, + ], + "position": Object { + "end": Object { + "column": 74, + "line": 1, + "offset": 73, + }, + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "root", +} +`); + }); + + it('should use remark serializer plugins when converting mdast to markdown', () => { + function withEscapedLessThanChar() { + if (this.Compiler) { + this.Compiler.prototype.visitors.text = node => { + return node.value.replace(/ { + const settings = { + emphasis: '_', + bullet: '-', + }; + + const plugins = [{ settings }]; + const result = remarkToMarkdown( + { + type: 'root', + children: [ + { + type: 'paragraph', + children: [ + { + type: 'text', + value: 'Some ', + }, + { + type: 'emphasis', + children: [ + { + type: 'text', + value: 'important', + }, + ], + }, + { + type: 'text', + value: ' points:', + }, + ], + }, + { + type: 'list', + ordered: false, + start: null, + spread: false, + children: [ + { + type: 'listItem', + spread: false, + checked: null, + children: [ + { + type: 'paragraph', + children: [ + { + type: 'text', + value: 'One', + }, + ], + }, + ], + }, + { + type: 'listItem', + spread: false, + checked: null, + children: [ + { + type: 'paragraph', + children: [ + { + type: 'text', + value: 'Two', + }, + ], + }, + ], + }, + ], + }, + ], + }, + plugins, + ); + expect(result).toMatchInlineSnapshot(` +"Some _important_ points: + +- One +- Two" +`); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkShortcodes.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkShortcodes.spec.js new file mode 100644 index 000000000000..53d4ea042ab9 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkShortcodes.spec.js @@ -0,0 +1,106 @@ +import { Map, OrderedMap } from 'immutable'; + +import { remarkParseShortcodes, getLinesWithOffsets } from '../remarkShortcodes'; + +// Stub of Remark Parser +function process(value, plugins, processEat = () => {}) { + function eat() { + return processEat; + } + + function Parser() {} + Parser.prototype.blockTokenizers = {}; + Parser.prototype.blockMethods = []; + remarkParseShortcodes.call({ Parser }, { plugins }); + Parser.prototype.blockTokenizers.shortcode(eat, value); +} + +function EditorComponent({ id = 'foo', fromBlock = jest.fn(), pattern }) { + return { + id, + fromBlock, + pattern, + }; +} + +describe('remarkParseShortcodes', () => { + describe('pattern matching', () => { + it('should work', () => { + const editorComponent = EditorComponent({ pattern: /bar/ }); + process('foo bar', Map({ [editorComponent.id]: editorComponent })); + expect(editorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['bar'])); + }); + it('should match value surrounded in newlines', () => { + const editorComponent = EditorComponent({ pattern: /^bar$/ }); + process('foo\n\nbar\n', Map({ [editorComponent.id]: editorComponent })); + expect(editorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['bar'])); + }); + it('should match multiline shortcodes', () => { + const editorComponent = EditorComponent({ pattern: /^foo\nbar$/ }); + process('foo\nbar', Map({ [editorComponent.id]: editorComponent })); + expect(editorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['foo\nbar'])); + }); + it('should match multiline shortcodes with empty lines', () => { + const editorComponent = EditorComponent({ pattern: /^foo\n\nbar$/ }); + process('foo\n\nbar', Map({ [editorComponent.id]: editorComponent })); + expect(editorComponent.fromBlock).toHaveBeenCalledWith( + expect.arrayContaining(['foo\n\nbar']), + ); + }); + it('should match shortcodes based on order of occurrence in value', () => { + const fooEditorComponent = EditorComponent({ id: 'foo', pattern: /foo/ }); + const barEditorComponent = EditorComponent({ id: 'bar', pattern: /bar/ }); + process( + 'foo\n\nbar', + OrderedMap([ + [barEditorComponent.id, barEditorComponent], + [fooEditorComponent.id, fooEditorComponent], + ]), + ); + expect(fooEditorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['foo'])); + }); + it('should match shortcodes based on order of occurrence in value even when some use line anchors', () => { + const barEditorComponent = EditorComponent({ id: 'bar', pattern: /bar/ }); + const bazEditorComponent = EditorComponent({ id: 'baz', pattern: /^baz$/ }); + process( + 'foo\n\nbar\n\nbaz', + OrderedMap([ + [bazEditorComponent.id, bazEditorComponent], + [barEditorComponent.id, barEditorComponent], + ]), + ); + expect(barEditorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['bar'])); + }); + }); + describe('output', () => { + it('should be a remark shortcode node', () => { + const processEat = jest.fn(); + const shortcodeData = { bar: 'baz' }; + const expectedNode = { type: 'shortcode', data: { shortcode: 'foo', shortcodeData } }; + const editorComponent = EditorComponent({ pattern: /bar/, fromBlock: () => shortcodeData }); + process('foo bar', Map({ [editorComponent.id]: editorComponent }), processEat); + expect(processEat).toHaveBeenCalledWith(expectedNode); + }); + }); +}); + +describe('getLinesWithOffsets', () => { + test('should split into lines', () => { + const value = ' line1\n\nline2 \n\n line3 \n\n'; + + const lines = getLinesWithOffsets(value); + expect(lines).toEqual([ + { line: ' line1', start: 0 }, + { line: 'line2', start: 8 }, + { line: ' line3', start: 16 }, + { line: '', start: 30 }, + ]); + }); + + test('should return single item on no match', () => { + const value = ' line1 '; + + const lines = getLinesWithOffsets(value); + expect(lines).toEqual([{ line: ' line1', start: 0 }]); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkSlate.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkSlate.spec.js new file mode 100644 index 000000000000..d55f4416c7df --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkSlate.spec.js @@ -0,0 +1,67 @@ +import { mergeAdjacentTexts } from '../remarkSlate'; +describe('remarkSlate', () => { + describe('mergeAdjacentTexts', () => { + it('should handle empty array', () => { + const children = []; + expect(mergeAdjacentTexts(children)).toBe(children); + }); + + it('should merge adjacent texts with same marks', () => { + const children = [ + { text: '
', marks: [] }, + { text: 'Netlify', marks: [] }, + { text: '', marks: [] }, + ]; + + expect(mergeAdjacentTexts(children)).toEqual([ + { + text: 'Netlify', + marks: [], + }, + ]); + }); + + it('should not merge adjacent texts with different marks', () => { + const children = [ + { text: '', marks: [] }, + { text: 'Netlify', marks: ['b'] }, + { text: '', marks: [] }, + ]; + + expect(mergeAdjacentTexts(children)).toEqual(children); + }); + + it('should handle mixed children array', () => { + const children = [ + { object: 'inline' }, + { text: '', marks: [] }, + { text: 'Netlify', marks: [] }, + { text: '', marks: [] }, + { object: 'inline' }, + { text: '', marks: [] }, + { text: 'Netlify', marks: ['b'] }, + { text: '', marks: [] }, + { text: '', marks: [] }, + { object: 'inline' }, + { text: '', marks: [] }, + ]; + + expect(mergeAdjacentTexts(children)).toEqual([ + { object: 'inline' }, + { + text: 'Netlify', + marks: [], + }, + { object: 'inline' }, + { text: '', marks: [] }, + { text: 'Netlify', marks: ['b'] }, + { + text: '', + marks: [], + }, + { object: 'inline' }, + { text: '', marks: [] }, + ]); + }); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkStripTrailingBreaks.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkStripTrailingBreaks.spec.js new file mode 100644 index 000000000000..67ff12b9689a --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkStripTrailingBreaks.spec.js @@ -0,0 +1,23 @@ +import unified from 'unified'; +import u from 'unist-builder'; + +import remarkStripTrailingBreaks from '../remarkStripTrailingBreaks'; + +function process(children) { + const tree = u('root', children); + const strippedMdast = unified().use(remarkStripTrailingBreaks).runSync(tree); + + return strippedMdast.children; +} + +describe('remarkStripTrailingBreaks', () => { + it('should remove trailing breaks at the end of a block', () => { + expect(process([u('break')])).toEqual([]); + expect(process([u('break'), u('text', '\n \n')])).toEqual([u('text', '\n \n')]); + expect(process([u('text', 'a'), u('break')])).toEqual([u('text', 'a')]); + }); + + it('should not remove trailing breaks that are not at the end of a block', () => { + expect(process([u('break'), u('text', 'a')])).toEqual([u('break'), u('text', 'a')]); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/slate.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/slate.spec.js new file mode 100644 index 000000000000..e05cd76ba962 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/slate.spec.js @@ -0,0 +1,300 @@ +/** @jsx h */ + +import { flow } from 'lodash'; + +import h from '../../../test-helpers/h'; +import { markdownToSlate, slateToMarkdown } from '../index'; + +const process = flow([markdownToSlate, slateToMarkdown]); + +describe('slate', () => { + it('should not decode encoded html entities in inline code', () => { + expect(process('<div>')).toEqual( + '<div>', + ); + }); + + it('should parse non-text children of mark nodes', () => { + expect(process('**a[b](c)d**')).toEqual('**a[b](c)d**'); + expect(process('**[a](b)**')).toEqual('**[a](b)**'); + expect(process('**![a](b)**')).toEqual('**![a](b)**'); + expect(process('_`a`_')).toEqual('*`a`*'); + }); + + it('should handle unstyled code nodes adjacent to styled code nodes', () => { + expect(process('`foo`***`bar`***')).toEqual('`foo`***`bar`***'); + }); + + it('should handle styled code nodes adjacent to non-code text', () => { + expect(process('_`a`b_')).toEqual('*`a`b*'); + expect(process('_`a`**b**_')).toEqual('*`a`**b***'); + }); + + it('should condense adjacent, identically styled text and inline nodes', () => { + expect(process('**a ~~b~~~~c~~**')).toEqual('**a ~~bc~~**'); + expect(process('**a ~~b~~~~[c](d)~~**')).toEqual('**a ~~b[c](d)~~**'); + }); + + it('should handle nested markdown entities', () => { + expect(process('**a**b**c**')).toEqual('**a**b**c**'); + expect(process('**a _b_ c**')).toEqual('**a *b* c**'); + expect(process('*`a`*')).toEqual('*`a`*'); + }); + + it('should parse inline images as images', () => { + expect(process('a ![b](c)')).toEqual('a ![b](c)'); + }); + + it('should not escape markdown entities in html', () => { + expect(process('*')).toEqual('*'); + }); + + it('should wrap break tags in surrounding marks', () => { + expect(process('*a \nb*')).toEqual('*a\\\nb*'); + }); + + // slateAst no longer valid + + it('should not output empty headers in markdown', () => { + // prettier-ignore + const slateAst = ( + + + foo + + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"foo"`); + }); + + it('should not output empty marks in markdown', () => { + // prettier-ignore + const slateAst = ( + + + + foobar + baz + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"foobarbaz"`); + }); + + it('should not produce invalid markdown when a styled block has trailing whitespace', () => { + // prettier-ignore + const slateAst = ( + + + foo bar bim bam + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"**foo** bar **bim *bam***"`); + }); + + it('should not produce invalid markdown when a styled block has leading whitespace', () => { + // prettier-ignore + const slateAst = ( + + + foo bar + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"foo **bar**"`); + }); + + it('should group adjacent marks into a single mark when possible', () => { + // prettier-ignore + const slateAst = ( + + + shared mark + + link + + {' '} + not shared mark + + another + link + + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot( + `"**shared mark*[link](link)*** **not shared mark***[another **link**](link)*"`, + ); + }); + + describe('links', () => { + it('should handle inline code in link content', () => { + // prettier-ignore + const slateAst = ( + + + + foo + + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"[\`foo\`](link)"`); + }); + }); + + describe('code marks', () => { + it('can contain other marks', () => { + // prettier-ignore + const slateAst = ( + + + foo + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"***\`foo\`***"`); + }); + + it('can be condensed when no other marks are present', () => { + // prettier-ignore + const slateAst = ( + + + foo + bar + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"\`foo\`"`); + }); + }); + + describe('with nested styles within a single word', () => { + it('should not produce invalid markdown when a bold word has italics applied to a smaller part', () => { + // prettier-ignore + const slateAst = ( + + + h + e + y + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"**h*e*y**"`); + }); + + it('should not produce invalid markdown when an italic word has bold applied to a smaller part', () => { + // prettier-ignore + const slateAst = ( + + + h + e + y + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"*h**e**y*"`); + }); + + it('should handle italics inside bold inside strikethrough', () => { + // prettier-ignore + const slateAst = ( + + + h + e + l + l + o + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"~~h**e*l*l**o~~"`); + }); + + it('should handle bold inside italics inside strikethrough', () => { + // prettier-ignore + const slateAst = ( + + + h + e + l + l + o + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"~~h*e**l**l*o~~"`); + }); + + it('should handle strikethrough inside italics inside bold', () => { + // prettier-ignore + const slateAst = ( + + + h + e + l + l + o + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"**h*e~~l~~l*o**"`); + }); + + it('should handle italics inside strikethrough inside bold', () => { + // prettier-ignore + const slateAst = ( + + + h + e + l + l + o + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"**h~~e*l*l~~o**"`); + }); + + it('should handle strikethrough inside bold inside italics', () => { + // prettier-ignore + const slateAst = ( + + + h + e + l + l + o + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"*h**e~~l~~l**o*"`); + }); + + it('should handle bold inside strikethrough inside italics', () => { + // prettier-ignore + const slateAst = ( + + + h + e + l + l + o + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"*h~~e**l**l~~o*"`); + }); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/index.js b/packages/decap-cms-widget-richtext/src/serializers/index.js new file mode 100644 index 000000000000..c58d561b58b4 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/index.js @@ -0,0 +1,227 @@ +import { trimEnd } from 'lodash'; +import unified from 'unified'; +import u from 'unist-builder'; +import markdownToRemarkPlugin from 'remark-parse'; +import remarkToMarkdownPlugin from 'remark-stringify'; +import remarkToRehype from 'remark-rehype'; +import rehypeToHtml from 'rehype-stringify'; +import htmlToRehype from 'rehype-parse'; +import rehypeToRemark from 'rehype-remark'; +import { Map } from 'immutable'; + +import remarkToRehypeShortcodes from './remarkRehypeShortcodes'; +import rehypePaperEmoji from './rehypePaperEmoji'; +import remarkAssertParents from './remarkAssertParents'; +import remarkPaddedLinks from './remarkPaddedLinks'; +import remarkWrapHtml from './remarkWrapHtml'; +import remarkToSlate from './remarkSlate'; +import remarkSquashReferences from './remarkSquashReferences'; +import { remarkParseShortcodes, createRemarkShortcodeStringifier } from './remarkShortcodes'; +import remarkEscapeMarkdownEntities from './remarkEscapeMarkdownEntities'; +import remarkStripTrailingBreaks from './remarkStripTrailingBreaks'; +import remarkAllowHtmlEntities from './remarkAllowHtmlEntities'; +import slateToRemark from './slateRemark'; +// import { getEditorComponents } from '../MarkdownControl'; + +/** + * This module contains all serializers for the Markdown widget. + * + * The value of a Markdown widget is transformed to various formats during + * editing, and these formats are referenced throughout serializer source + * documentation. Below is brief glossary of the formats used. + * + * - Markdown {string} + * The stringified Markdown value. The value of the field is persisted + * (stored) in this format, and the stringified value is also used when the + * editor is in "raw" Markdown mode. + * + * - MDAST {object} + * Also loosely referred to as "Remark". MDAST stands for MarkDown AST + * (Abstract Syntax Tree), and is an object representation of a Markdown + * document. Underneath, it's a Unist tree with a Markdown-specific schema. + * MDAST syntax is a part of the Unified ecosystem, and powers the Remark + * processor, so Remark plugins may be used. + * + * - HAST {object} + * Also loosely referred to as "Rehype". HAST, similar to MDAST, is an object + * representation of an HTML document. The field value takes this format + * temporarily before the document is stringified to HTML. + * + * - HTML {string} + * The field value is stringified to HTML for preview purposes - the HTML value + * is never parsed, it is output only. + * + * - Slate Raw AST {object} + * Slate's Raw AST is a very simple and unopinionated object representation of + * a document in a Slate editor. We define our own Markdown-specific schema + * for serialization to/from Slate's Raw AST and MDAST. + */ + +/** + * Deserialize a Markdown string to an MDAST. + */ +export function markdownToRemark(markdown, remarkPlugins) { + const processor = unified() + .use(markdownToRemarkPlugin, { fences: true, commonmark: true }) + .use(markdownToRemarkRemoveTokenizers, { inlineTokenizers: ['url'] }) + .use(remarkParseShortcodes, { plugins: Map() }) + .use(remarkAllowHtmlEntities) + .use(remarkSquashReferences) + .use(remarkPlugins); + + /** + * Parse the Markdown string input to an MDAST. + */ + const parsed = processor.parse(markdown); + + /** + * Further transform the MDAST with plugins. + */ + const result = processor.runSync(parsed); + + return result; +} + +/** + * Remove named tokenizers from the parser, effectively deactivating them. + */ +function markdownToRemarkRemoveTokenizers({ inlineTokenizers }) { + inlineTokenizers && + inlineTokenizers.forEach(tokenizer => { + delete this.Parser.prototype.inlineTokenizers[tokenizer]; + }); +} + +/** + * Serialize an MDAST to a Markdown string. + */ +export function remarkToMarkdown(obj, remarkPlugins) { + /** + * Rewrite the remark-stringify text visitor to simply return the text value, + * without encoding or escaping any characters. This means we're completely + * trusting the markdown that we receive. + */ + function remarkAllowAllText() { + const Compiler = this.Compiler; + const visitors = Compiler.prototype.visitors; + visitors.text = node => node.value; + } + + /** + * Provide an empty MDAST if no value is provided. + */ + const mdast = obj || u('root', [u('p', [u('text', '')])]); + + const remarkToMarkdownPluginOpts = { + commonmark: true, + fences: true, + listItemIndent: '1', + + /** + * Use asterisk for everything, it's the most versatile. Eventually using + * other characters should be an option. + */ + bullet: '*', + emphasis: '*', + strong: '*', + rule: '-', + }; + + const processor = unified() + .use({ settings: remarkToMarkdownPluginOpts }) + .use(remarkEscapeMarkdownEntities) + .use(remarkStripTrailingBreaks) + .use(remarkToMarkdownPlugin) + .use(remarkAllowAllText) + .use(createRemarkShortcodeStringifier({ plugins: Map() })) + .use(remarkPlugins); + + /** + * Transform the MDAST with plugins. + */ + const processedMdast = processor.runSync(mdast); + + /** + * Serialize the MDAST to markdown. + */ + const markdown = processor.stringify(processedMdast).replace(/\r?/g, ''); + + /** + * Return markdown with trailing whitespace removed. + */ + return trimEnd(markdown); +} + +/** + * Convert Markdown to HTML. + */ +export function markdownToHtml(markdown, { getAsset, resolveWidget, remarkPlugins = [] } = {}) { + const mdast = markdownToRemark(markdown, remarkPlugins); + + const hast = unified() + .use(remarkToRehypeShortcodes, { plugins: Map(), getAsset, resolveWidget }) + .use(remarkToRehype, { allowDangerousHTML: true }) + .runSync(mdast); + + const html = unified() + .use(rehypeToHtml, { + allowDangerousHtml: true, + allowDangerousCharacters: true, + closeSelfClosing: true, + entities: { useNamedReferences: true }, + }) + .stringify(hast); + + return html; +} + +/** + * Deserialize an HTML string to Slate's Raw AST. Currently used for HTML + * pastes. + */ +export function htmlToSlate(html) { + const hast = unified().use(htmlToRehype, { fragment: true }).parse(html); + + const mdast = unified() + .use(rehypePaperEmoji) + .use(rehypeToRemark, { minify: false }) + .runSync(hast); + + const slateRaw = unified() + .use(remarkAssertParents) + .use(remarkPaddedLinks) + .use(remarkWrapHtml) + .use(remarkToSlate) + .runSync(mdast); + + return slateRaw; +} + +/** + * Convert Markdown to Slate's Raw AST. + */ +export function markdownToSlate(markdown, { voidCodeBlock, remarkPlugins = [] } = {}) { + const mdast = markdownToRemark(markdown, remarkPlugins); + + const slateRaw = unified() + .use(remarkWrapHtml) + .use(remarkToSlate, { voidCodeBlock }) + .runSync(mdast); + + return slateRaw.children; +} + +/** + * Convert a Slate Raw AST to Markdown. + * + * Requires shortcode plugins to parse shortcode nodes back to text. + * + * Note that Unified is not utilized for the conversion from Slate's Raw AST to + * MDAST. The conversion is manual because Unified can only operate on Unist + * trees. + */ +export function slateToMarkdown(raw, { voidCodeBlock, remarkPlugins = [] } = {}) { + const mdast = slateToRemark(raw, { voidCodeBlock }); + const markdown = remarkToMarkdown(mdast, remarkPlugins); + return markdown; +} diff --git a/packages/decap-cms-widget-richtext/src/serializers/regexHelper.js b/packages/decap-cms-widget-richtext/src/serializers/regexHelper.js new file mode 100644 index 000000000000..50dd1ec722fc --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/regexHelper.js @@ -0,0 +1,137 @@ +import { last } from 'lodash'; + +/** + * Joins an array of regular expressions into a single expression, without + * altering the received expressions. + */ +export function joinPatternSegments(patterns) { + return patterns.map(p => p.source).join(''); +} + +/** + * Combines an array of regular expressions into a single expression, wrapping + * each in a non-capturing group and interposing alternation characters (|) so + * that each expression is executed separately. + */ +export function combinePatterns(patterns) { + return patterns.map(p => `(?:${p.source})`).join('|'); +} + +/** + * Modify substrings within a string if they match a (global) pattern. Can be + * inverted to only modify non-matches. + * + * params: + * matchPattern - regexp - a regular expression to check for matches + * replaceFn - function - a replacement function that receives a matched + * substring and returns a replacement substring + * text - string - the string to process + * invertMatchPattern - boolean - if true, non-matching substrings are modified + * instead of matching substrings + */ +export function replaceWhen(matchPattern, replaceFn, text, invertMatchPattern) { + /** + * Splits the string into an array of objects with the following shape: + * + * { + * index: number - the index of the substring within the string + * text: string - the substring + * match: boolean - true if the substring matched `matchPattern` + * } + * + * Loops through matches via recursion (`RegExp.exec` tracks the loop + * internally). + */ + function split(exp, text, acc) { + /** + * Get the next match starting from the end of the last match or start of + * string. + */ + const match = exp.exec(text); + const lastEntry = last(acc); + + /** + * `match` will be null if there are no matches. + */ + if (!match) return acc; + + /** + * If the match is at the beginning of the input string, normalize to a data + * object with the `match` flag set to `true`, and add to the accumulator. + */ + if (match.index === 0) { + addSubstring(acc, 0, match[0], true); + } else if (!lastEntry) { + /** + * If there are no entries in the accumulator, convert the substring before + * the match to a data object (without the `match` flag set to true) and + * push to the accumulator, followed by a data object for the matching + * substring. + */ + addSubstring(acc, 0, match.input.slice(0, match.index)); + addSubstring(acc, match.index, match[0], true); + } else if (match.index === lastEntry.index + lastEntry.text.length) { + /** + * If the last entry in the accumulator immediately preceded the current + * matched substring in the original string, just add the data object for + * the matching substring to the accumulator. + */ + addSubstring(acc, match.index, match[0], true); + } else { + /** + * Convert the substring before the match to a data object (without the + * `match` flag set to true), followed by a data object for the matching + * substring. + */ + const nextIndex = lastEntry.index + lastEntry.text.length; + const nextText = match.input.slice(nextIndex, match.index); + addSubstring(acc, nextIndex, nextText); + addSubstring(acc, match.index, match[0], true); + } + + /** + * Continue executing the expression. + */ + return split(exp, text, acc); + } + + /** + * Factory for converting substrings to data objects and adding to an output + * array. + */ + function addSubstring(arr, index, text, match = false) { + arr.push({ index, text, match }); + } + + /** + * Split the input string to an array of data objects, each representing a + * matching or non-matching string. + */ + const acc = split(matchPattern, text, []); + + /** + * Process the trailing substring after the final match, if one exists. + */ + const lastEntry = last(acc); + if (!lastEntry) return replaceFn(text); + + const nextIndex = lastEntry.index + lastEntry.text.length; + if (text.length > nextIndex) { + acc.push({ index: nextIndex, text: text.slice(nextIndex) }); + } + + /** + * Map the data objects in the accumulator to their string values, modifying + * matched strings with the replacement function. Modifies non-matches if + * `invertMatchPattern` is truthy. + */ + const replacedText = acc.map(entry => { + const isMatch = invertMatchPattern ? !entry.match : entry.match; + return isMatch ? replaceFn(entry.text) : entry.text; + }); + + /** + * Return the joined string. + */ + return replacedText.join(''); +} diff --git a/packages/decap-cms-widget-richtext/src/serializers/rehypePaperEmoji.js b/packages/decap-cms-widget-richtext/src/serializers/rehypePaperEmoji.js new file mode 100644 index 000000000000..cda7bab99dff --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/rehypePaperEmoji.js @@ -0,0 +1,16 @@ +/** + * Dropbox Paper outputs emoji characters as images, and stores the actual + * emoji character in a `data-emoji-ch` attribute on the image. This plugin + * replaces the images with the emoji characters. + */ +export default function rehypePaperEmoji() { + function transform(node) { + if (node.tagName === 'img' && node.properties.dataEmojiCh) { + return { type: 'text', value: node.properties.dataEmojiCh }; + } + node.children = node.children ? node.children.map(transform) : node.children; + return node; + } + + return transform; +} diff --git a/packages/decap-cms-widget-richtext/src/serializers/remarkAllowHtmlEntities.js b/packages/decap-cms-widget-richtext/src/serializers/remarkAllowHtmlEntities.js new file mode 100644 index 000000000000..8e48a0f940ee --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/remarkAllowHtmlEntities.js @@ -0,0 +1,58 @@ +export default function remarkAllowHtmlEntities() { + this.Parser.prototype.inlineTokenizers.text = text; + + /** + * This is a port of the `remark-parse` text tokenizer, adapted to exclude + * HTML entity decoding. + */ + function text(eat, value, silent) { + var self = this; + var methods; + var tokenizers; + var index; + var length; + var subvalue; + var position; + var tokenizer; + var name; + var min; + + /* istanbul ignore if - never used (yet) */ + if (silent) { + return true; + } + + methods = self.inlineMethods; + length = methods.length; + tokenizers = self.inlineTokenizers; + index = -1; + min = value.length; + + while (++index < length) { + name = methods[index]; + + if (name === 'text' || !tokenizers[name]) { + continue; + } + + tokenizer = tokenizers[name].locator; + + if (!tokenizer) { + eat.file.fail('Missing locator: `' + name + '`'); + } + + position = tokenizer.call(self, value, 1); + + if (position !== -1 && position < min) { + min = position; + } + } + + subvalue = value.slice(0, min); + + eat(subvalue)({ + type: 'text', + value: subvalue, + }); + } +} diff --git a/packages/decap-cms-widget-richtext/src/serializers/remarkAssertParents.js b/packages/decap-cms-widget-richtext/src/serializers/remarkAssertParents.js new file mode 100644 index 000000000000..431dd0a55d2d --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/remarkAssertParents.js @@ -0,0 +1,83 @@ +import { concat, last, nth, isEmpty } from 'lodash'; +import visitParents from 'unist-util-visit-parents'; + +/** + * remarkUnwrapInvalidNest + * + * Some MDAST node types can only be nested within specific node types - for + * example, a paragraph can't be nested within another paragraph, and a heading + * can't be nested in a "strong" type node. This kind of invalid MDAST can be + * generated by rehype-remark from invalid HTML. + * + * This plugin finds instances of invalid nesting, and unwraps the invalidly + * nested nodes as far up the parental line as necessary, splitting parent nodes + * along the way. The resulting node has no invalidly nested nodes, and all + * validly nested nodes retain their ancestry. Nodes that are emptied as a + * result of unnesting nodes are removed from the tree. + */ +export default function remarkUnwrapInvalidNest() { + return transform; + + function transform(tree) { + const invalidNest = findInvalidNest(tree); + + if (!invalidNest) return tree; + + splitTreeAtNest(tree, invalidNest); + + return transform(tree); + } + + /** + * visitParents uses unist-util-visit-parent to check every node in the + * tree while having access to every ancestor of the node. This is ideal + * for determining whether a block node has an ancestor that should not + * contain a block node. Note that it operates in a mutable fashion. + */ + function findInvalidNest(tree) { + /** + * Node types that are considered "blocks". + */ + const blocks = ['paragraph', 'heading', 'code', 'blockquote', 'list', 'table', 'thematicBreak']; + + /** + * Node types that can contain "block" nodes as direct children. We check + */ + const canContainBlocks = ['root', 'blockquote', 'listItem', 'tableCell']; + + let invalidNest; + + visitParents(tree, (node, parents) => { + const parentType = !isEmpty(parents) && last(parents).type; + const isInvalidNest = blocks.includes(node.type) && !canContainBlocks.includes(parentType); + + if (isInvalidNest) { + invalidNest = concat(parents, node); + return false; + } + }); + + return invalidNest; + } + + function splitTreeAtNest(tree, nest) { + const grandparent = nth(nest, -3) || tree; + const parent = nth(nest, -2); + const node = last(nest); + + const splitIndex = grandparent.children.indexOf(parent); + const splitChildren = grandparent.children; + const splitChildIndex = parent.children.indexOf(node); + + const childrenBefore = parent.children.slice(0, splitChildIndex); + const childrenAfter = parent.children.slice(splitChildIndex + 1); + const nodeBefore = !isEmpty(childrenBefore) && { ...parent, children: childrenBefore }; + const nodeAfter = !isEmpty(childrenAfter) && { ...parent, children: childrenAfter }; + + const childrenToInsert = [nodeBefore, node, nodeAfter].filter(val => !isEmpty(val)); + const beforeChildren = splitChildren.slice(0, splitIndex); + const afterChildren = splitChildren.slice(splitIndex + 1); + const newChildren = concat(beforeChildren, childrenToInsert, afterChildren); + grandparent.children = newChildren; + } +} diff --git a/packages/decap-cms-widget-richtext/src/serializers/remarkEscapeMarkdownEntities.js b/packages/decap-cms-widget-richtext/src/serializers/remarkEscapeMarkdownEntities.js new file mode 100644 index 000000000000..2a602ba58581 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/remarkEscapeMarkdownEntities.js @@ -0,0 +1,269 @@ +import { has, flow, partial, map } from 'lodash'; + +import { joinPatternSegments, combinePatterns, replaceWhen } from './regexHelper'; + +/** + * Reusable regular expressions segments. + */ +const patternSegments = { + /** + * Matches zero or more HTML attributes followed by the tag close bracket, + * which may be prepended by zero or more spaces. The attributes can use + * single or double quotes and may be prepended by zero or more spaces. + */ + htmlOpeningTagEnd: /(?: *\w+=(?:(?:"[^"]*")|(?:'[^']*')))* *>/, +}; + +/** + * Patterns matching substrings that should not be escaped. Array values must be + * joined before use. + */ +const nonEscapePatterns = { + /** + * HTML Tags + * + * Matches HTML opening tags and any attributes. Does not check for contents + * between tags or closing tags. + */ + htmlTags: [ + /** + * Matches the beginning of an HTML tag, excluding preformatted tag types. + */ + /<(?!pre|style|script)[\w]+/, + + /** + * Matches attributes. + */ + patternSegments.htmlOpeningTagEnd, + ], + + /** + * Preformatted HTML Blocks + * + * Matches HTML blocks with preformatted content. The content of these blocks, + * including the tags and attributes, should not be escaped at all. + */ + preformattedHtmlBlocks: [ + /** + * Matches the names of tags known to have preformatted content. The capture + * group is reused when matching the closing tag. + * + * NOTE: this pattern reuses a capture group, and could break if combined with + * other expressions using capture groups. + */ + /<(pre|style|script)/, + + /** + * Matches attributes. + */ + patternSegments.htmlOpeningTagEnd, + + /** + * Allow zero or more of any character (including line breaks) between the + * tags. Match lazily in case of subsequent blocks. + */ + /(.|[\n\r])*?/, + + /** + * Match closing tag via first capture group. + */ + /<\/\1>/, + ], +}; + +/** + * Escape patterns + * + * Each escape pattern matches a markdown entity and captures up to two + * groups. These patterns must use one of the following formulas: + * + * - Single capture group followed by match content - /(...).../ + * The captured characters should be escaped and the remaining match should + * remain unchanged. + * + * - Two capture groups surrounding matched content - /(...)...(...)/ + * The captured characters in both groups should be escaped and the matched + * characters in between should remain unchanged. + */ +const escapePatterns = [ + /** + * Emphasis/Bold - Asterisk + * + * Match strings surrounded by one or more asterisks on both sides. + */ + /(\*+)[^*]*(\1)/g, + + /** + * Emphasis - Underscore + * + * Match strings surrounded by a single underscore on both sides followed by + * a word boundary. Remark disregards whether a word boundary exists at the + * beginning of an emphasis node. + */ + /(_)[^_]+(_)\b/g, + + /** + * Bold - Underscore + * + * Match strings surrounded by multiple underscores on both sides. Remark + * disregards the absence of word boundaries on either side of a bold node. + */ + /(_{2,})[^_]*(\1)/g, + + /** + * Strikethrough + * + * Match strings surrounded by multiple tildes on both sides. + */ + /(~+)[^~]*(\1)/g, + + /** + * Inline Code + * + * Match strings surrounded by backticks. + */ + /(`+)[^`]*(\1)/g, + + /** + * Links and Images + * + * Match strings surrounded by square brackets, except when the opening + * bracket is followed by a caret. This could be improved to specifically + * match only the exact syntax of each covered entity, but doing so through + * current approach would incur a considerable performance penalty. + */ + /(\[(?!\^)+)[^\]]*]/g, +]; + +/** + * Generate new non-escape expression. The non-escape expression matches + * substrings whose contents should not be processed for escaping. + */ +const joinedNonEscapePatterns = map(nonEscapePatterns, pattern => { + return new RegExp(joinPatternSegments(pattern)); +}); +const nonEscapePattern = combinePatterns(joinedNonEscapePatterns); + +/** + * Create chain of successive escape functions for various markdown entities. + */ +const escapeFunctions = escapePatterns.map(pattern => partial(escapeDelimiters, pattern)); +const escapeAll = flow(escapeFunctions); + +/** + * Executes both the `escapeCommonChars` and `escapeLeadingChars` functions. + */ +function escapeAllChars(text) { + const partiallyEscapedMarkdown = escapeCommonChars(text); + return escapeLeadingChars(partiallyEscapedMarkdown); +} + +/** + * escapeLeadingChars + * + * Handles escaping for characters that must be positioned at the beginning of + * the string, such as headers and list items. + * + * Escapes '#', '*', '-', '>', '=', '|', and sequences of 3+ backticks or 4+ + * spaces when found at the beginning of a string, preceded by zero or more + * whitespace characters. + */ +function escapeLeadingChars(text) { + return text.replace(/^\s*([-#*>=|]| {4,}|`{3,})/, '$`\\$1'); +} + +/** + * escapeCommonChars + * + * Escapes active markdown entities. See escape pattern groups for details on + * which entities are replaced. + */ +function escapeCommonChars(text) { + /** + * Generate new non-escape expression (must happen at execution time because + * we use `RegExp.exec`, which tracks it's own state internally). + */ + const nonEscapeExpression = new RegExp(nonEscapePattern, 'gm'); + + /** + * Use `replaceWhen` to escape markdown entities only within substrings that + * are eligible for escaping. + */ + return replaceWhen(nonEscapeExpression, escapeAll, text, true); +} + +/** + * escapeDelimiters + * + * Executes `String.replace` for a given pattern, but only on the first two + * capture groups. Specifically intended for escaping opening (and optionally + * closing) markdown entities without escaping the content in between. + */ +function escapeDelimiters(pattern, text) { + return text.replace(pattern, (match, start, end) => { + const hasEnd = typeof end === 'string'; + const matchSliceEnd = hasEnd ? match.length - end.length : match.length; + const content = match.slice(start.length, matchSliceEnd); + return `${escape(start)}${content}${hasEnd ? escape(end) : ''}`; + }); +} + +/** + * escape + * + * Simple replacement function for escaping markdown entities. Prepends every + * character in the received string with a backslash. + */ +function escape(delim) { + let result = ''; + for (const char of delim) { + result += `\\${char}`; + } + return result; +} + +/** + * A Remark plugin for escaping markdown entities. + * + * When markdown entities are entered in raw markdown, they don't appear as + * characters in the resulting AST; for example, dashes surrounding a piece of + * text cause the text to be inserted in a special node type, but the asterisks + * themselves aren't present as text. Therefore, we generally don't expect to + * encounter markdown characters in text nodes. + * + * However, the CMS visual editor does not interpret markdown characters, and + * users will expect these characters to be represented literally. In that case, + * we need to escape them, otherwise they'll be interpreted during + * stringification. + */ +export default function remarkEscapeMarkdownEntities() { + function transform(node, index) { + /** + * Shortcode nodes will intentionally inject markdown entities in text node + * children not be escaped. + */ + if (has(node.data, 'shortcode')) return node; + + const children = node.children ? { children: node.children.map(transform) } : {}; + + /** + * Escape characters in text and html nodes only. We store a lot of normal + * text in html nodes to keep Remark from escaping html entities. + */ + if (['text', 'html'].includes(node.type) && node.value) { + /** + * Escape all characters if this is the first child node, otherwise only + * common characters. + */ + const value = index === 0 ? escapeAllChars(node.value) : escapeCommonChars(node.value); + return { ...node, value, ...children }; + } + + /** + * Always return nodes with recursively mapped children. + */ + return { ...node, ...children }; + } + + return transform; +} diff --git a/packages/decap-cms-widget-richtext/src/serializers/remarkImagesToText.js b/packages/decap-cms-widget-richtext/src/serializers/remarkImagesToText.js new file mode 100644 index 000000000000..6f305a1266b3 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/remarkImagesToText.js @@ -0,0 +1,26 @@ +/** + * Images must be parsed as shortcodes for asset proxying. This plugin converts + * MDAST image nodes back to text to allow shortcode pattern matching. Note that + * this transformation only occurs for images that are the sole child of a top + * level paragraph - any other image is left alone and treated as an inline + * image. + */ +export default function remarkImagesToText() { + return transform; + + function transform(node) { + const children = node.children.map(child => { + if ( + child.type === 'paragraph' && + child.children.length === 1 && + child.children[0].type === 'image' + ) { + const { alt, url, title } = child.children[0]; + const value = `![${alt || ''}](${url || ''}${title ? ` "${title}"` : ''})`; + child.children = [{ type: 'text', value }]; + } + return child; + }); + return { ...node, children }; + } +} diff --git a/packages/decap-cms-widget-richtext/src/serializers/remarkPaddedLinks.js b/packages/decap-cms-widget-richtext/src/serializers/remarkPaddedLinks.js new file mode 100644 index 000000000000..96ac4a5da18b --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/remarkPaddedLinks.js @@ -0,0 +1,120 @@ +import { find, findLast, startsWith, endsWith, trimStart, trimEnd, flatMap } from 'lodash'; +import u from 'unist-builder'; +import toString from 'mdast-util-to-string'; + +/** + * Convert leading and trailing spaces in a link to single spaces outside of the + * link. MDASTs derived from pasted Google Docs HTML require this treatment. + * + * Note that, because we're potentially replacing characters in a link node's + * children with character's in a link node's siblings, we have to operate on a + * parent (link) node and its children at once, rather than just processing + * children one at a time. + */ +export default function remarkPaddedLinks() { + function transform(node) { + /** + * Because we're operating on link nodes and their children at once, we can + * exit if the current node has no children. + */ + if (!node.children) return node; + + /** + * Process a node's children if any of them are links. If a node is a link + * with leading or trailing spaces, we'll get back an array of nodes instead + * of a single node, so we use `flatMap` to keep those nodes as siblings + * with the other children. + * + * If performance improvements are found desirable, we could change this to + * only pass in the link nodes instead of the entire array of children, but + * this seems unlikely to produce a noticeable perf gain. + */ + const hasLinkChild = node.children.some(child => child.type === 'link'); + const processedChildren = hasLinkChild + ? flatMap(node.children, transformChildren) + : node.children; + + /** + * Run all children through the transform recursively. + */ + const children = processedChildren.map(transform); + + return { ...node, children }; + } + + function transformChildren(node) { + if (node.type !== 'link') return node; + + /** + * Get the node's complete string value, check for leading and trailing + * whitespace, and get nodes from each edge where whitespace is found. + */ + const text = toString(node); + const leadingWhitespaceNode = startsWith(text, ' ') && getEdgeTextChild(node); + const trailingWhitespaceNode = endsWith(text, ' ') && getEdgeTextChild(node, true); + + if (!leadingWhitespaceNode && !trailingWhitespaceNode) return node; + + /** + * Trim the edge nodes in place. Unified handles everything in a mutable + * fashion, so it's often simpler to do the same when working with Unified + * ASTs. + */ + if (leadingWhitespaceNode) { + leadingWhitespaceNode.value = trimStart(leadingWhitespaceNode.value); + } + + if (trailingWhitespaceNode) { + trailingWhitespaceNode.value = trimEnd(trailingWhitespaceNode.value); + } + + /** + * Create an array of nodes. The first and last child will either be `false` + * or a text node. We filter out the false values before returning. + */ + const nodes = [ + leadingWhitespaceNode && u('text', ' '), + node, + trailingWhitespaceNode && u('text', ' '), + ]; + + return nodes.filter(val => val); + } + + /** + * Get the first or last non-blank text child of a node, regardless of + * nesting. If `end` is truthy, get the last node, otherwise first. + */ + function getEdgeTextChild(node, end) { + /** + * This was changed from a ternary to a long form if due to issues with istanbul's instrumentation and babel's code + * generation. + * TODO: watch https://github.com/istanbuljs/babel-plugin-istanbul/issues/95 + * when it is resolved then revert to ```const findFn = end ? findLast : find;``` + */ + let findFn; + if (end) { + findFn = findLast; + } else { + findFn = find; + } + + let edgeChildWithValue; + setEdgeChildWithValue(node); + return edgeChildWithValue; + + /** + * searchChildren checks a node and all of it's children deeply to find a + * non-blank text value. When the text node is found, we set it in an outside + * variable, as it may be deep in the tree and therefore wouldn't be returned + * by `find`/`findLast`. + */ + function setEdgeChildWithValue(child) { + if (!edgeChildWithValue && child.value) { + edgeChildWithValue = child; + } + findFn(child.children, setEdgeChildWithValue); + } + } + return transform; +} diff --git a/packages/decap-cms-widget-richtext/src/serializers/remarkRehypeShortcodes.js b/packages/decap-cms-widget-richtext/src/serializers/remarkRehypeShortcodes.js new file mode 100644 index 000000000000..67d612aaeec8 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/remarkRehypeShortcodes.js @@ -0,0 +1,67 @@ +import React from 'react'; +import { map, has } from 'lodash'; +import { renderToString } from 'react-dom/server'; +import u from 'unist-builder'; + +/** + * This plugin doesn't actually transform Remark (MDAST) nodes to Rehype + * (HAST) nodes, but rather, it prepares an MDAST shortcode node for HAST + * conversion by replacing the shortcode text with stringified HTML for + * previewing the shortcode output. + */ +export default function remarkToRehypeShortcodes({ plugins, getAsset, resolveWidget }) { + return transform; + + function transform(root) { + const transformedChildren = map(root.children, processShortcodes); + return { ...root, children: transformedChildren }; + } + + /** + * Mapping function to transform nodes that contain shortcodes. + */ + function processShortcodes(node) { + /** + * If the node doesn't contain shortcode data, return the original node. + */ + if (!has(node, ['data', 'shortcode'])) return node; + + /** + * Get shortcode data from the node, and retrieve the matching plugin by + * key. + */ + const { shortcode, shortcodeData } = node.data; + const plugin = plugins.get(shortcode); + + /** + * Run the shortcode plugin's `toPreview` method, which will return either + * an HTML string or a React component. If a React component is returned, + * render it to an HTML string. + */ + const value = getPreview(plugin, shortcodeData); + const valueHtml = typeof value === 'string' ? value : renderToString(value); + + /** + * Return a new 'html' type node containing the shortcode preview markup. + */ + const textNode = u('html', valueHtml); + const children = [textNode]; + return { ...node, children }; + } + + /** + * Retrieve the shortcode preview component. + */ + function getPreview(plugin, shortcodeData) { + const { toPreview, widget, fields } = plugin; + if (toPreview) { + return toPreview(shortcodeData, getAsset, fields); + } + const preview = resolveWidget(widget); + return React.createElement(preview.preview, { + value: shortcodeData, + field: plugin, + getAsset, + }); + } +} diff --git a/packages/decap-cms-widget-richtext/src/serializers/remarkShortcodes.js b/packages/decap-cms-widget-richtext/src/serializers/remarkShortcodes.js new file mode 100644 index 000000000000..6a31ace3393b --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/remarkShortcodes.js @@ -0,0 +1,106 @@ +export function remarkParseShortcodes({ plugins }) { + const Parser = this.Parser; + const tokenizers = Parser.prototype.blockTokenizers; + const methods = Parser.prototype.blockMethods; + + tokenizers.shortcode = createShortcodeTokenizer({ plugins }); + + methods.unshift('shortcode'); +} + +export function getLinesWithOffsets(value) { + const SEPARATOR = '\n\n'; + const splitted = value.split(SEPARATOR); + const trimmedLines = splitted + .reduce( + (acc, line) => { + const { start: previousLineStart, originalLength: previousLineOriginalLength } = + acc[acc.length - 1]; + + return [ + ...acc, + { + line: line.trimEnd(), + start: previousLineStart + previousLineOriginalLength + SEPARATOR.length, + originalLength: line.length, + }, + ]; + }, + [{ start: -SEPARATOR.length, originalLength: 0 }], + ) + .slice(1) + .map(({ line, start }) => ({ line, start })); + return trimmedLines; +} + +function matchFromLines({ trimmedLines, plugin }) { + for (const { line, start } of trimmedLines) { + const match = line.match(plugin.pattern); + if (match) { + match.index += start; + return match; + } + } +} + +function createShortcodeTokenizer({ plugins }) { + return function tokenizeShortcode(eat, value, silent) { + // Plugin patterns may rely on `^` and `$` tokens, even if they don't + // use the multiline flag. To support this, we fall back to searching + // through each line individually, trimming trailing whitespace and + // newlines, if we don't initially match on a pattern. We keep track of + // the starting position of each line so that we can sort correctly + // across the full multiline matches. + const trimmedLines = getLinesWithOffsets(value); + + // Attempt to find a regex match for each plugin's pattern, and then + // select the first by its occurrence in `value`. This ensures we won't + // skip a plugin that occurs later in the plugin registry, but earlier + // in the `value`. + const [{ plugin, match } = {}] = plugins + .toArray() + .map(plugin => ({ + match: value.match(plugin.pattern) || matchFromLines({ trimmedLines, plugin }), + plugin, + })) + .filter(({ match }) => !!match) + .sort((a, b) => a.match.index - b.match.index); + + if (match) { + if (silent) { + return true; + } + + const shortcodeData = plugin.fromBlock(match); + + try { + return eat(match[0])({ + type: 'shortcode', + data: { shortcode: plugin.id, shortcodeData }, + }); + } catch (e) { + console.warn( + `Sent invalid data to remark. Plugin: ${plugin.id}. Value: ${ + match[0] + }. Data: ${JSON.stringify(shortcodeData)}`, + ); + return false; + } + } + }; +} + +export function createRemarkShortcodeStringifier({ plugins }) { + return function remarkStringifyShortcodes() { + const Compiler = this.Compiler; + const visitors = Compiler.prototype.visitors; + + visitors.shortcode = shortcode; + + function shortcode(node) { + const { data } = node; + const plugin = plugins.find(plugin => data.shortcode === plugin.id); + return plugin.toBlock(data.shortcodeData); + } + }; +} diff --git a/packages/decap-cms-widget-richtext/src/serializers/remarkSlate.js b/packages/decap-cms-widget-richtext/src/serializers/remarkSlate.js new file mode 100644 index 000000000000..fcc05c5c2f79 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/remarkSlate.js @@ -0,0 +1,424 @@ +import { isEmpty, isArray, flatMap, map, flatten, isEqual } from 'lodash'; + +/** + * Map of MDAST node types to Slate node types. + */ +const typeMap = { + root: 'root', + paragraph: 'p', + blockquote: 'quote', + code: 'code-block', + listItem: 'li', + table: 'table', + tableRow: 'table-row', + tableCell: 'table-cell', + thematicBreak: 'thematic-break', + link: 'link', + image: 'image', + shortcode: 'shortcode', +}; + +/** + * Map of MDAST node types to Slate mark types. + */ +const markMap = { + strong: 'bold', + emphasis: 'italic', + delete: 'delete', + inlineCode: 'code', +}; + +function isText(node) { + return !!node.text; +} + +function isMarksEqual(node1, node2) { + return isEqual(node1.marks, node2.marks); +} + +export function mergeAdjacentTexts(children) { + if (children.length <= 0) { + return children; + } + + const mergedChildren = []; + + let isMerging = false; + let current; + + for (let i = 0; i < children.length - 1; i++) { + if (!isMerging) { + current = children[i]; + } + const next = children[i + 1]; + if (isText(current) && isText(next) && isMarksEqual(current, next)) { + isMerging = true; + current = { ...current, text: `${current.text}${next.text}` }; + } else { + mergedChildren.push(current); + isMerging = false; + } + } + + if (isMerging) { + mergedChildren.push(current); + } else { + mergedChildren.push(children[children.length - 1]); + } + + return mergedChildren; +} + +/** + * A Remark plugin for converting an MDAST to Slate Raw AST. Remark plugins + * return a `transformNode` function that receives the MDAST as it's first argument. + */ +export default function remarkToSlate({ voidCodeBlock } = {}) { + return transformNode; + + function transformNode(node) { + /** + * Call `transformNode` recursively on child nodes. + * + * If a node returns a falsey value, filter it out. Some nodes do not + * translate from MDAST to Slate, such as definitions for link/image + * references or footnotes. + */ + let children = + !['strong', 'emphasis', 'delete'].includes(node.type) && + !isEmpty(node.children) && + flatMap(node.children, transformNode).filter(val => val); + + if (Array.isArray(children)) { + // Merge adjacent text nodes with the same marks to conform to slate schema + children = mergeAdjacentTexts(children); + } + + /** + * Run individual nodes through the conversion factory. + */ + const output = convertNode(node, children || undefined); + return output; + } + + /** + * Add nodes to a parent node only if `nodes` is truthy. + */ + function addNodes(parent, nodes) { + return nodes ? { ...parent, children: nodes } : parent; + } + + /** + * Create a Slate Block node. + */ + function createBlock(type, nodes, props = {}) { + if (!isArray(nodes)) { + props = nodes; + nodes = undefined; + } + + // Ensure block nodes have at least one text child to conform to slate schema + const children = isEmpty(nodes) ? [createText('')] : nodes; + const node = { type, ...props }; + return addNodes(node, children); + } + + /** + * Create a Slate Inline node. + */ + function createInline(type, props = {}, nodes) { + const node = { type, ...props }; + + // Ensure inline nodes have at least one text child to conform to slate schema + const children = isEmpty(nodes) ? [createText('')] : nodes; + return addNodes(node, children); + } + + /** + * Create a Slate Raw text node. + */ + function createText(node) { + const newNode = {}; + if (typeof node === 'string') { + return { ...newNode, text: node }; + } + const { text, marks } = node; + return normalizeMarks({ ...newNode, text, marks }); + } + + function processMarkChild(childNode, marks) { + switch (childNode.type) { + /** + * If a text node is a direct child of the current node, it should be + * set aside as a text, and all marks that have been collected in the + * `marks` array should apply to that specific text. + */ + case 'html': + case 'text': + return { ...convertNode(childNode), marks }; + + /** + * MDAST inline code nodes don't have children, just a text value, similar + * to a text node, so it receives the same treatment as a text node, but we + * first add the inline code mark to the marks array. + */ + case 'inlineCode': { + return { ...convertNode(childNode), marks: [...marks, { type: 'code' }] }; + } + + /** + * Process nested style nodes. The recursive results should be pushed into + * the texts array. This way, every MDAST nested text structure becomes a + * flat array of texts that can serve as the value of a single Slate Raw + * text node. + */ + case 'strong': + case 'emphasis': + case 'delete': + return processMarkNode(childNode, marks); + + case 'link': { + const nodes = map(childNode.children, child => + normalizeMarks(processMarkChild(child, marks)), + ); + const result = convertNode(childNode, flatten(nodes)); + return result; + } + + /** + * Remaining nodes simply need mark data added to them, and to then be + * added into the cumulative children array. + */ + default: + return transformNode({ ...childNode, data: { ...childNode.data, marks } }); + } + } + + function processMarkNode(node, parentMarks = []) { + /** + * Add the current node's mark type to the marks collected from parent + * mark nodes, if any. + */ + const markType = markMap[node.type]; + const marks = markType + ? [...parentMarks.filter(({ type }) => type !== markType), { type: markType }] + : parentMarks; + + const children = flatMap(node.children, child => + normalizeMarks(processMarkChild(child, marks)), + ); + + return children; + } + + function normalizeMarks(node) { + if (node.marks) { + node.marks.forEach(mark => { + node[mark.type] = true; + }); + } + + return node; + } + + /** + * Convert a single MDAST node to a Slate Raw node. Uses local node factories + * that mimic the unist-builder function utilized in the slateRemark + * transformer. + */ + function convertNode(node, nodes) { + switch (node.type) { + /** + * General + * + * Convert simple cases that only require a type and children, with no + * additional properties. + */ + case 'root': + case 'paragraph': + case 'blockquote': + case 'tableRow': + case 'tableCell': { + return createBlock(typeMap[node.type], nodes); + } + + /** + * List Items + * + * Markdown list items can be empty, but a list item in the Slate schema + * should at least have an empty paragraph node. + */ + case 'listItem': { + const children = isEmpty(nodes) ? [createBlock('paragraph')] : nodes; + return createBlock(typeMap[node.type], children); + } + + /** + * Shortcodes + * + * Shortcode nodes are represented as "void" blocks in the Slate AST. They + * maintain the same data as MDAST shortcode nodes. Slate void blocks must + * contain a blank text node. + */ + case 'shortcode': { + const nodes = [createText('')]; + const data = { ...node.data, id: node.data.shortcode, shortcodeNew: true }; + return createBlock(typeMap[node.type], nodes, { data }); + } + + case 'text': { + const text = node.value; + return createText(text); + } + + /** + * HTML + * + * HTML nodes contain plain text like text nodes, except they only contain + * HTML. Our serialization results in non-HTML being placed in HTML nodes + * sometimes to ensure that we're never escaping HTML from the rich text + * editor. We do not replace line feeds in HTML because the HTML is raw + * in the rich text editor, so the writer knows they're writing HTML, and + * should expect soft breaks to be visually absent in the rendered HTML. + */ + case 'html': { + return createText(node.value); + } + + /** + * Inline Code + * + * Inline code nodes from an MDAST are represented in our Slate schema as + * text nodes with a "code" mark. We manually create the text containing + * the inline code value and a "code" mark, and place it in an array for use + * as a Slate text node's children array. + */ + case 'inlineCode': { + return createText({ text: node.value, code: true, marks: [{ type: 'code' }] }); + } + + /** + * Marks + * + * Marks are typically decorative sub-types that apply to text nodes. In an + * MDAST, marks are nodes that can contain other nodes. This nested + * hierarchy has to be flattened and split into distinct text nodes with + * their own set of marks. + */ + case 'strong': + case 'emphasis': + case 'delete': { + return processMarkNode(node); + } + + /** + * Headings + * + * MDAST headings use a single type with a separate "depth" property to + * indicate the heading level, while the Slate schema uses a separate node + * type for each heading level. Here we get the proper Slate node name based + * on the MDAST node depth. + */ + case 'heading': { + const slateType = `h${node.depth}`; + return createBlock(slateType, nodes); + } + + /** + * Code Blocks + * + * MDAST code blocks are a distinct node type with a simple text value. We + * convert that value into a nested child text node for Slate. If a void + * node is required due to a custom code block handler, the value is + * stored in the "code" data property instead. We also carry over the "lang" + * data property if it's defined. + */ + case 'code': { + const data = { + lang: node.lang, + ...(voidCodeBlock ? { code: node.value } : {}), + shortcode: 'code-block', + shortcodeData: { + code: node.value, + lang: node.lang, + }, + }; + const text = createText(voidCodeBlock ? '' : node.value); + const nodes = [text]; + const block = createBlock('shortcode', nodes, { data }); + return block; + } + + /** + * Lists + * + * MDAST has a single list type and an "ordered" property. We derive that + * information into the Slate schema's distinct list node types. We also + * include the "start" property, which indicates the number an ordered list + * starts at, if defined. + */ + case 'list': { + const slateType = node.ordered ? 'ol' : 'ul'; + const data = { start: node.start }; + return createBlock(slateType, nodes, { data }); + } + + /** + * Breaks + * + * MDAST soft break nodes represent a trailing double space or trailing + * slash from a Markdown document. In Slate, these are simply transformed to + * line breaks within a text node. + */ + case 'break': { + const { data } = node; + return createInline('break', { data }); + } + + /** + * Thematic Breaks + * + * Thematic breaks are void nodes in the Slate schema. + */ + case 'thematicBreak': { + return createBlock(typeMap[node.type]); + } + + /** + * Links + * + * MDAST stores the link attributes directly on the node, while our Slate + * schema references them in the data object. + */ + case 'link': { + const { title, url, data } = node; + const newData = { ...data, title, url }; + return createInline(typeMap[node.type], { data: newData }, nodes); + } + + /** + * Images + * + * Identical to link nodes except for the lack of child nodes and addition + * of alt attribute data MDAST stores the link attributes directly on the + * node, while our Slate schema references them in the data object. + */ + case 'image': { + const { title, url, alt, data } = node; + const newData = { ...data, title, alt, url }; + return createInline(typeMap[node.type], { data: newData }); + } + + /** + * Tables + * + * Tables are parsed separately because they may include an "align" + * property, which should be passed to the Slate node. + */ + case 'table': { + const data = { align: node.align }; + return createBlock(typeMap[node.type], nodes, { data }); + } + } + } +} diff --git a/packages/decap-cms-widget-richtext/src/serializers/remarkSquashReferences.js b/packages/decap-cms-widget-richtext/src/serializers/remarkSquashReferences.js new file mode 100644 index 000000000000..72dd4fb9d0c9 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/remarkSquashReferences.js @@ -0,0 +1,73 @@ +import { without, flatten } from 'lodash'; +import u from 'unist-builder'; +import mdastDefinitions from 'mdast-util-definitions'; + +/** + * Raw markdown may contain image references or link references. Because there + * is no way to maintain these references within the Slate AST, we convert image + * and link references to standard images and links by putting their url's + * inline. The definitions are then removed from the document. + * + * For example, the following markdown: + * + * ``` + * ![alpha][bravo] + * + * [bravo]: http://example.com/example.jpg + * ``` + * + * Yields: + * + * ``` + * ![alpha](http://example.com/example.jpg) + * ``` + * + */ +export default function remarkSquashReferences() { + return getTransform; + + function getTransform(node) { + const getDefinition = mdastDefinitions(node); + return transform.call(null, getDefinition, node); + } + + function transform(getDefinition, node) { + /** + * Bind the `getDefinition` function to `transform` and recursively map all + * nodes. + */ + const boundTransform = transform.bind(null, getDefinition); + const children = node.children ? node.children.map(boundTransform) : node.children; + + /** + * Combine reference and definition nodes into standard image and link + * nodes. + */ + if (['imageReference', 'linkReference'].includes(node.type)) { + const type = node.type === 'imageReference' ? 'image' : 'link'; + const definition = getDefinition(node.identifier); + + if (definition) { + const { title, url } = definition; + return u(type, { title, url, alt: node.alt }, children); + } + + const pre = u('text', node.type === 'imageReference' ? '![' : '['); + const post = u('text', ']'); + const nodes = children || [u('text', node.alt)]; + return [pre, ...nodes, post]; + } + + /** + * Remove definition nodes and filter the resulting null values from the + * filtered children array. + */ + if (node.type === 'definition') { + return null; + } + + const filteredChildren = without(children, null); + + return { ...node, children: flatten(filteredChildren) }; + } +} diff --git a/packages/decap-cms-widget-richtext/src/serializers/remarkStripTrailingBreaks.js b/packages/decap-cms-widget-richtext/src/serializers/remarkStripTrailingBreaks.js new file mode 100644 index 000000000000..36933face063 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/remarkStripTrailingBreaks.js @@ -0,0 +1,56 @@ +import mdastToString from 'mdast-util-to-string'; + +/** + * Removes break nodes that are at the end of a block. + * + * When a trailing double space or backslash is encountered at the end of a + * markdown block, Remark will interpret the character(s) literally, as only + * break entities followed by text qualify as breaks. A manually created MDAST, + * however, may have such entities, and users of visual editors shouldn't see + * these artifacts in resulting markdown. + */ +export default function remarkStripTrailingBreaks() { + function transform(node) { + if (node.children) { + node.children = node.children + .map((child, idx, children) => { + /** + * Only touch break nodes. Convert all subsequent nodes to their text + * value and exclude the break node if no non-whitespace characters + * are found. + */ + if (child.type === 'break') { + const subsequentNodes = children.slice(idx + 1); + + /** + * Create a small MDAST so that mdastToString can process all + * siblings as children of one node rather than making multiple + * calls. + */ + const fragment = { type: 'root', children: subsequentNodes }; + const subsequentText = mdastToString(fragment); + return subsequentText.trim() ? child : null; + } + + /** + * Always return the child if not a break. + */ + return child; + }) + + /** + * Because some break nodes may be excluded, we filter out the resulting + * null values. + */ + .filter(child => child) + + /** + * Recurse through the MDAST by transforming each individual child node. + */ + .map(transform); + } + return node; + } + + return transform; +} diff --git a/packages/decap-cms-widget-richtext/src/serializers/remarkWrapHtml.js b/packages/decap-cms-widget-richtext/src/serializers/remarkWrapHtml.js new file mode 100644 index 000000000000..6131faaa5744 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/remarkWrapHtml.js @@ -0,0 +1,20 @@ +import u from 'unist-builder'; + +/** + * Ensure that top level 'html' type nodes are wrapped in paragraphs. Html nodes + * are used for text nodes that we don't want Remark or Rehype to parse. + */ +export default function remarkWrapHtml() { + function transform(tree) { + tree.children = tree.children.map(node => { + if (node.type === 'html') { + return u('paragraph', [node]); + } + return node; + }); + + return tree; + } + + return transform; +} diff --git a/packages/decap-cms-widget-richtext/src/serializers/slateRemark.js b/packages/decap-cms-widget-richtext/src/serializers/slateRemark.js new file mode 100644 index 000000000000..7b408f2c6df1 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/slateRemark.js @@ -0,0 +1,467 @@ +import { get, without, last, map, intersection, omit } from 'lodash'; +import u from 'unist-builder'; +import mdastToString from 'mdast-util-to-string'; + +/** + * Map of Slate node types to MDAST/Remark node types. + */ +const typeMap = { + root: 'root', + p: 'paragraph', + h1: 'heading', + h2: 'heading', + h3: 'heading', + h4: 'heading', + h5: 'heading', + h6: 'heading', + quote: 'blockquote', + 'code-block': 'code', + ol: 'list', + ul: 'list', + li: 'listItem', + lic: 'paragraph', + table: 'table', + 'table-row': 'tableRow', + 'table-cell': 'tableCell', + break: 'break', + 'thematic-break': 'thematicBreak', + link: 'link', + image: 'image', + shortcode: 'shortcode', +}; + +/** + * Map of Slate mark types to MDAST/Remark node types. + */ +const markMap = { + bold: 'strong', + italic: 'emphasis', + delete: 'delete', + code: 'inlineCode', +}; + +const blockTypes = [ + 'p', + 'quote', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'ol', + 'ul', + 'li', + 'lic', + 'shortcode', + 'table', + 'table-row', + 'table-cell', +]; + +const inlineTypes = ['link', 'image', 'break']; + +const leadingWhitespaceExp = /^\s+\S/; +const trailingWhitespaceExp = /(?!\S)\s+$/; + +export default function slateToRemark(value, { voidCodeBlock }) { + /** + * The Slate Raw AST generally won't have a top level type, so we set it to + * "root" for clarity. + */ + const root = { + type: 'root', + children: value, + }; + + return transform(root); + + /** + * The transform function mimics the approach of a Remark plugin for + * conformity with the other serialization functions. This function converts + * Slate nodes to MDAST nodes, and recursively calls itself to process child + * nodes to arbitrary depth. + */ + function transform(node) { + /** + * Combine adjacent text and inline nodes before processing so they can + * share marks. + */ + const hasBlockChildren = + node.children && node.children[0] && blockTypes.includes(node.children[0].type); + const children = hasBlockChildren + ? node.children.map(transform).filter(v => v) + : convertInlineAndTextChildren(node.children); + + const output = convertBlockNode(node, children); + //console.log(JSON.stringify(output, null, 2)); + return output; + } + + function removeMarkFromNodes(nodes, markType) { + return nodes.map(node => { + const newNode = { ...node }; + switch (node.type) { + case 'link': { + const updatedNodes = removeMarkFromNodes(node.children, markType); + return { + ...node, + children: updatedNodes, + }; + } + + case 'image': + case 'break': { + const data = omit(node.data, 'marks'); + return { ...node, data }; + } + + default: + delete newNode[markType]; + newNode.marks = newNode.marks + ? newNode.marks.filter(({ type }) => type !== markType) + : []; + + if (newNode.marks.length === 0) { + delete newNode.marks; + } + return newNode; + } + }); + } + + function getNodeMarks(node) { + switch (node.type) { + case 'link': { + // Code marks can't always be condensed together. If all text in a link + // is wrapped in a mark, this function returns that mark and the node + // ends up nested inside of that mark. Code marks sometimes can't do + // that, like when they wrap all of the text content of a link. Here we + // remove code marks before processing so that they stay put. + const nodesWithoutCode = node.children.map(n => { + const newNode = { ...n }; + (newNode.marks = n.marks ? n.marks.filter(({ type }) => type !== 'code') : n.marks), + delete newNode.code; + return newNode; + }); + const childMarks = map(nodesWithoutCode, getNodeMarks); + return intersection(...childMarks); + } + + case 'break': + case 'image': + return map(get(node, ['data', 'marks']), mark => mark.type); + + default: + return getNodeMarkArray(node); + } + } + + function getNodeMarkArray(node) { + return Object.keys(markMap).filter(mark => !!node[mark]); + } + + function getSharedMarks(marks, node) { + const nodeMarks = getNodeMarks(node); + const sharedMarks = intersection(marks, nodeMarks); + if (sharedMarks[0] === 'code') { + return nodeMarks.length === 1 ? marks : []; + } + return sharedMarks; + } + + function extractFirstMark(nodes) { + let firstGroupMarks = getNodeMarks(nodes[0]) || []; + + // If code mark is present, but there are other marks, process others first. + // If only the code mark is present, don't allow it to be shared with other + // nodes. + if (firstGroupMarks[0] === 'code' && firstGroupMarks.length > 1) { + firstGroupMarks = [...without('firstGroupMarks', 'code'), 'code']; + } + + let splitIndex = 1; + + if (firstGroupMarks.length > 0) { + while (splitIndex < nodes.length) { + if (nodes[splitIndex]) { + const sharedMarks = getSharedMarks(firstGroupMarks, nodes[splitIndex]); + if (sharedMarks.length > 0) { + firstGroupMarks = sharedMarks; + } else { + break; + } + } + splitIndex += 1; + } + } + + const markType = firstGroupMarks[0]; + const childNodes = nodes.slice(0, splitIndex); + const updatedChildNodes = markType ? removeMarkFromNodes(childNodes, markType) : childNodes; + const remainingNodes = nodes.slice(splitIndex); + + return [markType, updatedChildNodes, remainingNodes]; + } + + /** + * Converts the strings returned from `splitToNamedParts` to Slate nodes. + */ + function splitWhitespace(node, { trailing } = {}) { + if (!node.text) { + return { trimmedNode: node }; + } + const exp = trailing ? trailingWhitespaceExp : leadingWhitespaceExp; + const index = node.text.search(exp); + if (index > -1) { + const substringIndex = trailing ? index : index + 1; + const firstSplit = node.text.slice(0, substringIndex); + const secondSplit = node.text.slice(substringIndex); + const whitespace = trailing ? secondSplit : firstSplit; + const text = trailing ? firstSplit : secondSplit; + return { whitespace, trimmedNode: { ...node, text } }; + } + return { trimmedNode: node }; + } + + function collectCenterNodes(nodes, leadingNode, trailingNode) { + switch (nodes.length) { + case 0: + return []; + case 1: + return [trailingNode]; + case 2: + return [leadingNode, trailingNode]; + default: + return [leadingNode, ...nodes.slice(1, -1), trailingNode]; + } + } + + function normalizeFlankingWhitespace(nodes) { + const { whitespace: leadingWhitespace, trimmedNode: leadingNode } = splitWhitespace(nodes[0]); + const lastNode = nodes.length > 1 ? last(nodes) : leadingNode; + const trailingSplitResult = splitWhitespace(lastNode, { trailing: true }); + const { whitespace: trailingWhitespace, trimmedNode: trailingNode } = trailingSplitResult; + const centerNodes = collectCenterNodes(nodes, leadingNode, trailingNode).filter(val => val); + return { leadingWhitespace, centerNodes, trailingWhitespace }; + } + + function createText(text) { + return text && u('html', text); + } + + function isNodeInline(node) { + return inlineTypes.includes(node.type); + } + + function convertInlineAndTextChildren(nodes = []) { + const convertedNodes = []; + let remainingNodes = [...nodes]; + + while (remainingNodes.length > 0) { + const nextNode = remainingNodes[0]; + + if (isNodeInline(nextNode) || getNodeMarkArray(nextNode).length > 0) { + const [markType, markNodes, remainder] = extractFirstMark(remainingNodes); + /** + * A node with a code mark will be a text node, and will not be adjacent + * to a sibling code node as the Slate schema requires them to be + * merged. Markdown also requires at least a space between inline code + * nodes. + */ + if (markType === 'code') { + const node = markNodes[0]; + convertedNodes.push(u(markMap[markType], node.data, node.text)); + } else if (!markType && markNodes.length === 1 && isNodeInline(nextNode)) { + const node = markNodes[0]; + convertedNodes.push(convertInlineNode(node, convertInlineAndTextChildren(node.children))); + } else { + const { leadingWhitespace, trailingWhitespace, centerNodes } = + normalizeFlankingWhitespace(markNodes); + const children = convertInlineAndTextChildren(centerNodes); + const markNode = u(markMap[markType], children); + + // Filter out empty marks, otherwise their output literally by + // remark-stringify, eg. an empty bold node becomes "****" + if (mdastToString(markNode) === '') { + remainingNodes = remainder; + continue; + } + + const normalizedNodes = [ + createText(leadingWhitespace), + markNode, + createText(trailingWhitespace), + ].filter(val => val); + convertedNodes.push(...normalizedNodes); + } + remainingNodes = remainder; + } else if (nextNode.type === 'break') { + remainingNodes = remainingNodes.slice(1); + convertedNodes.push(convertInlineNode(nextNode)); + } else { + remainingNodes.shift(); + convertedNodes.push(u('html', nextNode.text)); + } + } + + return convertedNodes; + } + + function convertCodeBlock(node) { + return { + ...node, + type: 'code-block', + data: { + ...node.data, + ...node.data.shortcodeData, + }, + }; + } + + function convertBlockNode(node, children) { + if (node.type == 'shortcode' && node.data.shortcode == 'code-block') { + node = convertCodeBlock(node); + } + + switch (node.type) { + /** + * General + * + * Convert simple cases that only require a type and children, with no + * additional properties. + */ + case 'root': + case 'p': + case 'quote': + case 'li': + case 'lic': + case 'table': + case 'table-row': + case 'table-cell': { + return u(typeMap[node.type], children); + } + + /** + * Lists + * + * Enclose list items in paragraphs + */ + // case 'list-item': + // return u(typeMap[node.type], [{ type: 'paragraph', children }]); + + /** + * Shortcodes + * + * Shortcode nodes only exist in Slate's Raw AST if they were inserted + * via the plugin toolbar in memory, so they should always have + * shortcode data attached. The "shortcode" data property contains the + * name of the registered shortcode plugin, and the "shortcodeData" data + * property contains the data received from the shortcode plugin's + * `fromBlock` method when the shortcode node was created. + * + * Here we create a `shortcode` MDAST node that contains only the shortcode + * data. + */ + case 'shortcode': { + const { data } = node; + return u(typeMap[node.type], { data }); + } + + /** + * Headings + * + * The MDAST schema just has a single "heading" type with the depth stored in + * a "depth" property on the node. Here we derive the depth from the Slate + * node type - e.g., for "h2", we need a depth value of "2". + */ + case 'h1': + case 'h2': + case 'h3': + case 'h4': + case 'h5': + case 'h6': { + const depth = parseInt(node.type[1]); + const mdastNode = u(typeMap[node.type], { depth }, children); + if (mdastToString(mdastNode)) { + return mdastNode; + } + return; + } + + /** + * Code Blocks + * + * Code block nodes may have a single text child, or instead be void and + * store their value in `data.code`. They also may have a code language + * stored in the "lang" data property. Here we transfer both the node value + * and the "lang" data property to the new MDAST node, and spread any + * remaining data as `data`. + */ + case 'code-block': { + const { lang, code, ...data } = get(node, 'data', {}); + const value = voidCodeBlock ? code : children[0]?.value; + return u(typeMap[node.type], { lang, data }, value || ''); + } + + /** + * Lists + * + * Our Slate schema has separate node types for ordered and unordered + * lists, but the MDAST spec uses a single type with a boolean "ordered" + * property to indicate whether the list is numbered. The MDAST spec also + * allows for a "start" property to indicate the first number used for an + * ordered list. Here we translate both values to our Slate schema. + */ + case 'ol': + case 'ul': { + const ordered = node.type === 'ol'; + const props = { ordered, start: get(node.data, 'start') || 1 }; + return u(typeMap[node.type], props, children); + } + + /** + * Thematic Break + * + * Thematic break is a block level break. They cannot have children. + */ + case 'thematic-break': { + return u(typeMap[node.type]); + } + } + } + + function convertInlineNode(node, children) { + switch (node.type) { + /** + * Break + * + * Breaks are phrasing level breaks. They cannot have children. + */ + case 'break': { + return u(typeMap[node.type]); + } + + /** + * Links + * + * Url is now stored in data for slate, so we need to pull it out. + */ + case 'link': { + const { title, data } = node; + return u(typeMap[node.type], { url: data?.url, title, ...data }, children); + } + + /** + * Images + * + * This transformation is almost identical to that of links, except for the + * lack of child nodes and addition of `alt` attribute data. + */ + case 'image': { + const { url, title, alt, ...data } = get(node, 'data', {}); + return u(typeMap[node.type], { url, title, alt, data }); + } + } + } +} diff --git a/packages/decap-cms-widget-richtext/test-helpers/h.js b/packages/decap-cms-widget-richtext/test-helpers/h.js new file mode 100644 index 000000000000..f3aaef045836 --- /dev/null +++ b/packages/decap-cms-widget-richtext/test-helpers/h.js @@ -0,0 +1,32 @@ +import { createHyperscript } from 'slate-hyperscript'; + +const h = createHyperscript({ + blocks: { + paragraph: 'p', + 'heading-one': 'h1', + 'heading-two': 'h2', + 'heading-three': 'h3', + 'heading-four': 'h4', + 'heading-five': 'h5', + 'heading-six': 'h6', + quote: 'quote', + 'code-block': 'code-block', + 'bulleted-list': 'ul', + 'numbered-list': 'ol', + 'thematic-break': 'thematic-break', + table: 'table', + }, + inlines: { + link: 'link', + break: 'break', + image: 'image', + }, + marks: { + b: 'bold', + i: 'italic', + s: 'strikethrough', + code: 'code', + }, +}); + +export default h; From 7cd2eee96a438f85913eb36a8908f78e04de6399 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Thu, 28 Mar 2024 12:50:37 +0100 Subject: [PATCH 05/43] feat(richtext): add link plugin --- .../src/RichtextControl/VisualEditor.js | 5 +++ .../components/Element/LinkElement.js | 21 ++++++++++++ .../components/Toolbar/LinkToolbarButton.js | 33 +++++++++++++++++++ .../components/Toolbar/Toolbar.js | 8 +++++ 4 files changed, 67 insertions(+) create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/LinkElement.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/LinkToolbarButton.js diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index 894ce52226ce..bbf5ae1a16c3 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -21,6 +21,7 @@ import { } from '@udecode/plate-heading'; import { createSoftBreakPlugin, createExitBreakPlugin } from '@udecode/plate-break'; import { createListPlugin, ELEMENT_UL, ELEMENT_OL, ELEMENT_LI } from '@udecode/plate-list'; +import { createLinkPlugin, ELEMENT_LINK } from '@udecode/plate-link'; import { ClassNames } from '@emotion/react'; import { fonts, lengths, zIndex } from 'decap-cms-ui-default'; @@ -33,6 +34,7 @@ import ParagraphElement from './components/Element/ParagraphElement'; import HeadingElement from './components/Element/HeadingElement'; import ListElement from './components/Element/ListElement'; import { markdownToSlate, slateToMarkdown } from '../serializers'; +import LinkElement from './components/Element/LinkElement'; function visualEditorStyles({ minimal }) { return ` @@ -66,6 +68,7 @@ export default function VisualEditor({ t, field, className, isDisabled, onChange createItalicPlugin(), createCodePlugin(), createListPlugin(), + createLinkPlugin(), createSoftBreakPlugin({ options: { rules: [{ hotkey: 'shift+enter' }], @@ -101,6 +104,7 @@ export default function VisualEditor({ t, field, className, isDisabled, onChange [MARK_CODE]: CodeLeaf, [MARK_ITALIC]: withProps(PlateLeaf, { as: 'em' }), [ELEMENT_PARAGRAPH]: ParagraphElement, + [ELEMENT_LINK]: LinkElement, [ELEMENT_H1]: withProps(HeadingElement, { variant: 'h1' }), [ELEMENT_H2]: withProps(HeadingElement, { variant: 'h2' }), [ELEMENT_H3]: withProps(HeadingElement, { variant: 'h3' }), @@ -127,6 +131,7 @@ export default function VisualEditor({ t, field, className, isDisabled, onChange } function handleChange(value) { + console.log('handleChange', value); const mdValue = slateToMarkdown(value, {}); onChange(mdValue); } diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/LinkElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/LinkElement.js new file mode 100644 index 000000000000..39b1ce62ab35 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/LinkElement.js @@ -0,0 +1,21 @@ +import React from 'react'; +import { PlateElement, useElement } from '@udecode/plate-common'; +import { useLink } from '@udecode/plate-link'; +import styled from '@emotion/styled'; + +const StyledA = styled.a` + text-decoration: underline; + font-size: inherit; +`; + +function LinkElement({ children, ...rest }) { + const element = useElement(); + const { props } = useLink({ element }); + return ( + + {children} + + ); +} + +export default LinkElement; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/LinkToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/LinkToolbarButton.js new file mode 100644 index 000000000000..8301416426e8 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/LinkToolbarButton.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { + useLinkToolbarButton, + useLinkToolbarButtonState, + upsertLink, + unwrapLink, +} from '@udecode/plate-link'; +import { useEditorRef } from '@udecode/plate-common'; + +import ToolbarButton from './ToolbarButton'; + +function LinkToolbarButton({ t, ...rest }) { + const state = useLinkToolbarButtonState(); + const { + props: { pressed }, + } = useLinkToolbarButton(state); + + const editor = useEditorRef(); + + function handleClick() { + const url = window.prompt(t('editor.editorWidgets.markdown.linkPrompt'), ''); + + if (url) { + upsertLink(editor, { url, skipValidation: true }); + } else if (url == '') { + unwrapLink(editor); + } + } + + return ; +} + +export default LinkToolbarButton; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js index 09983ea77b3e..ed16b8f337b0 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js @@ -8,6 +8,7 @@ import { MARK_BOLD, MARK_CODE, MARK_ITALIC } from '@udecode/plate-basic-marks'; import MarkToolbarButton from './MarkToolbarButton'; import HeadingToolbarButton from './HeadingToolbarButton'; import ListToolbarButton from './ListToolbarButton'; +import LinkToolbarButton from './LinkToolbarButton'; const ToolbarContainer = styled.div` position: relative; @@ -58,6 +59,13 @@ function Toolbar(props) { disabled={disabled} /> )} + Date: Fri, 29 Mar 2024 09:16:00 +0100 Subject: [PATCH 06/43] feat(richtext): add blockquote --- .../src/RichtextControl/VisualEditor.js | 22 +++++++++++++---- .../components/Element/BlockquoteElement.js | 24 +++++++++++++++++++ .../Toolbar/BlockquoteToolbarButton.js | 20 ++++++++++++++++ .../components/Toolbar/Toolbar.js | 9 +++++++ 4 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/BlockquoteElement.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index bbf5ae1a16c3..6d3e41fe10ff 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -22,6 +22,8 @@ import { import { createSoftBreakPlugin, createExitBreakPlugin } from '@udecode/plate-break'; import { createListPlugin, ELEMENT_UL, ELEMENT_OL, ELEMENT_LI } from '@udecode/plate-list'; import { createLinkPlugin, ELEMENT_LINK } from '@udecode/plate-link'; +import { createBlockquotePlugin, ELEMENT_BLOCKQUOTE } from '@udecode/plate-block-quote'; +import { createTrailingBlockPlugin } from '@udecode/plate-trailing-block'; import { ClassNames } from '@emotion/react'; import { fonts, lengths, zIndex } from 'decap-cms-ui-default'; @@ -35,6 +37,7 @@ import HeadingElement from './components/Element/HeadingElement'; import ListElement from './components/Element/ListElement'; import { markdownToSlate, slateToMarkdown } from '../serializers'; import LinkElement from './components/Element/LinkElement'; +import BlockquoteElement from './components/Element/BlockquoteElement'; function visualEditorStyles({ minimal }) { return ` @@ -69,9 +72,18 @@ export default function VisualEditor({ t, field, className, isDisabled, onChange createCodePlugin(), createListPlugin(), createLinkPlugin(), + createBlockquotePlugin(), createSoftBreakPlugin({ options: { - rules: [{ hotkey: 'shift+enter' }], + rules: [ + { hotkey: 'shift+enter' }, + { + hotkey: 'enter', + query: { + allow: [ELEMENT_BLOCKQUOTE], + }, + }, + ], }, }), createExitBreakPlugin({ @@ -97,6 +109,9 @@ export default function VisualEditor({ t, field, className, isDisabled, onChange ], }, }), + createTrailingBlockPlugin({ + options: { type: ELEMENT_PARAGRAPH }, + }), ], { components: { @@ -104,6 +119,7 @@ export default function VisualEditor({ t, field, className, isDisabled, onChange [MARK_CODE]: CodeLeaf, [MARK_ITALIC]: withProps(PlateLeaf, { as: 'em' }), [ELEMENT_PARAGRAPH]: ParagraphElement, + [ELEMENT_BLOCKQUOTE]: BlockquoteElement, [ELEMENT_LINK]: LinkElement, [ELEMENT_H1]: withProps(HeadingElement, { variant: 'h1' }), [ELEMENT_H2]: withProps(HeadingElement, { variant: 'h2' }), @@ -136,9 +152,7 @@ export default function VisualEditor({ t, field, className, isDisabled, onChange onChange(mdValue); } - const initialValue = props.value - ? markdownToSlate(props.value, {}) - : emptyValue; + const initialValue = props.value ? markdownToSlate(props.value, {}) : emptyValue; return ( diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/BlockquoteElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/BlockquoteElement.js new file mode 100644 index 000000000000..dd825c7a8d42 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/BlockquoteElement.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { PlateElement } from '@udecode/plate-common'; +import styled from '@emotion/styled'; +import { colors } from 'decap-cms-ui-default'; + +const bottomMargin = '16px'; + +const StyledBlockQuote = styled.blockquote` + padding-left: 16px; + border-left: 3px solid ${colors.background}; + margin-left: 0; + margin-right: 0; + margin-bottom: ${bottomMargin}; +`; + +function BlockquoteElement({ children, ...props }) { + return ( + + {children} + + ); +} + +export default BlockquoteElement; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js new file mode 100644 index 000000000000..0b8f145b6f8c --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { useEditorRef, focusEditor, toggleNodeType } from '@udecode/plate-common'; +import { unwrapList } from '@udecode/plate-list'; +import { ELEMENT_BLOCKQUOTE } from '@udecode/plate-block-quote'; + +import ToolbarButton from './ToolbarButton'; + +function BlockquoteToolbarButton(props) { + const editor = useEditorRef(); + + function handleClick() { + unwrapList(editor); + toggleNodeType(editor, { activeType: ELEMENT_BLOCKQUOTE }); + focusEditor(editor); + } + const pressed = false; + return ; +} + +export default BlockquoteToolbarButton; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js index ed16b8f337b0..3bd7002d40a9 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js @@ -9,6 +9,7 @@ import MarkToolbarButton from './MarkToolbarButton'; import HeadingToolbarButton from './HeadingToolbarButton'; import ListToolbarButton from './ListToolbarButton'; import LinkToolbarButton from './LinkToolbarButton'; +import BlockquoteToolbarButton from './BlockquoteToolbarButton'; const ToolbarContainer = styled.div` position: relative; @@ -67,6 +68,14 @@ function Toolbar(props) { t={t} /> + {isVisible('blockquote') && ( + + )} Date: Thu, 4 Apr 2024 14:46:53 +0200 Subject: [PATCH 07/43] feat(richtext): add blockquote plugin special keydown events --- package-lock.json | 1002 ++++------------- .../decap-cms-widget-richtext/package.json | 14 +- .../src/RichtextControl/VisualEditor.js | 3 + .../Toolbar/BlockquoteToolbarButton.js | 5 +- .../plugins/createBlockquoteExitBreak.js | 55 + 5 files changed, 305 insertions(+), 774 deletions(-) create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/plugins/createBlockquoteExitBreak.js diff --git a/package-lock.json b/package-lock.json index 74573e98ca49..dcf4ef86b9e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3070,12 +3070,18 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.5.0", - "license": "MIT", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", "dependencies": { - "@floating-ui/utils": "^0.1.3" + "@floating-ui/utils": "^0.2.1" } }, + "node_modules/@floating-ui/core/node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, "node_modules/@floating-ui/dom": { "version": "1.5.3", "license": "MIT", @@ -7744,132 +7750,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@udecode/cn": { - "version": "29.0.1", - "resolved": "https://registry.npmjs.org/@udecode/cn/-/cn-29.0.1.tgz", - "integrity": "sha512-U41vXvTBKU+06CiQivy4pIWB7RzfaB3DlqkQMNv8UNK164pJhM3v6P0D45kFpbU2uOSOCGpYRSo4kMp9y8RtcQ==", - "dependencies": { - "@udecode/react-utils": "29.0.1" - }, - "peerDependencies": { - "class-variance-authority": ">=0.7.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "tailwind-merge": ">=2.2.0" - } - }, - "node_modules/@udecode/plate": { - "version": "30.9.4", - "resolved": "https://registry.npmjs.org/@udecode/plate/-/plate-30.9.4.tgz", - "integrity": "sha512-HSI27P/sqU+4mrK7YsRGDusmDLpMjT/Gp+Mnuj70/7q1kYKBXP4yxjgEa2ikCnoNXRMsDYhUKS2EOSHfSYy9wA==", - "dependencies": { - "@udecode/plate-alignment": "30.5.3", - "@udecode/plate-autoformat": "30.5.3", - "@udecode/plate-basic-elements": "30.7.0", - "@udecode/plate-basic-marks": "30.5.3", - "@udecode/plate-block-quote": "30.5.3", - "@udecode/plate-break": "30.5.3", - "@udecode/plate-code-block": "30.7.0", - "@udecode/plate-combobox": "30.5.3", - "@udecode/plate-comments": "30.5.3", - "@udecode/plate-common": "30.4.5", - "@udecode/plate-diff": "30.9.0", - "@udecode/plate-find-replace": "30.5.3", - "@udecode/plate-floating": "30.5.3", - "@udecode/plate-font": "30.5.3", - "@udecode/plate-heading": "30.5.3", - "@udecode/plate-highlight": "30.5.3", - "@udecode/plate-horizontal-rule": "30.5.3", - "@udecode/plate-indent": "30.5.3", - "@udecode/plate-indent-list": "30.5.3", - "@udecode/plate-kbd": "30.5.3", - "@udecode/plate-line-height": "30.5.3", - "@udecode/plate-link": "30.9.4", - "@udecode/plate-list": "30.5.3", - "@udecode/plate-media": "30.5.3", - "@udecode/plate-mention": "30.5.3", - "@udecode/plate-node-id": "30.5.3", - "@udecode/plate-normalizers": "30.5.3", - "@udecode/plate-paragraph": "30.5.3", - "@udecode/plate-reset-node": "30.5.3", - "@udecode/plate-resizable": "30.5.3", - "@udecode/plate-select": "30.5.3", - "@udecode/plate-serializer-csv": "30.9.4", - "@udecode/plate-serializer-docx": "30.9.4", - "@udecode/plate-serializer-html": "30.5.3", - "@udecode/plate-serializer-md": "30.9.4", - "@udecode/plate-suggestion": "30.9.0", - "@udecode/plate-tabbable": "30.5.3", - "@udecode/plate-table": "30.9.4", - "@udecode/plate-toggle": "30.9.2", - "@udecode/plate-trailing-block": "30.5.3" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-alignment": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-alignment/-/plate-alignment-30.5.3.tgz", - "integrity": "sha512-zQK2pA5lhUsxCJtNP0eDytfAHjUzqd1znhQh6CzRgoZeALtFkEKee/v329iEv7ZdVFWGtooQoaNraq8PF8+7yg==", - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-autoformat": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-autoformat/-/plate-autoformat-30.5.3.tgz", - "integrity": "sha512-yvRS9Fw3eMr9djRExW8sf6BSc+BYMp0G6tCXHIXvTOfne55+WU4D8eFlrcjy94EhGGddCUS45Q/YvDALYAsTpA==", - "dependencies": { - "lodash": "^4.17.21" - }, - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-basic-elements": { - "version": "30.7.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-basic-elements/-/plate-basic-elements-30.7.0.tgz", - "integrity": "sha512-FYriqwvthx+3BPljePD/eVBszDyi1g1i2RMSs1PlXxqcDlXBuaUtWurWpnViJLK0hzfGagTzaOz6soEMlB5qZw==", - "dependencies": { - "@udecode/plate-block-quote": "30.5.3", - "@udecode/plate-code-block": "30.7.0", - "@udecode/plate-heading": "30.5.3", - "@udecode/plate-paragraph": "30.5.3" - }, - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, "node_modules/@udecode/plate-basic-marks": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-basic-marks/-/plate-basic-marks-30.5.3.tgz", - "integrity": "sha512-/p5WVEz20mWVg+HNrMemDLJ/n0AM2e0GZwn5NTQULXa5i9DcqqcZOXlOayXhxjG4P9/KV9nPdOttQtxti/Sr3g==", + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-basic-marks/-/plate-basic-marks-31.0.0.tgz", + "integrity": "sha512-yV05ohuWk7ZcVxshLQIoqRbJTCbn8hANaMR98PuOMHFajr68/Qvdn9B5MuOzLqPHyOOQ4yVBdzPNcsG6l1DuAg==", "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", + "@udecode/plate-common": ">=31.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", "slate": ">=0.94.0", @@ -7879,11 +7765,11 @@ } }, "node_modules/@udecode/plate-block-quote": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-block-quote/-/plate-block-quote-30.5.3.tgz", - "integrity": "sha512-InFQ/IaS2BFj74CaDU4V/hlbcefXG3joRBw2cH8QJgbB1t4GSBTW8ZoMDDA6L6N9edBUu8R3vQWQgfZhp303Ig==", + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-block-quote/-/plate-block-quote-31.0.0.tgz", + "integrity": "sha512-82gWC4uXsYvkkmtz4/mvlgAx7s6FgkUP80ZVVMJ2O9p9C6HipJs6/fvs5VWR1L8P04+leRIqi5dknm6ZzW5Epg==", "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", + "@udecode/plate-common": ">=31.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", "slate": ">=0.94.0", @@ -7893,11 +7779,11 @@ } }, "node_modules/@udecode/plate-break": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-break/-/plate-break-30.5.3.tgz", - "integrity": "sha512-MwJrHw+1qFIs6HOBoaHGV/jaj6JXj0+BzrxT7FOLvdTK5vVFWC21a+WwgOJWZBZ3NVVRpxp0PxxhwpaqtEkKRw==", + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-break/-/plate-break-31.0.0.tgz", + "integrity": "sha512-dt5btIRIAWVioh9/O/JX8X2UYThGw4/Aks3aRjWjRitwONmTwca9UzxKMj7W0li758Yvd2WPeBJNPIWGJ9cpYw==", "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", + "@udecode/plate-common": ">=31.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", "slate": ">=0.94.0", @@ -7906,32 +7792,20 @@ "slate-react": ">=0.99.0" } }, - "node_modules/@udecode/plate-code-block": { - "version": "30.7.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-code-block/-/plate-code-block-30.7.0.tgz", - "integrity": "sha512-/wodH5+SH9eALLIiUAkcwRE2EO4eIBIe5bIoCYMToe3dwaDF4MVHwBU5jZLzi6cy9osar396CQfPmW1j63MJLQ==", - "dependencies": { - "prismjs": "^1.29.0" - }, - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-combobox": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-combobox/-/plate-combobox-30.5.3.tgz", - "integrity": "sha512-bAq3jWFEPwFwsm0NYoBz+vg6w/8NBKk3Az2x1/HOZ4vwoQNhbI6JLLPWnpTabPDDaF1hIvj9kqV+rOIbSS241g==", + "node_modules/@udecode/plate-common": { + "version": "31.3.2", + "resolved": "https://registry.npmjs.org/@udecode/plate-common/-/plate-common-31.3.2.tgz", + "integrity": "sha512-yhfFoJUlX81gOur093uDXrZu8lflm43DpcRhjHtX0pE6MiwSWIGygTNdjyXEE/MWC4/mwdeU1k94ahCXkfiffw==", "dependencies": { - "downshift": "^6.1.12" + "@udecode/plate-core": "31.3.2", + "@udecode/plate-utils": "31.3.2", + "@udecode/react-utils": "31.0.0", + "@udecode/slate": "31.0.0", + "@udecode/slate-react": "31.0.0", + "@udecode/slate-utils": "31.3.2", + "@udecode/utils": "31.0.0" }, "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", "react": ">=16.8.0", "react-dom": ">=16.8.0", "slate": ">=0.94.0", @@ -7940,66 +7814,46 @@ "slate-react": ">=0.99.0" } }, - "node_modules/@udecode/plate-comments": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-comments/-/plate-comments-30.5.3.tgz", - "integrity": "sha512-DiepkcQ4G+TNTA86fvbuue5sva3XmETkZjYDMsBin/NMn/foiBA2O+SorK3nZ5kCCSZsFgG07UhufnRXFbkN6w==", + "node_modules/@udecode/plate-common/node_modules/@udecode/react-utils": { + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/react-utils/-/react-utils-31.0.0.tgz", + "integrity": "sha512-zvXVIOELvKeizFK9a7nCBGizH/tO7EFOl4N7YSL8fUrd6SZPIoHTNEZg5YKC3bcdB7LUoBno1BJOcWOZDHE5SA==", "dependencies": { - "lodash": "^4.17.21" + "@radix-ui/react-slot": "^1.0.2", + "@udecode/utils": "31.0.0", + "clsx": "^1.2.1" }, "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" + "react-dom": ">=16.8.0" } }, - "node_modules/@udecode/plate-common": { - "version": "30.4.5", - "resolved": "https://registry.npmjs.org/@udecode/plate-common/-/plate-common-30.4.5.tgz", - "integrity": "sha512-p/hF7rvuEqyrxvsfgjaBswv82C/Z1/S5vNj+m33UG91cnPs5sLHbofd5qh7vRgKKfZ/uk028mNpUgemo1bFgbA==", - "dependencies": { - "@udecode/plate-core": "30.4.5", - "@udecode/plate-utils": "30.4.5", - "@udecode/react-utils": "29.0.1", - "@udecode/slate": "25.0.0", - "@udecode/slate-react": "29.0.1", - "@udecode/slate-utils": "25.0.0", - "@udecode/utils": "24.3.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } + "node_modules/@udecode/plate-common/node_modules/@udecode/utils": { + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-31.0.0.tgz", + "integrity": "sha512-06JTl1UAm3mzLLAx8hdMUFw4XRQG727z9JoJ9PeBnmFb9q4Cg3DdmbFnhVJMrBPWlyOwoHtPrBjnanTFeiP36Q==" }, "node_modules/@udecode/plate-core": { - "version": "30.4.5", - "resolved": "https://registry.npmjs.org/@udecode/plate-core/-/plate-core-30.4.5.tgz", - "integrity": "sha512-x/X0dCLoWFyC7wEI9hTcVMR8C/xiTkF0w9I5fyhCMg1mXz/y4DB0CMute+hYT0Wz7rqgj9DYT4v8ryrB9fEu9A==", - "dependencies": { - "@udecode/slate": "25.0.0", - "@udecode/slate-react": "29.0.1", - "@udecode/slate-utils": "25.0.0", - "@udecode/utils": "24.3.0", + "version": "31.3.2", + "resolved": "https://registry.npmjs.org/@udecode/plate-core/-/plate-core-31.3.2.tgz", + "integrity": "sha512-sBEB2vMu2KG4/KTBwyui1mBzORBm+tPg05p/mk+/Ihy/gBlxBpIyiuRUo3iRD9ZTm+sAJLZT5e3Vv8wWyu3Bfg==", + "dependencies": { + "@udecode/slate": "31.0.0", + "@udecode/slate-react": "31.0.0", + "@udecode/slate-utils": "31.3.2", + "@udecode/utils": "31.0.0", "clsx": "^1.2.1", "is-hotkey": "^0.2.0", - "jotai": "^2.6.0", - "jotai-optics": "0.3.1", + "jotai": "^2.7.1", + "jotai-optics": "0.3.2", "jotai-x": "^1.2.2", "lodash": "^4.17.21", - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "optics-ts": "2.4.1", - "react-hotkeys-hook": "^4.4.1", - "use-deep-compare": "^1.1.0", - "zustand": "^4.4.7", - "zustand-x": "^3.0.1" + "react-hotkeys-hook": "^4.5.0", + "use-deep-compare": "^1.2.1", + "zustand": "^4.5.2", + "zustand-x": "^3.0.2" }, "peerDependencies": { "react": ">=16.8.0", @@ -8010,64 +7864,21 @@ "slate-react": ">=0.99.0" } }, - "node_modules/@udecode/plate-diff": { - "version": "30.9.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-diff/-/plate-diff-30.9.0.tgz", - "integrity": "sha512-FiChegLUmW4T4iFDMxBCUjExn0C1rgi4rZM57HtJ6phN3EyotPWHXgn2R7cfGhL6vBAY+4E44CS69dVyeKzkcg==", - "dependencies": { - "lodash": "^4.17.21" - }, - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-find-replace": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-find-replace/-/plate-find-replace-30.5.3.tgz", - "integrity": "sha512-xmv373CgN+fuZR3ESaLS56PkNblNoXSrt5w53MV7I3ISy3vUYU2TZnuvTtn1v59HHSWIZdlIlyTySrsJT0YxdA==", - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } + "node_modules/@udecode/plate-core/node_modules/@udecode/utils": { + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-31.0.0.tgz", + "integrity": "sha512-06JTl1UAm3mzLLAx8hdMUFw4XRQG727z9JoJ9PeBnmFb9q4Cg3DdmbFnhVJMrBPWlyOwoHtPrBjnanTFeiP36Q==" }, "node_modules/@udecode/plate-floating": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-floating/-/plate-floating-30.5.3.tgz", - "integrity": "sha512-9KxpZdKLy45a3Z+MJqSGmuJKQrl7CrNsLyUdjKD4Iqd1DIdBwl65dGqTmgI1EycF2jUsWIrgGE3W71f7E5/JdA==", + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-floating/-/plate-floating-31.0.0.tgz", + "integrity": "sha512-k1KZjpGCH+x/rDCSUZ1Kd4ttPc/35Xp/T+pmI2jYQ48dlorYQPJSuJxpFE/cpBp1g8G2lLh4xbH5BTYR1bypAQ==", "dependencies": { - "@floating-ui/core": "^1.3.1", + "@floating-ui/core": "^1.6.0", "@floating-ui/react": "^0.22.3" }, "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-font": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-font/-/plate-font-30.5.3.tgz", - "integrity": "sha512-S40ES4ihWBHF9/BscZGkQCV0b/wNtQcKdaHS6DvQ3JxnzY73XEwwvaJ19FBrPJ2U/CD+i+9SIF5do1bgW5jQuw==", - "dependencies": { - "lodash": "^4.17.21" - }, - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", + "@udecode/plate-common": ">=31.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", "slate": ">=0.94.0", @@ -8077,100 +7888,11 @@ } }, "node_modules/@udecode/plate-heading": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-heading/-/plate-heading-30.5.3.tgz", - "integrity": "sha512-F0SRJSXQtIw6N4AXcENyR01KNSZdflExsQnsEyjDGHZfF0x4bjCt7AeMr79ZDJ+ZAFTrOUKGR53+z2CV2G5ixg==", - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-highlight": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-highlight/-/plate-highlight-30.5.3.tgz", - "integrity": "sha512-20RlkxTNkJTJH+c1fE3D0wwt2WfUBKJMp97I7S0cVkWE7jv/HVzFsXsfdzQeS2Yms4tEA/ZiZQCXoRmqbHvkOw==", - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-horizontal-rule": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-horizontal-rule/-/plate-horizontal-rule-30.5.3.tgz", - "integrity": "sha512-qsAnS9eW/REH+fXXWUy8O27VhYOEFRMhMlXIp83dIDKP2BtXeR2JeVHdM2wa5oEo+3G7o7Qy2DS5Yg51A3wu/Q==", - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-indent": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-indent/-/plate-indent-30.5.3.tgz", - "integrity": "sha512-39V7egkg0Gk0z5nKveAh6gipeH12MQdtSzGphGtpLQqyMF+d+eOLsaZDJsO0XfZlkDTeonA8cDr5ZWERc9SHXQ==", + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-heading/-/plate-heading-31.0.0.tgz", + "integrity": "sha512-TA9hc1sydRiLykqfJ5FmdYkLNpVkMxY7u0YL1QVOTQLCXYPbJVNuQ1vUsFxsU3mmqGb/J5Xsu87kLRHSnRQR0A==", "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-indent-list": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-indent-list/-/plate-indent-list-30.5.3.tgz", - "integrity": "sha512-j9UYOdGf8Qif3X6uMaZrV1yildC9CJSrN4jpscFy5T+80g0ysnb7g6cv4R1FNBzTT6tth3FM7cXacbIuyK7DSQ==", - "dependencies": { - "@udecode/plate-indent": "30.5.3", - "@udecode/plate-list": "30.5.3", - "clsx": "^1.2.1" - }, - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-kbd": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-kbd/-/plate-kbd-30.5.3.tgz", - "integrity": "sha512-pPpNbrBAIF6mSTRzR1/WcT0xUS4g/9MmGufAdm8FpFogR6N8CVGZRylqCdx+i0IDHjKHy4mkMLDUP/HVUuWnmQ==", - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-line-height": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-line-height/-/plate-line-height-30.5.3.tgz", - "integrity": "sha512-8y329FUhLcEJQf11+JDs26YCAgH6qhMAEVHwvtQuZKfonHRSFabzkt13f61b2uRZ7piqz8fYWJCEXwGLaPYevA==", - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", + "@udecode/plate-common": ">=31.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", "slate": ">=0.94.0", @@ -8180,15 +7902,15 @@ } }, "node_modules/@udecode/plate-link": { - "version": "30.9.4", - "resolved": "https://registry.npmjs.org/@udecode/plate-link/-/plate-link-30.9.4.tgz", - "integrity": "sha512-aBVOPNeI62nzzDFSCxj3NDdpn1XsmsOpcdAleG4ZrtB9o4ndTXDTN1m0NumComk7KPRFAod7NWOuo7KwNMK6JQ==", + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-link/-/plate-link-31.0.0.tgz", + "integrity": "sha512-607injR8Bf5Tek4LIjjcuPoDDFMq0i+3cuX1AlIsqbwqZH3gffQZ700OKhisy17bu8SD9aqAgiCfKKlHXM4cdA==", "dependencies": { - "@udecode/plate-floating": "30.5.3", - "@udecode/plate-normalizers": "30.5.3" + "@udecode/plate-floating": "31.0.0", + "@udecode/plate-normalizers": "31.0.0" }, "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", + "@udecode/plate-common": ">=31.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", "slate": ">=0.94.0", @@ -8198,66 +7920,15 @@ } }, "node_modules/@udecode/plate-list": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-list/-/plate-list-30.5.3.tgz", - "integrity": "sha512-Q6c1hE4oAZp3OkJzoeRIp+ULKcugsNx0Eh4o/yKyWJAx/DzZNPJyuuAyClA9nZMdWv96UAjvEZ75Em3BcFtTwg==", + "version": "31.1.3", + "resolved": "https://registry.npmjs.org/@udecode/plate-list/-/plate-list-31.1.3.tgz", + "integrity": "sha512-TjD5JeKsuzsdSkepgjFMDcJsB6BHdowO/ppHLxUCge74RwPVF7U97HPc7oEtx+OLsrC13gkbpZvwLHfdCy9dDQ==", "dependencies": { - "@udecode/plate-reset-node": "30.5.3", + "@udecode/plate-reset-node": "31.0.0", "lodash": "^4.17.21" }, "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-media": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-media/-/plate-media-30.5.3.tgz", - "integrity": "sha512-cO4o+257oDMqOtgLMgFxUbFLWov+HUi8GXpd6NbUxPkoGUw24vo3or6Wni+X3DlUJQF0Do5/g9bwZlQcT1IZGw==", - "dependencies": { - "js-video-url-parser": "^0.5.1" - }, - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-mention": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-mention/-/plate-mention-30.5.3.tgz", - "integrity": "sha512-OvlgSaHT39dxSRgjS9NsEvbduNbuTWG4KLoEHFJpFgJmGwrVmcPR/AuCpHLiTlDixHVWHJjisp+/imF1eJolQQ==", - "dependencies": { - "@udecode/plate-combobox": "30.5.3" - }, - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-node-id": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-node-id/-/plate-node-id-30.5.3.tgz", - "integrity": "sha512-4Dho3kuW/SZWBA5kKzuAaupJbRVMKq8Et0LXWbV3qO5/XtCGsOVZtOaeeIldplLXvYYQp3GN/NqGF6GGdwDziQ==", - "dependencies": { - "lodash": "^4.17.21" - }, - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", + "@udecode/plate-common": ">=31.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", "slate": ">=0.94.0", @@ -8267,14 +7938,14 @@ } }, "node_modules/@udecode/plate-normalizers": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-normalizers/-/plate-normalizers-30.5.3.tgz", - "integrity": "sha512-jf8H5OPPLEYgaoQ0pyHZfSXwzZBxI959BxHy83Y1wvhB5Yykgc8NflNGme3ds/rMED3z90E7QOCL2h1waHNtNQ==", + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-normalizers/-/plate-normalizers-31.0.0.tgz", + "integrity": "sha512-4HD39jOgv5Hf8sQqXtZdQLcWYHToXGjBPFU33pYHbEBmxEN9cd5ndxRTCnoOspI9HJMkpu0OzrhUfhLvoPDP4w==", "dependencies": { "lodash": "^4.17.21" }, "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", + "@udecode/plate-common": ">=31.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", "slate": ">=0.94.0", @@ -8284,11 +7955,11 @@ } }, "node_modules/@udecode/plate-paragraph": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-paragraph/-/plate-paragraph-30.5.3.tgz", - "integrity": "sha512-vqvN6Gex1aj189C3ohuq85g6reajYqJMFb4CETGqUTifmKw0ReeJ6a8OYhNqX7v2xE+4gEBm+Z8qO3Z3CnoHqw==", + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-paragraph/-/plate-paragraph-31.0.0.tgz", + "integrity": "sha512-uuaksPfDhK5ShVhjZ0pbXlUgy5nKKDkXzrAfDEZJzwF1R2N0HTy2WcmNJFm+aN8ZUFbZ4MHBuUUldZc/aLuCqw==", "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", + "@udecode/plate-common": ">=31.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", "slate": ">=0.94.0", @@ -8298,39 +7969,11 @@ } }, "node_modules/@udecode/plate-reset-node": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-reset-node/-/plate-reset-node-30.5.3.tgz", - "integrity": "sha512-bBUnE3uMw+jp7zAaZtagCRB9WpBZxJfLdhc1YdqwU1Hmqqy4l0GaH4/oq2QtnN8DtZnOV/PkJlus8tgsP3yzjg==", - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-resizable": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-resizable/-/plate-resizable-30.5.3.tgz", - "integrity": "sha512-fBsWIA8JHDCH8Q7NHkhu300rVTMM8ELcAT/MAD+FyILiLdtYNWA/o9nncjc7HRpyLvsrEKDN2PPCTizKUeaf2Q==", - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-select": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-select/-/plate-select-30.5.3.tgz", - "integrity": "sha512-cVWqikhiwMOBI7IQOCL/vi8HzR4lDfGnlGfeRl4nE6XqbCEmydtKsXzKbsUmxDIv0AhTPgAhWoRjYEv/S7Yoag==", + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-reset-node/-/plate-reset-node-31.0.0.tgz", + "integrity": "sha512-pIdexCNsJx21UHeHrDxeOTS2w0NfthCD5klZGiiKBkU+sd65btmyY1fEQmlVaaeqWKzYCXai2ctSlRAycBV7wA==", "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", + "@udecode/plate-common": ">=31.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", "slate": ">=0.94.0", @@ -8339,83 +7982,12 @@ "slate-react": ">=0.99.0" } }, - "node_modules/@udecode/plate-serializer-csv": { - "version": "30.9.4", - "resolved": "https://registry.npmjs.org/@udecode/plate-serializer-csv/-/plate-serializer-csv-30.9.4.tgz", - "integrity": "sha512-kPiHT84/HzUsqZMLajOWYNUPnKremtvGlHrsl5vt9KwMjih7WPPHxsjimfFyE1m0vc97lPdGqDBaxFRD+lxWFA==", - "dependencies": { - "@udecode/plate-table": "30.9.4", - "papaparse": "^5.4.1" - }, - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-serializer-docx": { - "version": "30.9.4", - "resolved": "https://registry.npmjs.org/@udecode/plate-serializer-docx/-/plate-serializer-docx-30.9.4.tgz", - "integrity": "sha512-pKNc/HVOO4sOhRlSmG8Ukvyd7A2mG+3NGDGHcTtjnJOitDIb3809aMeMWZvMSCIE7bAljYu3RjAO2righw9l/Q==", - "dependencies": { - "@udecode/plate-heading": "30.5.3", - "@udecode/plate-indent": "30.5.3", - "@udecode/plate-indent-list": "30.5.3", - "@udecode/plate-media": "30.5.3", - "@udecode/plate-paragraph": "30.5.3", - "@udecode/plate-table": "30.9.4", - "validator": "^13.9.0" - }, - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-serializer-html": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-serializer-html/-/plate-serializer-html-30.5.3.tgz", - "integrity": "sha512-RESODsZPLiv5efaoOfWph+4+1JPIwNJzhSpqPK7L9TX/wO4M5W8CY9F9IXtdftpn/Xt+WOX1ApPt+7xEWeCQLA==", - "dependencies": { - "html-entities": "^2.4.0" - }, - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-serializer-md": { - "version": "30.9.4", - "resolved": "https://registry.npmjs.org/@udecode/plate-serializer-md/-/plate-serializer-md-30.9.4.tgz", - "integrity": "sha512-ah6ccXZ9oTlJ7uUeJEFeS5LCBNG1thCnrPvTmwDjoSqaFFFm/ifHAafRqsREN1vW4yqTRILCOuDVDG2ciccNXg==", - "dependencies": { - "@udecode/plate-basic-marks": "30.5.3", - "@udecode/plate-block-quote": "30.5.3", - "@udecode/plate-code-block": "30.7.0", - "@udecode/plate-heading": "30.5.3", - "@udecode/plate-horizontal-rule": "30.5.3", - "@udecode/plate-link": "30.9.4", - "@udecode/plate-list": "30.5.3", - "@udecode/plate-media": "30.5.3", - "@udecode/plate-paragraph": "30.5.3", - "remark-parse": "^9.0.0", - "unified": "^9.2.2" - }, + "node_modules/@udecode/plate-trailing-block": { + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-trailing-block/-/plate-trailing-block-31.0.0.tgz", + "integrity": "sha512-vW4BEP3rp9wbEf1ntDcKcAN95ve6ZbO7lLgh9lUBXMVLEvbfcruamXOdMPRYAosokdPF/U+HeNqKtPhkngAMdw==", "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", + "@udecode/plate-common": ">=31.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", "slate": ">=0.94.0", @@ -8424,75 +7996,21 @@ "slate-react": ">=0.99.0" } }, - "node_modules/@udecode/plate-serializer-md/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "engines": { - "node": ">=4" - } - }, - "node_modules/@udecode/plate-serializer-md/node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@udecode/plate-serializer-md/node_modules/remark-parse": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", - "integrity": "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==", - "dependencies": { - "mdast-util-from-markdown": "^0.8.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@udecode/plate-serializer-md/node_modules/unified": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", - "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", - "dependencies": { - "bail": "^1.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^2.0.0", - "trough": "^1.0.0", - "vfile": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@udecode/plate-suggestion": { - "version": "30.9.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-suggestion/-/plate-suggestion-30.9.0.tgz", - "integrity": "sha512-Ca1el7ei+pvm1SAh6nIgUQbH4cNpoi3nfkQFvAg96V2G+Yn9dBBcHIJ6w/971hgJ7Lp7tzGaUdlwfCjjdO1J5Q==", - "dependencies": { - "@udecode/plate-diff": "30.9.0", + "node_modules/@udecode/plate-utils": { + "version": "31.3.2", + "resolved": "https://registry.npmjs.org/@udecode/plate-utils/-/plate-utils-31.3.2.tgz", + "integrity": "sha512-IedyGPqF/yrSjc1ODBSM5xZXC5yPORyqq5vFlk+uNV4khPMDer2PMGnv5se6yVQhSMdAN81gLtlFKONDKIjAjQ==", + "dependencies": { + "@udecode/plate-core": "31.3.2", + "@udecode/react-utils": "31.0.0", + "@udecode/slate": "31.0.0", + "@udecode/slate-react": "31.0.0", + "@udecode/slate-utils": "31.3.2", + "@udecode/utils": "31.0.0", + "clsx": "^1.2.1", "lodash": "^4.17.21" }, "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", "react": ">=16.8.0", "react-dom": ">=16.8.0", "slate": ">=0.94.0", @@ -8501,103 +8019,61 @@ "slate-react": ">=0.99.0" } }, - "node_modules/@udecode/plate-tabbable": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-tabbable/-/plate-tabbable-30.5.3.tgz", - "integrity": "sha512-NJaVPOjjG20MPjbvpPmPdYZKkzMF2f7xZKdjpOPMd6SpgvtvV4Cs7iIRDONT6i/i/zyG7tmdSekEOm9Ifxg40w==", + "node_modules/@udecode/plate-utils/node_modules/@udecode/react-utils": { + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/react-utils/-/react-utils-31.0.0.tgz", + "integrity": "sha512-zvXVIOELvKeizFK9a7nCBGizH/tO7EFOl4N7YSL8fUrd6SZPIoHTNEZg5YKC3bcdB7LUoBno1BJOcWOZDHE5SA==", "dependencies": { - "tabbable": "^6.2.0" + "@radix-ui/react-slot": "^1.0.2", + "@udecode/utils": "31.0.0", + "clsx": "^1.2.1" }, "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" + "react-dom": ">=16.8.0" } }, - "node_modules/@udecode/plate-table": { - "version": "30.9.4", - "resolved": "https://registry.npmjs.org/@udecode/plate-table/-/plate-table-30.9.4.tgz", - "integrity": "sha512-53Y2Iu4QbKuvUkuvijmG+758x+gylwdvv0g04w++k061MEjOc0JkVBbsz6Otv4h5vWTbXX2tLooTZK05NyX/Jw==", - "dependencies": { - "@udecode/plate-resizable": "30.5.3", - "lodash": "^4.17.21" - }, - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-react": ">=0.99.0" - } + "node_modules/@udecode/plate-utils/node_modules/@udecode/utils": { + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-31.0.0.tgz", + "integrity": "sha512-06JTl1UAm3mzLLAx8hdMUFw4XRQG727z9JoJ9PeBnmFb9q4Cg3DdmbFnhVJMrBPWlyOwoHtPrBjnanTFeiP36Q==" }, - "node_modules/@udecode/plate-toggle": { - "version": "30.9.2", - "resolved": "https://registry.npmjs.org/@udecode/plate-toggle/-/plate-toggle-30.9.2.tgz", - "integrity": "sha512-yY/JtDN+P66T9QjLoYJxIfITe3QEpjkSBCowRc0dhOIBTXlcownktmGpiSSQfbFSHAo5XSzO4JJoDodTkW/xGg==", + "node_modules/@udecode/slate": { + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/slate/-/slate-31.0.0.tgz", + "integrity": "sha512-VK84em/ZQYgu2PnXBLG8ON47n3DAZZL//yA3oWs4J3hTg92UTXpizNZiwk9iA+mb+xcRomwuWwpTUCyE8VI3rQ==", "dependencies": { - "@udecode/plate-indent": "30.5.3", - "@udecode/plate-node-id": "30.5.3", - "lodash": "^4.17.21" + "@udecode/utils": "31.0.0" }, "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-trailing-block": { - "version": "30.5.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-trailing-block/-/plate-trailing-block-30.5.3.tgz", - "integrity": "sha512-0Vzt2cVXFlFy8gwoFxnTTW8knd7cKt4LFhETXUlVOzPQk0crRZ4s+I/oENwzUQ9v/iF5dsUQ5Tn2MTUJjCn1NQ==", - "peerDependencies": { - "@udecode/plate-common": ">=30.4.5 < 31", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" + "slate-history": ">=0.93.0" } }, - "node_modules/@udecode/plate-utils": { - "version": "30.4.5", - "resolved": "https://registry.npmjs.org/@udecode/plate-utils/-/plate-utils-30.4.5.tgz", - "integrity": "sha512-cJ0auswNFxhv/qF9yqrIbgPa3mqxWtLtBQ/N+1zqMfEM3vzWE+4WlHpMJb/SdAC/Dvuc5zzfB26/t2IyhrZp5w==", - "dependencies": { - "@udecode/plate-core": "30.4.5", - "@udecode/react-utils": "29.0.1", - "@udecode/slate": "25.0.0", - "@udecode/slate-react": "29.0.1", - "@udecode/slate-utils": "25.0.0", - "@udecode/utils": "24.3.0", - "clsx": "^1.2.1", - "lodash": "^4.17.21" + "node_modules/@udecode/slate-react": { + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/slate-react/-/slate-react-31.0.0.tgz", + "integrity": "sha512-+xYLSZO7u9KrJoCY88udFVT29fp4waX0mFM+gmhd10Kfb/l5xB5yt5248PCqbkehwb93TBntODXtbQzmwNFkag==", + "dependencies": { + "@udecode/react-utils": "31.0.0", + "@udecode/slate": "31.0.0", + "@udecode/utils": "31.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0", "slate": ">=0.94.0", "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", "slate-react": ">=0.99.0" } }, - "node_modules/@udecode/react-utils": { - "version": "29.0.1", - "resolved": "https://registry.npmjs.org/@udecode/react-utils/-/react-utils-29.0.1.tgz", - "integrity": "sha512-+bFJFTDsWArFaC4AZFap0VdCvEbu5ZA16avj4xjjdBBho4TiHOZ7RMDliwTUetA3DOm5LG02dmZ1U4ORNC0m3w==", + "node_modules/@udecode/slate-react/node_modules/@udecode/react-utils": { + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/react-utils/-/react-utils-31.0.0.tgz", + "integrity": "sha512-zvXVIOELvKeizFK9a7nCBGizH/tO7EFOl4N7YSL8fUrd6SZPIoHTNEZg5YKC3bcdB7LUoBno1BJOcWOZDHE5SA==", "dependencies": { "@radix-ui/react-slot": "^1.0.2", - "@udecode/utils": "24.3.0", + "@udecode/utils": "31.0.0", "clsx": "^1.2.1" }, "peerDependencies": { @@ -8605,42 +8081,18 @@ "react-dom": ">=16.8.0" } }, - "node_modules/@udecode/slate": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/@udecode/slate/-/slate-25.0.0.tgz", - "integrity": "sha512-mGb9nMDwIygLqERwJ8kTOfo3wIxMQ0xLJEPKn09jrshEIxUCyO3mYj8y/5vOMcrzj6yexOsgQ6VNX8ylS3lnIQ==", - "dependencies": { - "@udecode/utils": "24.3.0" - }, - "peerDependencies": { - "slate": ">=0.94.0", - "slate-history": ">=0.93.0" - } - }, - "node_modules/@udecode/slate-react": { - "version": "29.0.1", - "resolved": "https://registry.npmjs.org/@udecode/slate-react/-/slate-react-29.0.1.tgz", - "integrity": "sha512-DOiGXxfL43tVyNg0LneTQGQBW/HkF2srwIM8b0Al/x082HHfo2PP2WkFqPqTh1uGUAa2RBRh9xFKmNkKeuyWSw==", - "dependencies": { - "@udecode/react-utils": "29.0.1", - "@udecode/slate": "25.0.0", - "@udecode/utils": "24.3.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-react": ">=0.99.0" - } + "node_modules/@udecode/slate-react/node_modules/@udecode/utils": { + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-31.0.0.tgz", + "integrity": "sha512-06JTl1UAm3mzLLAx8hdMUFw4XRQG727z9JoJ9PeBnmFb9q4Cg3DdmbFnhVJMrBPWlyOwoHtPrBjnanTFeiP36Q==" }, "node_modules/@udecode/slate-utils": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/@udecode/slate-utils/-/slate-utils-25.0.0.tgz", - "integrity": "sha512-H8dECl5Tu44Nt946rkSXCJ1yzsc2R9GXSoA9oNIBmcyNo3jTHZOyG/Ocn3RGgfzAK996A43GBD/keNabJEPtQg==", + "version": "31.3.2", + "resolved": "https://registry.npmjs.org/@udecode/slate-utils/-/slate-utils-31.3.2.tgz", + "integrity": "sha512-ziQN60VItE9GHE7B8+sBnXFJ3P8bVJhfYA0TiwBtjRSoyUWIglUwCbFSBP+QsHmsNp8m+YztPavK6djjczD30Q==", "dependencies": { - "@udecode/slate": "25.0.0", - "@udecode/utils": "24.3.0", + "@udecode/slate": "31.0.0", + "@udecode/utils": "31.0.0", "lodash": "^4.17.21" }, "peerDependencies": { @@ -8648,10 +8100,15 @@ "slate-history": ">=0.93.0" } }, - "node_modules/@udecode/utils": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-24.3.0.tgz", - "integrity": "sha512-/Y2lh/Ih1wx4zN35Ky2Z1G1/5f7cSAS7F6dkhrcbJUnDF0srTidoEIRabK+og/yIK/MCEFfOsQGetoV7Ert5hg==" + "node_modules/@udecode/slate-utils/node_modules/@udecode/utils": { + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-31.0.0.tgz", + "integrity": "sha512-06JTl1UAm3mzLLAx8hdMUFw4XRQG727z9JoJ9PeBnmFb9q4Cg3DdmbFnhVJMrBPWlyOwoHtPrBjnanTFeiP36Q==" + }, + "node_modules/@udecode/slate/node_modules/@udecode/utils": { + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-31.0.0.tgz", + "integrity": "sha512-06JTl1UAm3mzLLAx8hdMUFw4XRQG727z9JoJ9PeBnmFb9q4Cg3DdmbFnhVJMrBPWlyOwoHtPrBjnanTFeiP36Q==" }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", @@ -11830,11 +11287,6 @@ "dev": true, "license": "MIT" }, - "node_modules/compute-scroll-into-view": { - "version": "1.0.20", - "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", - "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==" - }, "node_modules/concat-map": { "version": "0.0.1", "license": "MIT" @@ -14087,26 +13539,6 @@ "node": ">=12" } }, - "node_modules/downshift": { - "version": "6.1.12", - "resolved": "https://registry.npmjs.org/downshift/-/downshift-6.1.12.tgz", - "integrity": "sha512-7XB/iaSJVS4T8wGFT3WRXmSF1UlBHAA40DshZtkrIscIN+VC+Lh363skLxFTvJwtNgHxAMDGEHT4xsyQFWL+UA==", - "dependencies": { - "@babel/runtime": "^7.14.8", - "compute-scroll-into-view": "^1.0.17", - "prop-types": "^15.7.2", - "react-is": "^17.0.2", - "tslib": "^2.3.0" - }, - "peerDependencies": { - "react": ">=16.12.0" - } - }, - "node_modules/downshift/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" - }, "node_modules/duplexer": { "version": "0.1.2", "license": "MIT" @@ -16856,7 +16288,10 @@ } }, "node_modules/html-entities": { - "version": "2.4.0", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "dev": true, "funding": [ { "type": "github", @@ -16866,8 +16301,7 @@ "type": "patreon", "url": "https://patreon.com/mdevils" } - ], - "license": "MIT" + ] }, "node_modules/html-escaper": { "version": "2.0.2", @@ -18835,9 +18269,9 @@ } }, "node_modules/jotai": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.7.1.tgz", - "integrity": "sha512-bsaTPn02nFgWNP6cBtg/htZhCu4s0wxqoklRHePp6l/vlsypR9eLn7diRliwXYWMXDpPvW/LLA2afI8vwgFFaw==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.7.2.tgz", + "integrity": "sha512-6Ft5kpNu8p93Ssf1Faoza3hYQZRIYp7rioK8MwTTFnbQKwUyZElwquPwl1h6U0uo9hC0jr+ghO3gcSjc6P35/Q==", "engines": { "node": ">=12.20.0" }, @@ -18855,9 +18289,9 @@ } }, "node_modules/jotai-optics": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/jotai-optics/-/jotai-optics-0.3.1.tgz", - "integrity": "sha512-KibUx9IneM2hGWGIYGs/v0KCxU985lg7W2c6dt5RodJCB2XPbmok8rkkLmdVk9+fKsn2shkPMi+AG8XzHgB3+w==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/jotai-optics/-/jotai-optics-0.3.2.tgz", + "integrity": "sha512-RH6SvqU5hmkVqnHmaqf9zBXvIAs4jLxkDHS4fr5ljuBKHs8+HQ02v+9hX7ahTppxx6dUb0GGUE80jQKJ0kFTLw==", "peerDependencies": { "jotai": ">=1.11.0", "optics-ts": "*" @@ -18897,11 +18331,6 @@ "version": "4.0.0", "license": "MIT" }, - "node_modules/js-video-url-parser": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/js-video-url-parser/-/js-video-url-parser-0.5.1.tgz", - "integrity": "sha512-/vwqT67k0AyIGMHAvSOt+n4JfrZWF7cPKgKswDO35yr27GfW4HtjpQVlTx6JLF45QuPm8mkzFHkZgFVnFm4x/w==" - }, "node_modules/js-yaml": { "version": "4.1.0", "license": "MIT", @@ -21607,14 +21036,15 @@ "license": "ISC" }, "node_modules/nanoid": { - "version": "3.3.6", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -26786,11 +26216,6 @@ "version": "1.0.11", "license": "(MIT AND Zlib)" }, - "node_modules/papaparse": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", - "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" - }, "node_modules/parent-module": { "version": "1.0.1", "license": "MIT", @@ -27478,14 +26903,6 @@ "node": ">= 0.8" } }, - "node_modules/prismjs": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", - "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", - "engines": { - "node": ">=6" - } - }, "node_modules/proc-log": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", @@ -33154,14 +32571,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/validator": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", - "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/value-equal": { "version": "1.0.1", "license": "MIT" @@ -35176,14 +34585,22 @@ "version": "3.1.0", "license": "MIT", "dependencies": { - "@udecode/cn": "^29.0.1", - "@udecode/plate": "^30.5.0", + "@udecode/plate-basic-marks": "^31.0.0", + "@udecode/plate-block-quote": "^31.0.0", + "@udecode/plate-break": "^31.0.0", + "@udecode/plate-common": "^31.0.0", + "@udecode/plate-heading": "^31.0.0", + "@udecode/plate-link": "^31.0.0", + "@udecode/plate-list": "^31.1.3", + "@udecode/plate-paragraph": "^31.0.0", + "@udecode/plate-trailing-block": "^31.0.0", "class-variance-authority": "^0.7.0", "lucide-react": "^0.331.0", "slate": "^0.102.0", "slate-history": "^0.100.0", "slate-hyperscript": "^0.100.0", - "slate-react": "^0.102.0" + "slate-react": "^0.102.0", + "unified": "^9.0.0" }, "peerDependencies": { "@emotion/react": "^11.11.1", @@ -35197,6 +34614,36 @@ "react-immutable-proptypes": "^2.1.0" } }, + "packages/decap-cms-widget-richtext/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "packages/decap-cms-widget-richtext/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "engines": { + "node": ">=8" + } + }, "packages/decap-cms-widget-richtext/node_modules/slate-hyperscript": { "version": "0.100.0", "resolved": "https://registry.npmjs.org/slate-hyperscript/-/slate-hyperscript-0.100.0.tgz", @@ -35208,6 +34655,23 @@ "slate": ">=0.65.3" } }, + "packages/decap-cms-widget-richtext/node_modules/unified": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", + "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", + "dependencies": { + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^2.0.0", + "trough": "^1.0.0", + "vfile": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "packages/decap-cms-widget-select": { "version": "3.1.1", "license": "MIT", diff --git a/packages/decap-cms-widget-richtext/package.json b/packages/decap-cms-widget-richtext/package.json index d70fe69792ff..8a1fe45b8b36 100644 --- a/packages/decap-cms-widget-richtext/package.json +++ b/packages/decap-cms-widget-richtext/package.json @@ -21,14 +21,22 @@ "build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward --copy-files --extensions \".js,.jsx,.ts,.tsx\"" }, "dependencies": { - "@udecode/cn": "^29.0.1", - "@udecode/plate": "^30.5.0", + "@udecode/plate-common": "^31.0.0", + "@udecode/plate-basic-marks": "^31.0.0", + "@udecode/plate-block-quote": "^31.0.0", + "@udecode/plate-break": "^31.0.0", + "@udecode/plate-heading": "^31.0.0", + "@udecode/plate-link": "^31.0.0", + "@udecode/plate-list": "^31.1.3", + "@udecode/plate-paragraph": "^31.0.0", + "@udecode/plate-trailing-block": "^31.0.0", "class-variance-authority": "^0.7.0", "lucide-react": "^0.331.0", "slate": "^0.102.0", "slate-history": "^0.100.0", "slate-hyperscript": "^0.100.0", - "slate-react": "^0.102.0" + "slate-react": "^0.102.0", + "unified": "^9.0.0" }, "peerDependencies": { "@emotion/react": "^11.11.1", diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index 6d3e41fe10ff..a74cadbee122 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -38,6 +38,7 @@ import ListElement from './components/Element/ListElement'; import { markdownToSlate, slateToMarkdown } from '../serializers'; import LinkElement from './components/Element/LinkElement'; import BlockquoteElement from './components/Element/BlockquoteElement'; +import createBlockquoteExtPlugin from './plugins/createBlockquoteExitBreak'; function visualEditorStyles({ minimal }) { return ` @@ -63,6 +64,7 @@ const emptyValue = [ ]; export default function VisualEditor({ t, field, className, isDisabled, onChange, ...props }) { + console.log('plff', createBlockquotePlugin()) const plugins = createPlugins( [ createParagraphPlugin(), @@ -73,6 +75,7 @@ export default function VisualEditor({ t, field, className, isDisabled, onChange createListPlugin(), createLinkPlugin(), createBlockquotePlugin(), + createBlockquoteExtPlugin(), createSoftBreakPlugin({ options: { rules: [ diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js index 0b8f145b6f8c..08686f3adbbd 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js @@ -1,6 +1,7 @@ import React from 'react'; -import { useEditorRef, focusEditor, toggleNodeType } from '@udecode/plate-common'; +import { useEditorRef, focusEditor } from '@udecode/plate-common'; import { unwrapList } from '@udecode/plate-list'; +import { toggleWrapNodes } from '@udecode/slate-utils'; import { ELEMENT_BLOCKQUOTE } from '@udecode/plate-block-quote'; import ToolbarButton from './ToolbarButton'; @@ -10,7 +11,7 @@ function BlockquoteToolbarButton(props) { function handleClick() { unwrapList(editor); - toggleNodeType(editor, { activeType: ELEMENT_BLOCKQUOTE }); + toggleWrapNodes(editor, ELEMENT_BLOCKQUOTE); focusEditor(editor); } const pressed = false; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/createBlockquoteExitBreak.js b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/createBlockquoteExitBreak.js new file mode 100644 index 000000000000..d649e95c9765 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/createBlockquoteExitBreak.js @@ -0,0 +1,55 @@ +import { + createPluginFactory, + getBlockAbove, + isHotkey, + isAncestorEmpty, + unwrapNodes, + isFirstChild, +} from '@udecode/plate-common'; +import { ELEMENT_BLOCKQUOTE } from '@udecode/plate-block-quote'; + +export const KEY_BLOCKQUOTE_EXIT_BREAK = 'blockquoteExitBreakPlugin'; + +function isWithinBlockquote(editor, entry) { + const blockAbove = getBlockAbove(editor, { at: entry[1] }); + return blockAbove?.[0]?.type === ELEMENT_BLOCKQUOTE; +} + +function unwrap(editor) { + unwrapNodes(editor, { split: true, match: n => n.type === ELEMENT_BLOCKQUOTE }); + return true; +} + +function onKeyDownBlockquoteExitBreak(editor, { options: { rules } }) { + return event => { + if (event.defaultPrevented) return; + + const entry = getBlockAbove(editor); + if (!entry) return; + + rules.forEach(({ hotkey, isFirstParagraph }) => { + if ( + isHotkey(hotkey, event) && + isAncestorEmpty(editor, entry[0]) && + isWithinBlockquote(editor, entry) && + (!isFirstParagraph || isFirstChild(entry[1])) && + unwrap(editor) + ) { + event.preventDefault(); + event.stopPropagation(); + } + }); + }; +} + +const createBlockquoteExtPlugin = createPluginFactory({ + key: KEY_BLOCKQUOTE_EXIT_BREAK, + handlers: { + onKeyDown: onKeyDownBlockquoteExitBreak, + }, + options: { + rules: [{ hotkey: 'enter' }, { hotkey: 'backspace', isFirstParagraph: true }], + }, +}); + +export default createBlockquoteExtPlugin; From 181d6449441bfa978a676db7472a47e00e454924 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Thu, 4 Apr 2024 15:25:13 +0200 Subject: [PATCH 08/43] feat(richtext): use own querynode --- .../src/RichtextControl/VisualEditor.js | 3 +-- ...tBreak.js => createBlockquoteExtPlugin.js} | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) rename packages/decap-cms-widget-richtext/src/RichtextControl/plugins/{createBlockquoteExitBreak.js => createBlockquoteExtPlugin.js} (71%) diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index a74cadbee122..89e8cc0dfb85 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -38,7 +38,7 @@ import ListElement from './components/Element/ListElement'; import { markdownToSlate, slateToMarkdown } from '../serializers'; import LinkElement from './components/Element/LinkElement'; import BlockquoteElement from './components/Element/BlockquoteElement'; -import createBlockquoteExtPlugin from './plugins/createBlockquoteExitBreak'; +import createBlockquoteExtPlugin from './plugins/createBlockquoteExtPlugin'; function visualEditorStyles({ minimal }) { return ` @@ -64,7 +64,6 @@ const emptyValue = [ ]; export default function VisualEditor({ t, field, className, isDisabled, onChange, ...props }) { - console.log('plff', createBlockquotePlugin()) const plugins = createPlugins( [ createParagraphPlugin(), diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/createBlockquoteExitBreak.js b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/createBlockquoteExtPlugin.js similarity index 71% rename from packages/decap-cms-widget-richtext/src/RichtextControl/plugins/createBlockquoteExitBreak.js rename to packages/decap-cms-widget-richtext/src/RichtextControl/plugins/createBlockquoteExtPlugin.js index d649e95c9765..403d958e3128 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/createBlockquoteExitBreak.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/createBlockquoteExtPlugin.js @@ -5,6 +5,7 @@ import { isAncestorEmpty, unwrapNodes, isFirstChild, + isSelectionAtBlockStart, } from '@udecode/plate-common'; import { ELEMENT_BLOCKQUOTE } from '@udecode/plate-block-quote'; @@ -15,6 +16,14 @@ function isWithinBlockquote(editor, entry) { return blockAbove?.[0]?.type === ELEMENT_BLOCKQUOTE; } +function queryNode(editor, entry, { empty, first, start }) { + return ( + (!empty || isAncestorEmpty(editor, entry[0])) && + (!first || isFirstChild(entry[1])) && + (!start || isSelectionAtBlockStart(editor)) + ); +} + function unwrap(editor) { unwrapNodes(editor, { split: true, match: n => n.type === ELEMENT_BLOCKQUOTE }); return true; @@ -27,12 +36,11 @@ function onKeyDownBlockquoteExitBreak(editor, { options: { rules } }) { const entry = getBlockAbove(editor); if (!entry) return; - rules.forEach(({ hotkey, isFirstParagraph }) => { + rules.forEach(({ hotkey, query }) => { if ( isHotkey(hotkey, event) && - isAncestorEmpty(editor, entry[0]) && isWithinBlockquote(editor, entry) && - (!isFirstParagraph || isFirstChild(entry[1])) && + queryNode(editor, entry, query) && unwrap(editor) ) { event.preventDefault(); @@ -48,7 +56,10 @@ const createBlockquoteExtPlugin = createPluginFactory({ onKeyDown: onKeyDownBlockquoteExitBreak, }, options: { - rules: [{ hotkey: 'enter' }, { hotkey: 'backspace', isFirstParagraph: true }], + rules: [ + { hotkey: 'enter', query: { empty: true } }, + { hotkey: 'backspace', query: { first: true, start: true } }, + ], }, }); From c6ac95039926f9f0fff3e3b07b76c7570b244562 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Thu, 8 Aug 2024 10:10:56 +0200 Subject: [PATCH 09/43] feat(richtext): use getBlockAbove for heading state --- .../components/Toolbar/HeadingToolbarButton.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js index d211871266f2..8b0058402e69 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js @@ -1,9 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { - findNode, focusEditor, - isBlock, + getBlockAbove, isSelectionExpanded, toggleNodeType, useEditorRef, @@ -35,9 +34,7 @@ function HeadingToolbarButton({ disabled, isVisible, t }) { const value = useEditorSelector(editor => { if (!isSelectionExpanded(editor)) { - const entry = findNode(editor, { - match: n => isBlock(editor, n), - }); + const entry = getBlockAbove(editor) if (entry) { return entry[0].type; @@ -47,6 +44,8 @@ function HeadingToolbarButton({ disabled, isVisible, t }) { return ELEMENT_PARAGRAPH; }, []); + + function handleChange(optionKey) { unwrapList(editor); toggleNodeType(editor, { activeType: optionKey }); From 08c14c8b32066dc6b62af1b07bd0ae69c6cc1256 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Thu, 8 Aug 2024 10:11:24 +0200 Subject: [PATCH 10/43] feat(richtext): add blockquote toolbar button state --- .../components/Toolbar/BlockquoteToolbarButton.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js index 08686f3adbbd..101429018e47 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js @@ -1,5 +1,10 @@ import React from 'react'; -import { useEditorRef, focusEditor } from '@udecode/plate-common'; +import { + useEditorRef, + focusEditor, + useEditorSelector, + findNode, +} from '@udecode/plate-common'; import { unwrapList } from '@udecode/plate-list'; import { toggleWrapNodes } from '@udecode/slate-utils'; import { ELEMENT_BLOCKQUOTE } from '@udecode/plate-block-quote'; @@ -9,12 +14,14 @@ import ToolbarButton from './ToolbarButton'; function BlockquoteToolbarButton(props) { const editor = useEditorRef(); + const pressed = useEditorSelector(editor => !!findNode(editor, { match: { type: ELEMENT_BLOCKQUOTE } }), []); + function handleClick() { unwrapList(editor); toggleWrapNodes(editor, ELEMENT_BLOCKQUOTE); focusEditor(editor); } - const pressed = false; + return ; } From c0b12f9cd08ba99c7f1772019a93d4ce38fad482 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Thu, 8 Aug 2024 14:12:44 +0200 Subject: [PATCH 11/43] feat(richtext): add editor components to toolbar --- .../src/RichtextControl.js | 3 +- .../src/RichtextControl/VisualEditor.js | 67 ++++++++++++++++- .../Toolbar/EditorComponentsToolbarButton.js | 72 +++++++++++++++++++ .../components/Toolbar/Toolbar.js | 15 +++- 4 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/EditorComponentsToolbarButton.js diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl.js b/packages/decap-cms-widget-richtext/src/RichtextControl.js index da942321d207..1f8a43329733 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl.js @@ -19,13 +19,14 @@ export default class MarkdownControl extends React.Component { }; render() { - const { classNameWrapper, field, t, isDisabled, onChange, value } = this.props; + const { classNameWrapper, field, t, isDisabled, getEditorComponents, onChange, value } = this.props; const visualEditor = (
f.get('widget') === 'image'), + f => { + // merge `media_library` config + if (field.has('media_library')) { + f = f.set( + 'media_library', + field.get('media_library').mergeDeep(f.get('media_library')), + ); + } + // merge 'media_folder' + if (field.has('media_folder') && !f.has('media_folder')) { + f = f.set('media_folder', field.get('media_folder')); + } + // merge 'public_folder' + if (field.has('public_folder') && !f.has('public_folder')) { + f = f.set('public_folder', field.get('public_folder')); + } + return f; + }, + ); + } + } +} + const emptyValue = [ { id: '1', @@ -63,7 +96,27 @@ const emptyValue = [ }, ]; -export default function VisualEditor({ t, field, className, isDisabled, onChange, ...props }) { +export default function VisualEditor(props) { + + const { + t, + field, + className, + isDisabled, + onChange, + getEditorComponents, + } = props; + + let editorComponents = getEditorComponents(); + const codeBlockComponent = fromJS(editorComponents.find(({ type }) => type === 'code-block')); + + editorComponents = + codeBlockComponent || editorComponents.has('code-block') + ? editorComponents + : editorComponents.set('code-block', { label: 'Code Block', type: 'code-block' }); + + mergeMediaConfig(editorComponents, field); + const plugins = createPlugins( [ createParagraphPlugin(), @@ -148,6 +201,13 @@ export default function VisualEditor({ t, field, className, isDisabled, onChange console.log('handleToggleMode'); } + function handleInsertEditorComponent(a) { + console.log('handleInsertEditorComponent', a); + + + + } + function handleChange(value) { console.log('handleChange', value); const mdValue = slateToMarkdown(value, {}); @@ -172,9 +232,9 @@ export default function VisualEditor({ t, field, className, isDisabled, onChange onBlockClick={handleBlockClick} onLinkClick={handleLinkClick} onToggleMode={handleToggleMode} - plugins={null} buttons={[]} - editorComponents={[]} + editorComponents={editorComponents} + allowedEditorComponents={field.get('editor_components')} onAddAsset={() => false} getAsset={() => false} hasInline={() => false} @@ -183,6 +243,7 @@ export default function VisualEditor({ t, field, className, isDisabled, onChange hasListItems={() => false} isShowModeToggle={() => false} onChange={() => false} + onInsertEditorComponent={handleInsertEditorComponent} t={t} disabled={isDisabled} /> diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/EditorComponentsToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/EditorComponentsToolbarButton.js new file mode 100644 index 000000000000..84bdc0fed688 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/EditorComponentsToolbarButton.js @@ -0,0 +1,72 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from '@emotion/styled'; +import { Dropdown, DropdownButton, DropdownItem } from 'decap-cms-ui-default'; +import { List } from 'immutable'; +import ImmutablePropTypes from 'react-immutable-proptypes'; + +import ToolbarButton from './ToolbarButton'; + +const ToolbarDropdownWrapper = styled.div` + display: inline-block; + position: relative; +`; + +function EditorComponentsToolbarButton({ disabled, editorComponents, allowedEditorComponents, t, onChange }) { + function handleChange(optionKey) { + onChange(optionKey) + } + + const editorComponentOptions = editorComponents + ? editorComponents + .toList() + .filter(({ id }) => (allowedEditorComponents ? allowedEditorComponents.includes(id) : true)) + : List(); + + const showEditorComponents = editorComponentOptions.size >= 1; + + return ( + <> + {showEditorComponents && ( + + ( + + + + )} + > + {!disabled && + editorComponentOptions.map(option => ( + e.preventDefault()} + onClick={() => handleChange(option.id)} + /> + ))} + + + )} + + ); +} + +EditorComponentsToolbarButton.propTypes = { + editorComponents: ImmutablePropTypes.map, + allowedEditorComponents: ImmutablePropTypes.list, + onChange: PropTypes.func, + disabled: PropTypes.bool, + t: PropTypes.func.isRequired, +}; + +export default EditorComponentsToolbarButton; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js index 3bd7002d40a9..6ef68096ed4d 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js @@ -4,12 +4,14 @@ import styled from '@emotion/styled'; import { List } from 'immutable'; import { colors, transitions } from 'decap-cms-ui-default'; import { MARK_BOLD, MARK_CODE, MARK_ITALIC } from '@udecode/plate-basic-marks'; +import ImmutablePropTypes from 'react-immutable-proptypes'; import MarkToolbarButton from './MarkToolbarButton'; import HeadingToolbarButton from './HeadingToolbarButton'; import ListToolbarButton from './ListToolbarButton'; import LinkToolbarButton from './LinkToolbarButton'; import BlockquoteToolbarButton from './BlockquoteToolbarButton'; +import EditorComponentsToolbarButton from './EditorComponentsToolbarButton'; const ToolbarContainer = styled.div` position: relative; @@ -23,7 +25,7 @@ const ToolbarContainer = styled.div` `; function Toolbar(props) { - const { disabled, t } = props; + const { disabled, t, editorComponents, allowedEditorComponents, onInsertEditorComponent } = props; function isVisible(button) { const { buttons } = props; @@ -88,6 +90,14 @@ function Toolbar(props) { icon="list-numbered" disabled={disabled} /> +
); @@ -96,6 +106,9 @@ function Toolbar(props) { Toolbar.propTypes = { buttons: PropTypes.array, disabled: PropTypes.bool, + editorComponents: ImmutablePropTypes.map, + allowedEditorComponents: ImmutablePropTypes.list, + onInsertEditorComponent: PropTypes.func, t: PropTypes.func.isRequired, }; From 50092e0c5b293aad8c289bbb5b204117ad954688 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Wed, 23 Oct 2024 10:24:01 +0200 Subject: [PATCH 12/43] chore(eslimt): update eslint config to allow package.json exports property in dependencies --- .eslintrc.js | 1 + package.json | 1 + tsconfig.json | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 6753607da287..5acc26e1c1ae 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -64,6 +64,7 @@ module.exports = { node: { extensions: ['.js', '.jsx', '.ts', '.tsx'], }, + exports: {}, }, 'import/core-modules': [...packages, 'decap-cms-app/dist/esm'], }, diff --git a/package.json b/package.json index bd1c71a90b94..3e8888ffab72 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "cypress-plugin-tab": "^1.0.0", "dotenv": "^10.0.0", "eslint": "^8.12.0", + "eslint-import-resolver-exports": "^1.0.0-beta.5", "eslint-plugin-cypress": "^2.6.0", "eslint-plugin-import": "^2.18.2", "eslint-plugin-prettier": "^4.0.0", diff --git a/tsconfig.json b/tsconfig.json index e2aa43627a14..f953092fe550 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,8 +3,8 @@ "declaration": true, "jsx": "react", "target": "esnext", - "module": "esnext", - "moduleResolution": "node", + "module": "nodenext", + "moduleResolution": "nodenext", "esModuleInterop": true, "noEmit": true, "strict": true, From dee442dbf9bc59763902a79e82893069275fe871 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Wed, 23 Oct 2024 10:24:50 +0200 Subject: [PATCH 13/43] feat(richtext): upgrade plate to 39, add shortcodes --- dev-test/config.yml | 1 + package-lock.json | 582 +++++++++--------- .../decap-cms-widget-markdown/package.json | 6 +- .../MarkdownControl/components/VoidBlock.js | 1 + .../src/serializers/index.js | 3 + .../decap-cms-widget-richtext/package.json | 22 +- .../src/RichtextControl.js | 30 +- .../src/RichtextControl/VisualEditor.js | 204 +++--- .../src/RichtextControl/components/Editor.js | 2 +- .../components/Element/BlockquoteElement.js | 2 +- .../components/Element/HeadingElement.js | 2 +- .../components/Element/LinkElement.js | 4 +- .../components/Element/ListElement.js | 2 +- .../components/Element/ParagraphElement.js | 2 +- .../components/Element/ShortcodeElement.js | 100 +++ .../components/Leaf/CodeLeaf.js | 2 +- .../Toolbar/BlockquoteToolbarButton.js | 18 +- .../Toolbar/EditorComponentsToolbarButton.js | 51 +- .../Toolbar/HeadingToolbarButton.js | 18 +- .../components/Toolbar/LinkToolbarButton.js | 10 +- .../components/Toolbar/ListToolbarButton.js | 2 +- .../components/Toolbar/MarkToolbarButton.js | 2 +- .../components/Toolbar/Toolbar.js | 12 +- .../src/RichtextControl/editorContext.js | 16 + .../plugins/BlockquoteExtPlugin.js | 58 ++ .../plugins/ShortcodePlugin.js | 16 + .../plugins/createBlockquoteExtPlugin.js | 66 -- .../src/RichtextControl/withProps.js | 4 +- .../src/serializers/index.js | 13 +- 29 files changed, 701 insertions(+), 550 deletions(-) create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ShortcodeElement.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/editorContext.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/plugins/BlockquoteExtPlugin.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ShortcodePlugin.js delete mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/plugins/createBlockquoteExtPlugin.js diff --git a/dev-test/config.yml b/dev-test/config.yml index c3a0fd5e70f4..e5fc64c1c97d 100644 --- a/dev-test/config.yml +++ b/dev-test/config.yml @@ -52,6 +52,7 @@ collections: # A list of collections the CMS should be able to edit - { label: 'Body', name: 'body', widget: 'richtext', hint: 'Main content goes here.' } - { label: 'Body', name: 'bodyold', widget: 'markdown', hint: 'Main content goes here.' } + - { label: 'Body', name: 'bodyold2', widget: 'markdown', hint: 'Main content goes here 2.' } - name: 'restaurants' # Used in routes, ie.: /admin/collections/:slug/edit label: 'Restaurants' # Used in the UI diff --git a/package-lock.json b/package-lock.json index dcf4ef86b9e6..9766baa494cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,6 +76,7 @@ "cypress-plugin-tab": "^1.0.0", "dotenv": "^10.0.0", "eslint": "^8.12.0", + "eslint-import-resolver-exports": "^1.0.0-beta.5", "eslint-plugin-cypress": "^2.6.0", "eslint-plugin-import": "^2.18.2", "eslint-plugin-prettier": "^4.0.0", @@ -3070,17 +3071,17 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", - "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", "dependencies": { - "@floating-ui/utils": "^0.2.1" + "@floating-ui/utils": "^0.2.8" } }, "node_modules/@floating-ui/core/node_modules/@floating-ui/utils": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", - "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" }, "node_modules/@floating-ui/dom": { "version": "1.5.3", @@ -3091,13 +3092,13 @@ } }, "node_modules/@floating-ui/react": { - "version": "0.22.3", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.22.3.tgz", - "integrity": "sha512-RlF+7yU3/abTZcUez44IHoEH89yDHHonkYzZocynTWbl6J6MiMINMbyZSmSKdRKdadrC+MwQLdEexu++irvZhQ==", + "version": "0.26.25", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.25.tgz", + "integrity": "sha512-hZOmgN0NTOzOuZxI1oIrDu3Gcl8WViIkvPMpB4xdd4QD6xAMtwgwr3VPoiyH/bLtRcS1cDnhxLSD1NsMJmwh/A==", "dependencies": { - "@floating-ui/react-dom": "^1.3.0", - "aria-hidden": "^1.1.3", - "tabbable": "^6.0.1" + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=16.8.0", @@ -3105,28 +3106,21 @@ } }, "node_modules/@floating-ui/react-dom": { - "version": "2.0.2", - "dev": true, - "license": "MIT", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", "dependencies": { - "@floating-ui/dom": "^1.5.1" + "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, - "node_modules/@floating-ui/react/node_modules/@floating-ui/react-dom": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.3.0.tgz", - "integrity": "sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==", - "dependencies": { - "@floating-ui/dom": "^1.2.1" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } + "node_modules/@floating-ui/react/node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" }, "node_modules/@floating-ui/utils": { "version": "0.1.6", @@ -5123,6 +5117,7 @@ }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.0.1", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10" @@ -5435,6 +5430,7 @@ }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.13.10", @@ -7009,10 +7005,6 @@ "@types/node": "*" } }, - "node_modules/@types/is-hotkey": { - "version": "0.1.8", - "license": "MIT" - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.5", "dev": true, @@ -7077,6 +7069,7 @@ }, "node_modules/@types/lodash": { "version": "4.14.200", + "dev": true, "license": "MIT" }, "node_modules/@types/mdast": { @@ -7751,215 +7744,201 @@ } }, "node_modules/@udecode/plate-basic-marks": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-basic-marks/-/plate-basic-marks-31.0.0.tgz", - "integrity": "sha512-yV05ohuWk7ZcVxshLQIoqRbJTCbn8hANaMR98PuOMHFajr68/Qvdn9B5MuOzLqPHyOOQ4yVBdzPNcsG6l1DuAg==", + "version": "39.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-basic-marks/-/plate-basic-marks-39.0.0.tgz", + "integrity": "sha512-GtHFK1gwmhfnwl0Lf3xiRuNS832bNaelx5Sr/uzSVpH7Xo4p7Ssdxp+vc9LsiUrQcBQyuybvyYGbiY7i2o5DCA==", "peerDependencies": { - "@udecode/plate-common": ">=31.0.0", + "@udecode/plate-common": ">=39.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", - "slate": ">=0.94.0", + "slate": ">=0.103.0", "slate-history": ">=0.93.0", "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" + "slate-react": ">=0.108.0" } }, "node_modules/@udecode/plate-block-quote": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-block-quote/-/plate-block-quote-31.0.0.tgz", - "integrity": "sha512-82gWC4uXsYvkkmtz4/mvlgAx7s6FgkUP80ZVVMJ2O9p9C6HipJs6/fvs5VWR1L8P04+leRIqi5dknm6ZzW5Epg==", + "version": "39.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-block-quote/-/plate-block-quote-39.0.0.tgz", + "integrity": "sha512-5TxkIQFvYxX6CEOM0dtBnM/SX70kqXFHlz6ncEYC9hJnaNTpI6jiUqBfx5S2schVpFgtaxhi/0x17oTsFHsFMQ==", "peerDependencies": { - "@udecode/plate-common": ">=31.0.0", + "@udecode/plate-common": ">=39.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", - "slate": ">=0.94.0", + "slate": ">=0.103.0", "slate-history": ">=0.93.0", "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" + "slate-react": ">=0.108.0" } }, "node_modules/@udecode/plate-break": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-break/-/plate-break-31.0.0.tgz", - "integrity": "sha512-dt5btIRIAWVioh9/O/JX8X2UYThGw4/Aks3aRjWjRitwONmTwca9UzxKMj7W0li758Yvd2WPeBJNPIWGJ9cpYw==", + "version": "39.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-break/-/plate-break-39.0.0.tgz", + "integrity": "sha512-4H4p9zuGBgC/K5YC9Kywgfz1KhImz2WMZZmub4YzZMddOf3iVuhOT1+KfP1GDiEWVzBcKH2R6iIxJ6rqWDsyGw==", "peerDependencies": { - "@udecode/plate-common": ">=31.0.0", + "@udecode/plate-common": ">=39.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", - "slate": ">=0.94.0", + "slate": ">=0.103.0", "slate-history": ">=0.93.0", "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" + "slate-react": ">=0.108.0" } }, "node_modules/@udecode/plate-common": { - "version": "31.3.2", - "resolved": "https://registry.npmjs.org/@udecode/plate-common/-/plate-common-31.3.2.tgz", - "integrity": "sha512-yhfFoJUlX81gOur093uDXrZu8lflm43DpcRhjHtX0pE6MiwSWIGygTNdjyXEE/MWC4/mwdeU1k94ahCXkfiffw==", + "version": "39.1.8", + "resolved": "https://registry.npmjs.org/@udecode/plate-common/-/plate-common-39.1.8.tgz", + "integrity": "sha512-6KLikZg4vQybdNIZnjE3A3cTo6n0OdslmkbYJQNmnQZQtcR0k91qrRIgc7YNwJ0UnqMBXjUK9egX1y+ZRrW0Gg==", "dependencies": { - "@udecode/plate-core": "31.3.2", - "@udecode/plate-utils": "31.3.2", - "@udecode/react-utils": "31.0.0", - "@udecode/slate": "31.0.0", - "@udecode/slate-react": "31.0.0", - "@udecode/slate-utils": "31.3.2", - "@udecode/utils": "31.0.0" + "@udecode/plate-core": "39.1.4", + "@udecode/plate-utils": "39.1.8", + "@udecode/react-hotkeys": "37.0.0", + "@udecode/react-utils": "39.0.0", + "@udecode/slate": "38.0.4", + "@udecode/slate-react": "39.1.4", + "@udecode/slate-utils": "39.1.4", + "@udecode/utils": "37.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0", - "slate": ">=0.94.0", + "slate": ">=0.103.0", "slate-history": ">=0.93.0", "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" + "slate-react": ">=0.108.0" } }, - "node_modules/@udecode/plate-common/node_modules/@udecode/react-utils": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/react-utils/-/react-utils-31.0.0.tgz", - "integrity": "sha512-zvXVIOELvKeizFK9a7nCBGizH/tO7EFOl4N7YSL8fUrd6SZPIoHTNEZg5YKC3bcdB7LUoBno1BJOcWOZDHE5SA==", - "dependencies": { - "@radix-ui/react-slot": "^1.0.2", - "@udecode/utils": "31.0.0", - "clsx": "^1.2.1" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@udecode/plate-common/node_modules/@udecode/utils": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-31.0.0.tgz", - "integrity": "sha512-06JTl1UAm3mzLLAx8hdMUFw4XRQG727z9JoJ9PeBnmFb9q4Cg3DdmbFnhVJMrBPWlyOwoHtPrBjnanTFeiP36Q==" - }, "node_modules/@udecode/plate-core": { - "version": "31.3.2", - "resolved": "https://registry.npmjs.org/@udecode/plate-core/-/plate-core-31.3.2.tgz", - "integrity": "sha512-sBEB2vMu2KG4/KTBwyui1mBzORBm+tPg05p/mk+/Ihy/gBlxBpIyiuRUo3iRD9ZTm+sAJLZT5e3Vv8wWyu3Bfg==", - "dependencies": { - "@udecode/slate": "31.0.0", - "@udecode/slate-react": "31.0.0", - "@udecode/slate-utils": "31.3.2", - "@udecode/utils": "31.0.0", - "clsx": "^1.2.1", + "version": "39.1.4", + "resolved": "https://registry.npmjs.org/@udecode/plate-core/-/plate-core-39.1.4.tgz", + "integrity": "sha512-Q19mm4ilQjvGXo7Xb8K7JUo3GOvYcFyO+QiMtgLnrL32yjiqywNA17a80eKPa3HBNfjXZfSrUyDQ03WIoPa7iQ==", + "dependencies": { + "@udecode/react-hotkeys": "37.0.0", + "@udecode/react-utils": "39.0.0", + "@udecode/slate": "38.0.4", + "@udecode/slate-react": "39.1.4", + "@udecode/slate-utils": "39.1.4", + "@udecode/utils": "37.0.0", + "clsx": "^2.1.1", "is-hotkey": "^0.2.0", - "jotai": "^2.7.1", - "jotai-optics": "0.3.2", - "jotai-x": "^1.2.2", + "jotai": "~2.8.4", + "jotai-optics": "0.4.0", + "jotai-x": "1.2.4", "lodash": "^4.17.21", "nanoid": "^3.3.7", "optics-ts": "2.4.1", - "react-hotkeys-hook": "^4.5.0", - "use-deep-compare": "^1.2.1", - "zustand": "^4.5.2", - "zustand-x": "^3.0.2" + "use-deep-compare": "^1.3.0", + "zustand": "^4.5.5", + "zustand-x": "^3.0.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0", - "slate": ">=0.94.0", + "slate": ">=0.103.0", "slate-history": ">=0.93.0", "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" + "slate-react": ">=0.108.0" } }, - "node_modules/@udecode/plate-core/node_modules/@udecode/utils": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-31.0.0.tgz", - "integrity": "sha512-06JTl1UAm3mzLLAx8hdMUFw4XRQG727z9JoJ9PeBnmFb9q4Cg3DdmbFnhVJMrBPWlyOwoHtPrBjnanTFeiP36Q==" + "node_modules/@udecode/plate-core/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } }, "node_modules/@udecode/plate-floating": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-floating/-/plate-floating-31.0.0.tgz", - "integrity": "sha512-k1KZjpGCH+x/rDCSUZ1Kd4ttPc/35Xp/T+pmI2jYQ48dlorYQPJSuJxpFE/cpBp1g8G2lLh4xbH5BTYR1bypAQ==", + "version": "39.1.6", + "resolved": "https://registry.npmjs.org/@udecode/plate-floating/-/plate-floating-39.1.6.tgz", + "integrity": "sha512-oHy8Zfs5JMNkk1Slnv6BR+4LYQV0oFTWinfkJY7vSDs+dzdnHvbSfnW7/TFxt/yPMvK8CA5qCTckfSIt8kQKQg==", "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/react": "^0.22.3" + "@floating-ui/core": "^1.6.7", + "@floating-ui/react": "^0.26.23" }, "peerDependencies": { - "@udecode/plate-common": ">=31.0.0", + "@udecode/plate-common": ">=39.1.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", - "slate": ">=0.94.0", + "slate": ">=0.103.0", "slate-history": ">=0.93.0", "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" + "slate-react": ">=0.108.0" } }, "node_modules/@udecode/plate-heading": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-heading/-/plate-heading-31.0.0.tgz", - "integrity": "sha512-TA9hc1sydRiLykqfJ5FmdYkLNpVkMxY7u0YL1QVOTQLCXYPbJVNuQ1vUsFxsU3mmqGb/J5Xsu87kLRHSnRQR0A==", + "version": "39.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-heading/-/plate-heading-39.0.0.tgz", + "integrity": "sha512-J7No90ttd/2sXbbRnHZ9EgNR//A91UZ57bb15quYE+rlGSyuk1eANzMX5vbmgWMIQ8O53vSqWC12clo9sVaVZA==", "peerDependencies": { - "@udecode/plate-common": ">=31.0.0", + "@udecode/plate-common": ">=39.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", - "slate": ">=0.94.0", + "slate": ">=0.103.0", "slate-history": ">=0.93.0", "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" + "slate-react": ">=0.108.0" } }, "node_modules/@udecode/plate-link": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-link/-/plate-link-31.0.0.tgz", - "integrity": "sha512-607injR8Bf5Tek4LIjjcuPoDDFMq0i+3cuX1AlIsqbwqZH3gffQZ700OKhisy17bu8SD9aqAgiCfKKlHXM4cdA==", + "version": "39.1.9", + "resolved": "https://registry.npmjs.org/@udecode/plate-link/-/plate-link-39.1.9.tgz", + "integrity": "sha512-GzPUFsXjceZ6truQm6lCRI+2iW0SwdoA0P8sJL4VqxDlSULT//Y4+m8Sh5d6MpxHuDjOeBGdOIq2g2Qqt3vokA==", "dependencies": { - "@udecode/plate-floating": "31.0.0", - "@udecode/plate-normalizers": "31.0.0" + "@udecode/plate-floating": "39.1.6", + "@udecode/plate-normalizers": "39.0.0" }, "peerDependencies": { - "@udecode/plate-common": ">=31.0.0", + "@udecode/plate-common": ">=39.1.8", "react": ">=16.8.0", "react-dom": ">=16.8.0", - "slate": ">=0.94.0", + "slate": ">=0.103.0", "slate-history": ">=0.93.0", "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" + "slate-react": ">=0.108.0" } }, "node_modules/@udecode/plate-list": { - "version": "31.1.3", - "resolved": "https://registry.npmjs.org/@udecode/plate-list/-/plate-list-31.1.3.tgz", - "integrity": "sha512-TjD5JeKsuzsdSkepgjFMDcJsB6BHdowO/ppHLxUCge74RwPVF7U97HPc7oEtx+OLsrC13gkbpZvwLHfdCy9dDQ==", + "version": "39.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-list/-/plate-list-39.0.0.tgz", + "integrity": "sha512-dwtW9r6vZOF0hvGaCsj5haNSB9GX4/RhpBI8gyFlGbOpUPqHdD38VWnFaOjrTQISt3X5CrLSmQJFI9Z7QLyawQ==", "dependencies": { - "@udecode/plate-reset-node": "31.0.0", + "@udecode/plate-reset-node": "39.0.0", "lodash": "^4.17.21" }, "peerDependencies": { - "@udecode/plate-common": ">=31.0.0", + "@udecode/plate-common": ">=39.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", - "slate": ">=0.94.0", + "slate": ">=0.103.0", "slate-history": ">=0.93.0", "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" + "slate-react": ">=0.108.0" } }, "node_modules/@udecode/plate-normalizers": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-normalizers/-/plate-normalizers-31.0.0.tgz", - "integrity": "sha512-4HD39jOgv5Hf8sQqXtZdQLcWYHToXGjBPFU33pYHbEBmxEN9cd5ndxRTCnoOspI9HJMkpu0OzrhUfhLvoPDP4w==", + "version": "39.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-normalizers/-/plate-normalizers-39.0.0.tgz", + "integrity": "sha512-2awYYNcjbQovg0UUTMy6B6hTdva77BIyp2Ou8AlJbZqYHpN+Z/5JCNuRdkMBSDbDImRUsFo4I2jx0I9b6IG4fA==", "dependencies": { "lodash": "^4.17.21" }, "peerDependencies": { - "@udecode/plate-common": ">=31.0.0", + "@udecode/plate-common": ">=39.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", - "slate": ">=0.94.0", + "slate": ">=0.103.0", "slate-history": ">=0.93.0", "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" + "slate-react": ">=0.108.0" } }, "node_modules/@udecode/plate-paragraph": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-paragraph/-/plate-paragraph-31.0.0.tgz", - "integrity": "sha512-uuaksPfDhK5ShVhjZ0pbXlUgy5nKKDkXzrAfDEZJzwF1R2N0HTy2WcmNJFm+aN8ZUFbZ4MHBuUUldZc/aLuCqw==", + "version": "36.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-paragraph/-/plate-paragraph-36.0.0.tgz", + "integrity": "sha512-Zbm76VygSfj4hkP1kfjwaYZisvmF3XP79f1uVTieQfcx/16s+Ln4BCVasCCbS+PO94yjsujW+ww05bUzGqRxpA==", "peerDependencies": { - "@udecode/plate-common": ">=31.0.0", + "@udecode/plate-common": ">=36.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", "slate": ">=0.94.0", @@ -7969,146 +7948,173 @@ } }, "node_modules/@udecode/plate-reset-node": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-reset-node/-/plate-reset-node-31.0.0.tgz", - "integrity": "sha512-pIdexCNsJx21UHeHrDxeOTS2w0NfthCD5klZGiiKBkU+sd65btmyY1fEQmlVaaeqWKzYCXai2ctSlRAycBV7wA==", + "version": "39.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-reset-node/-/plate-reset-node-39.0.0.tgz", + "integrity": "sha512-x1utEQIzDaQRkAAuG4GJvm7pUpiPqS70K6no/2dCV3iXCQDi2eeeZsIB6fBk30yymFEXx9POPYNYRoujtH+Cbw==", "peerDependencies": { - "@udecode/plate-common": ">=31.0.0", + "@udecode/plate-common": ">=39.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", - "slate": ">=0.94.0", + "slate": ">=0.103.0", "slate-history": ">=0.93.0", "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" + "slate-react": ">=0.108.0" } }, "node_modules/@udecode/plate-trailing-block": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-trailing-block/-/plate-trailing-block-31.0.0.tgz", - "integrity": "sha512-vW4BEP3rp9wbEf1ntDcKcAN95ve6ZbO7lLgh9lUBXMVLEvbfcruamXOdMPRYAosokdPF/U+HeNqKtPhkngAMdw==", + "version": "39.0.0", + "resolved": "https://registry.npmjs.org/@udecode/plate-trailing-block/-/plate-trailing-block-39.0.0.tgz", + "integrity": "sha512-OXBzZ9pGFhJeSWtKrUIffMxsUV00pVOxPL4YRmn8i4nJxEBlLEgcfPK6vfU2WUczgBmYiM++W8y4rZOWBsdyqg==", "peerDependencies": { - "@udecode/plate-common": ">=31.0.0", + "@udecode/plate-common": ">=39.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0", - "slate": ">=0.94.0", + "slate": ">=0.103.0", "slate-history": ">=0.93.0", "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" + "slate-react": ">=0.108.0" } }, "node_modules/@udecode/plate-utils": { - "version": "31.3.2", - "resolved": "https://registry.npmjs.org/@udecode/plate-utils/-/plate-utils-31.3.2.tgz", - "integrity": "sha512-IedyGPqF/yrSjc1ODBSM5xZXC5yPORyqq5vFlk+uNV4khPMDer2PMGnv5se6yVQhSMdAN81gLtlFKONDKIjAjQ==", - "dependencies": { - "@udecode/plate-core": "31.3.2", - "@udecode/react-utils": "31.0.0", - "@udecode/slate": "31.0.0", - "@udecode/slate-react": "31.0.0", - "@udecode/slate-utils": "31.3.2", - "@udecode/utils": "31.0.0", - "clsx": "^1.2.1", + "version": "39.1.8", + "resolved": "https://registry.npmjs.org/@udecode/plate-utils/-/plate-utils-39.1.8.tgz", + "integrity": "sha512-9apGZRXgrWz9ke9TSU9rJers73Ue4lIfzq7pa61wdbZ1VqHh5kQ9+PpIVokZT26q5IjJIz7CuSQ8Vog0u5x7dQ==", + "dependencies": { + "@udecode/plate-core": "39.1.4", + "@udecode/react-utils": "39.0.0", + "@udecode/slate": "38.0.4", + "@udecode/slate-react": "39.1.4", + "@udecode/slate-utils": "39.1.4", + "@udecode/utils": "37.0.0", + "clsx": "^2.1.1", "lodash": "^4.17.21" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0", - "slate": ">=0.94.0", + "slate": ">=0.103.0", "slate-history": ">=0.93.0", "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" + "slate-react": ">=0.110.0" + } + }, + "node_modules/@udecode/plate-utils/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" } }, - "node_modules/@udecode/plate-utils/node_modules/@udecode/react-utils": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/react-utils/-/react-utils-31.0.0.tgz", - "integrity": "sha512-zvXVIOELvKeizFK9a7nCBGizH/tO7EFOl4N7YSL8fUrd6SZPIoHTNEZg5YKC3bcdB7LUoBno1BJOcWOZDHE5SA==", + "node_modules/@udecode/react-hotkeys": { + "version": "37.0.0", + "resolved": "https://registry.npmjs.org/@udecode/react-hotkeys/-/react-hotkeys-37.0.0.tgz", + "integrity": "sha512-3ZV5LiaTnKyhXwN6U0NE2cofNsNN2IPMkNCDntbSIIRLYmI+o6LRkDwAucSNh/BIdNXfvxscsR04RYyIwjGbJw==", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@udecode/react-utils": { + "version": "39.0.0", + "resolved": "https://registry.npmjs.org/@udecode/react-utils/-/react-utils-39.0.0.tgz", + "integrity": "sha512-EoX6T7VmQe9bcR2bIqoobcsX66vo45XKt26rY4eJPWjaTys3yGdyD2iMDy/mEYFFh8ZOUC1V+sNw+XBwQOgyCw==", "dependencies": { - "@radix-ui/react-slot": "^1.0.2", - "@udecode/utils": "31.0.0", - "clsx": "^1.2.1" + "@radix-ui/react-slot": "^1.1.0", + "@udecode/utils": "37.0.0", + "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, - "node_modules/@udecode/plate-utils/node_modules/@udecode/utils": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-31.0.0.tgz", - "integrity": "sha512-06JTl1UAm3mzLLAx8hdMUFw4XRQG727z9JoJ9PeBnmFb9q4Cg3DdmbFnhVJMrBPWlyOwoHtPrBjnanTFeiP36Q==" + "node_modules/@udecode/react-utils/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } }, - "node_modules/@udecode/slate": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/slate/-/slate-31.0.0.tgz", - "integrity": "sha512-VK84em/ZQYgu2PnXBLG8ON47n3DAZZL//yA3oWs4J3hTg92UTXpizNZiwk9iA+mb+xcRomwuWwpTUCyE8VI3rQ==", + "node_modules/@udecode/react-utils/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", "dependencies": { - "@udecode/utils": "31.0.0" + "@radix-ui/react-compose-refs": "1.1.0" }, "peerDependencies": { - "slate": ">=0.94.0", - "slate-history": ">=0.93.0" + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@udecode/slate-react": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/slate-react/-/slate-react-31.0.0.tgz", - "integrity": "sha512-+xYLSZO7u9KrJoCY88udFVT29fp4waX0mFM+gmhd10Kfb/l5xB5yt5248PCqbkehwb93TBntODXtbQzmwNFkag==", + "node_modules/@udecode/react-utils/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@udecode/slate": { + "version": "38.0.4", + "resolved": "https://registry.npmjs.org/@udecode/slate/-/slate-38.0.4.tgz", + "integrity": "sha512-PlnavxtTd4xIqC/HdNl/nebMUogBjXgq1WyiH1mCXBNzMRuTpss7ByhXOr/SO67h6CKpaeGc1kf3dqZsHQWHZw==", "dependencies": { - "@udecode/react-utils": "31.0.0", - "@udecode/slate": "31.0.0", - "@udecode/utils": "31.0.0" + "@udecode/utils": "37.0.0" }, "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-react": ">=0.99.0" + "slate": ">=0.103.0", + "slate-history": ">=0.93.0" } }, - "node_modules/@udecode/slate-react/node_modules/@udecode/react-utils": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/react-utils/-/react-utils-31.0.0.tgz", - "integrity": "sha512-zvXVIOELvKeizFK9a7nCBGizH/tO7EFOl4N7YSL8fUrd6SZPIoHTNEZg5YKC3bcdB7LUoBno1BJOcWOZDHE5SA==", + "node_modules/@udecode/slate-react": { + "version": "39.1.4", + "resolved": "https://registry.npmjs.org/@udecode/slate-react/-/slate-react-39.1.4.tgz", + "integrity": "sha512-jM8hbaYonRJpXJmdmrMkzKuClkJ5f5lHf4tDnkmgqi2THXZ9eM4D7SD7MSxQeKt20klQA+CeNKfavaSobXQlJg==", "dependencies": { - "@radix-ui/react-slot": "^1.0.2", - "@udecode/utils": "31.0.0", - "clsx": "^1.2.1" + "@udecode/react-utils": "39.0.0", + "@udecode/slate": "38.0.4", + "@udecode/utils": "37.0.0" }, "peerDependencies": { "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "react-dom": ">=16.8.0", + "slate": ">=0.103.0", + "slate-history": ">=0.93.0", + "slate-react": ">=0.108.0" } }, - "node_modules/@udecode/slate-react/node_modules/@udecode/utils": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-31.0.0.tgz", - "integrity": "sha512-06JTl1UAm3mzLLAx8hdMUFw4XRQG727z9JoJ9PeBnmFb9q4Cg3DdmbFnhVJMrBPWlyOwoHtPrBjnanTFeiP36Q==" - }, "node_modules/@udecode/slate-utils": { - "version": "31.3.2", - "resolved": "https://registry.npmjs.org/@udecode/slate-utils/-/slate-utils-31.3.2.tgz", - "integrity": "sha512-ziQN60VItE9GHE7B8+sBnXFJ3P8bVJhfYA0TiwBtjRSoyUWIglUwCbFSBP+QsHmsNp8m+YztPavK6djjczD30Q==", + "version": "39.1.4", + "resolved": "https://registry.npmjs.org/@udecode/slate-utils/-/slate-utils-39.1.4.tgz", + "integrity": "sha512-OehFzAcT1FLPI2ZleUK9Y8Tv/LL1b5B3MIDppNqXlzzJp9F5dFzPEx9/1Ye7MSZfBusBZeqYUNtRMjcKFlExUQ==", "dependencies": { - "@udecode/slate": "31.0.0", - "@udecode/utils": "31.0.0", + "@udecode/slate": "38.0.4", + "@udecode/utils": "37.0.0", "lodash": "^4.17.21" }, "peerDependencies": { - "slate": ">=0.94.0", + "slate": ">=0.103.0", "slate-history": ">=0.93.0" } }, - "node_modules/@udecode/slate-utils/node_modules/@udecode/utils": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-31.0.0.tgz", - "integrity": "sha512-06JTl1UAm3mzLLAx8hdMUFw4XRQG727z9JoJ9PeBnmFb9q4Cg3DdmbFnhVJMrBPWlyOwoHtPrBjnanTFeiP36Q==" - }, - "node_modules/@udecode/slate/node_modules/@udecode/utils": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-31.0.0.tgz", - "integrity": "sha512-06JTl1UAm3mzLLAx8hdMUFw4XRQG727z9JoJ9PeBnmFb9q4Cg3DdmbFnhVJMrBPWlyOwoHtPrBjnanTFeiP36Q==" + "node_modules/@udecode/utils": { + "version": "37.0.0", + "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-37.0.0.tgz", + "integrity": "sha512-30ixi2pznIXyIqpFocX+X5Sj38js+wZ0RLY14eZv1C1zwWo5BxSuJfzpGQTvGcLPJnij019tEpmGH61QdDxtrQ==" }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", @@ -8913,6 +8919,7 @@ }, "node_modules/aria-hidden": { "version": "1.2.3", + "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -14049,6 +14056,28 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-import-resolver-exports": { + "version": "1.0.0-beta.5", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-exports/-/eslint-import-resolver-exports-1.0.0-beta.5.tgz", + "integrity": "sha512-o6t0w7muUpXr7MkUVzD5igQoDfAQvTmcPp8HEAJdNF8eOuAO+yn6I/TTyMxz9ecCwzX7e02vzlkHURoScUuidg==", + "dev": true, + "dependencies": { + "resolve.exports": "^2.0.0" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, + "node_modules/eslint-import-resolver-exports/node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "dev": true, @@ -18269,9 +18298,9 @@ } }, "node_modules/jotai": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.7.2.tgz", - "integrity": "sha512-6Ft5kpNu8p93Ssf1Faoza3hYQZRIYp7rioK8MwTTFnbQKwUyZElwquPwl1h6U0uo9hC0jr+ghO3gcSjc6P35/Q==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.8.4.tgz", + "integrity": "sha512-f6jwjhBJcDtpeauT2xH01gnqadKEySwwt1qNBLvAXcnojkmb76EdqRt05Ym8IamfHGAQz2qMKAwftnyjeSoHAA==", "engines": { "node": ">=12.20.0" }, @@ -18289,18 +18318,18 @@ } }, "node_modules/jotai-optics": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/jotai-optics/-/jotai-optics-0.3.2.tgz", - "integrity": "sha512-RH6SvqU5hmkVqnHmaqf9zBXvIAs4jLxkDHS4fr5ljuBKHs8+HQ02v+9hX7ahTppxx6dUb0GGUE80jQKJ0kFTLw==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/jotai-optics/-/jotai-optics-0.4.0.tgz", + "integrity": "sha512-osbEt9AgS55hC4YTZDew2urXKZkaiLmLqkTS/wfW5/l0ib8bmmQ7kBXSFaosV6jDDWSp00IipITcJARFHdp42g==", "peerDependencies": { - "jotai": ">=1.11.0", - "optics-ts": "*" + "jotai": ">=2.0.0", + "optics-ts": ">=2.0.0" } }, "node_modules/jotai-x": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/jotai-x/-/jotai-x-1.2.2.tgz", - "integrity": "sha512-HaFl3O4aKdBdeTyuzzcvnBWvicXkxl0DBINsqasqWrL7mZov4AAuXUSAsAY817UDwMe1+k77uBazUCFlaiyU3A==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/jotai-x/-/jotai-x-1.2.4.tgz", + "integrity": "sha512-FyLrAR/ZDtmaWgif4cNRuJvMam/RSFv+B11/p4T427ws/T+8WhZzwmULwNogG6ZbZq+v1XpH6f9aN1lYqY5dLg==", "peerDependencies": { "@types/react": ">=17.0.0", "jotai": ">=2.0.0", @@ -27338,15 +27367,6 @@ "react-dom": ">= 16.3" } }, - "node_modules/react-hotkeys-hook": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.5.0.tgz", - "integrity": "sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==", - "peerDependencies": { - "react": ">=16.8.1", - "react-dom": ">=16.8.1" - } - }, "node_modules/react-immutable-proptypes": { "version": "2.2.0", "license": "MIT", @@ -29552,9 +29572,9 @@ } }, "node_modules/slate": { - "version": "0.102.0", - "resolved": "https://registry.npmjs.org/slate/-/slate-0.102.0.tgz", - "integrity": "sha512-RT+tHgqOyZVB1oFV9Pv99ajwh4OUCN9p28QWdnDTIzaN/kZxMsHeQN39UNAgtkZTVVVygFqeg7/R2jiptCvfyA==", + "version": "0.110.2", + "resolved": "https://registry.npmjs.org/slate/-/slate-0.110.2.tgz", + "integrity": "sha512-4xGULnyMCiEQ0Ml7JAC1A6HVE6MNpPJU7Eq4cXh1LxlrR0dFXC3XC+rNfQtUJ7chHoPkws57x7DDiWiZAt+PBA==", "dependencies": { "immer": "^10.0.3", "is-plain-object": "^5.0.0", @@ -29602,13 +29622,11 @@ } }, "node_modules/slate-react": { - "version": "0.102.0", - "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.102.0.tgz", - "integrity": "sha512-SAcFsK5qaOxXjm0hr/t2pvIxfRv6HJGzmWkG58TdH4LdJCsgKS1n6hQOakHPlRVCwPgwvngB6R+t3pPjv8MqwA==", + "version": "0.110.2", + "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.110.2.tgz", + "integrity": "sha512-J5M4FHFAXmHWdz3wCixhFZcO3/hX6u1VOM6zSlIs7g43hGp0o9woo4zcCm1xgGB8m/BWYucQCq2n2t3g4PIjug==", "dependencies": { "@juggle/resize-observer": "^3.4.0", - "@types/is-hotkey": "^0.1.8", - "@types/lodash": "^4.14.200", "direction": "^1.0.4", "is-hotkey": "^0.2.0", "is-plain-object": "^5.0.0", @@ -32398,9 +32416,9 @@ } }, "node_modules/use-deep-compare": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/use-deep-compare/-/use-deep-compare-1.2.1.tgz", - "integrity": "sha512-JTnOZAr0fq1ix6CQ4XANoWIh03xAiMFlP/lVAYDdAOZwur6nqBSdATn1/Q9PLIGIW+C7xmFZBCcaA4KLDcQJtg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-deep-compare/-/use-deep-compare-1.3.0.tgz", + "integrity": "sha512-94iG+dEdEP/Sl3WWde+w9StIunlV8Dgj+vkt5wTwMoFQLaijiEZSXXy8KtcStpmEDtIptRJiNeD4ACTtVvnIKA==", "dependencies": { "dequal": "2.0.3" }, @@ -32469,9 +32487,9 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } @@ -33758,11 +33776,11 @@ "license": "0BSD" }, "node_modules/zustand": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", - "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.5.tgz", + "integrity": "sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q==", "dependencies": { - "use-sync-external-store": "1.2.0" + "use-sync-external-store": "1.2.2" }, "engines": { "node": ">=12.7.0" @@ -33785,9 +33803,9 @@ } }, "node_modules/zustand-x": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/zustand-x/-/zustand-x-3.0.2.tgz", - "integrity": "sha512-tb4qMWbmgrWEdemb+LlrJiHI1ZMxwlQNz7jDHN5iA/vmU8xlpAX80MQZ2FNLP2KejBFEnsA1RWRAO/0D5O0rPw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/zustand-x/-/zustand-x-3.0.4.tgz", + "integrity": "sha512-dVD8WUEpR/0mMdLah9j8i+r6PMAq9Ii2u+BX/9Bn4MHRt8sSnRQ90YMUlTVonZYAHGb2UHZwPpE2gMb8GtYDDw==", "dependencies": { "immer": "^10.0.3", "lodash.mapvalues": "^4.6.0", @@ -33798,9 +33816,9 @@ } }, "node_modules/zustand-x/node_modules/immer": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.4.tgz", - "integrity": "sha512-cuBuGK40P/sk5IzWa9QPUaAdvPHjkk1c+xYsd9oZw+YQQEV+10G0P5uMpGctZZKnyQ+ibRO08bD25nWLmYi2pw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -34416,12 +34434,12 @@ "remark-slate": "^1.8.6", "remark-slate-transformer": "^0.7.4", "remark-stringify": "^6.0.4", - "slate": "^0.102.0", + "slate": "^0.110.2", "slate-base64-serializer": "^0.2.107", "slate-history": "^0.100.0", "slate-hyperscript": "^0.100.0", "slate-plain-serializer": "^0.7.1", - "slate-react": "^0.102.0", + "slate-react": "^0.110.2", "slate-soft-break": "^0.9.0", "unified": "^9.2.0", "unist-builder": "^1.0.3", @@ -34585,21 +34603,21 @@ "version": "3.1.0", "license": "MIT", "dependencies": { - "@udecode/plate-basic-marks": "^31.0.0", - "@udecode/plate-block-quote": "^31.0.0", - "@udecode/plate-break": "^31.0.0", - "@udecode/plate-common": "^31.0.0", - "@udecode/plate-heading": "^31.0.0", - "@udecode/plate-link": "^31.0.0", - "@udecode/plate-list": "^31.1.3", - "@udecode/plate-paragraph": "^31.0.0", - "@udecode/plate-trailing-block": "^31.0.0", + "@udecode/plate-basic-marks": "^39.0.0", + "@udecode/plate-block-quote": "^39.0.0", + "@udecode/plate-break": "^39.0.0", + "@udecode/plate-common": "^39.1.8", + "@udecode/plate-heading": "^39.0.0", + "@udecode/plate-link": "^39.1.9", + "@udecode/plate-list": "^39.0.0", + "@udecode/plate-paragraph": "^36.0.0", + "@udecode/plate-trailing-block": "^39.0.0", "class-variance-authority": "^0.7.0", "lucide-react": "^0.331.0", - "slate": "^0.102.0", + "slate": "^0.110.2", "slate-history": "^0.100.0", "slate-hyperscript": "^0.100.0", - "slate-react": "^0.102.0", + "slate-react": "^0.110.2", "unified": "^9.0.0" }, "peerDependencies": { diff --git a/packages/decap-cms-widget-markdown/package.json b/packages/decap-cms-widget-markdown/package.json index 6685cd84bc80..4fa27b0ed4d4 100644 --- a/packages/decap-cms-widget-markdown/package.json +++ b/packages/decap-cms-widget-markdown/package.json @@ -34,12 +34,12 @@ "remark-slate": "^1.8.6", "remark-slate-transformer": "^0.7.4", "remark-stringify": "^6.0.4", - "slate": "^0.102.0", - "slate-hyperscript": "^0.100.0", + "slate": "^0.110.2", "slate-base64-serializer": "^0.2.107", "slate-history": "^0.100.0", + "slate-hyperscript": "^0.100.0", "slate-plain-serializer": "^0.7.1", - "slate-react": "^0.102.0", + "slate-react": "^0.110.2", "slate-soft-break": "^0.9.0", "unified": "^9.2.0", "unist-builder": "^1.0.3", diff --git a/packages/decap-cms-widget-markdown/src/MarkdownControl/components/VoidBlock.js b/packages/decap-cms-widget-markdown/src/MarkdownControl/components/VoidBlock.js index ad2b0a7b9ef0..52aaaa01cab7 100644 --- a/packages/decap-cms-widget-markdown/src/MarkdownControl/components/VoidBlock.js +++ b/packages/decap-cms-widget-markdown/src/MarkdownControl/components/VoidBlock.js @@ -42,6 +42,7 @@ function VoidBlock({ attributes, children, element }) { insertAtPath([...path.slice(0, -1), path[path.length - 1] + 1]); } + const insertBefore = path[0] === 0; const nextElement = editor.children[path[0] + 1]; const insertAfter = path[0] === editor.children.length - 1 || editor.isVoid(nextElement); diff --git a/packages/decap-cms-widget-markdown/src/serializers/index.js b/packages/decap-cms-widget-markdown/src/serializers/index.js index b6e1a85de359..b31ae4ebc4f8 100644 --- a/packages/decap-cms-widget-markdown/src/serializers/index.js +++ b/packages/decap-cms-widget-markdown/src/serializers/index.js @@ -220,7 +220,10 @@ export function markdownToSlate(markdown, { voidCodeBlock, remarkPlugins = [] } * trees. */ export function slateToMarkdown(raw, { voidCodeBlock, remarkPlugins = [] } = {}) { + console.log('old raw', raw); const mdast = slateToRemark(raw, { voidCodeBlock }); + console.log('old mdast', mdast); const markdown = remarkToMarkdown(mdast, remarkPlugins); + console.log('old md', markdown); return markdown; } diff --git a/packages/decap-cms-widget-richtext/package.json b/packages/decap-cms-widget-richtext/package.json index 8a1fe45b8b36..3bb4d42bb01e 100644 --- a/packages/decap-cms-widget-richtext/package.json +++ b/packages/decap-cms-widget-richtext/package.json @@ -21,21 +21,21 @@ "build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward --copy-files --extensions \".js,.jsx,.ts,.tsx\"" }, "dependencies": { - "@udecode/plate-common": "^31.0.0", - "@udecode/plate-basic-marks": "^31.0.0", - "@udecode/plate-block-quote": "^31.0.0", - "@udecode/plate-break": "^31.0.0", - "@udecode/plate-heading": "^31.0.0", - "@udecode/plate-link": "^31.0.0", - "@udecode/plate-list": "^31.1.3", - "@udecode/plate-paragraph": "^31.0.0", - "@udecode/plate-trailing-block": "^31.0.0", + "@udecode/plate-basic-marks": "^39.0.0", + "@udecode/plate-block-quote": "^39.0.0", + "@udecode/plate-break": "^39.0.0", + "@udecode/plate-common": "^39.1.8", + "@udecode/plate-heading": "^39.0.0", + "@udecode/plate-link": "^39.1.9", + "@udecode/plate-list": "^39.0.0", + "@udecode/plate-paragraph": "^36.0.0", + "@udecode/plate-trailing-block": "^39.0.0", "class-variance-authority": "^0.7.0", "lucide-react": "^0.331.0", - "slate": "^0.102.0", + "slate": "^0.110.2", "slate-history": "^0.100.0", "slate-hyperscript": "^0.100.0", - "slate-react": "^0.102.0", + "slate-react": "^0.110.2", "unified": "^9.0.0" }, "peerDependencies": { diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl.js b/packages/decap-cms-widget-richtext/src/RichtextControl.js index 1f8a43329733..d9703a9c08bb 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import VisualEditor from './RichtextControl/VisualEditor'; +import { EditorProvider } from './RichtextControl/editorContext'; export default class MarkdownControl extends React.Component { static propTypes = { @@ -18,20 +19,25 @@ export default class MarkdownControl extends React.Component { isDisabled: PropTypes.bool, }; + render() { - const { classNameWrapper, field, t, isDisabled, getEditorComponents, onChange, value } = this.props; + const { classNameWrapper, field, t, isDisabled, getEditorComponents, editorControl, onChange, value } = + this.props; + const visualEditor = ( -
- -
+ +
+ +
+
); return visualEditor; } diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index 214d5565ef40..18baf33d175f 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -1,45 +1,31 @@ import React from 'react'; -import { createPlugins, Plate, PlateLeaf } from '@udecode/plate-common'; -import { createParagraphPlugin, ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph'; -import { - createBoldPlugin, - MARK_BOLD, - createItalicPlugin, - MARK_ITALIC, - createCodePlugin, - MARK_CODE, -} from '@udecode/plate-basic-marks'; -import { - createHeadingPlugin, - ELEMENT_H1, - ELEMENT_H2, - ELEMENT_H3, - ELEMENT_H4, - ELEMENT_H5, - ELEMENT_H6, - KEYS_HEADING, -} from '@udecode/plate-heading'; -import { createSoftBreakPlugin, createExitBreakPlugin } from '@udecode/plate-break'; -import { createListPlugin, ELEMENT_UL, ELEMENT_OL, ELEMENT_LI } from '@udecode/plate-list'; -import { createLinkPlugin, ELEMENT_LINK } from '@udecode/plate-link'; -import { createBlockquotePlugin, ELEMENT_BLOCKQUOTE } from '@udecode/plate-block-quote'; -import { createTrailingBlockPlugin } from '@udecode/plate-trailing-block'; +import { usePlateEditor, Plate, ParagraphPlugin, PlateLeaf } from '@udecode/plate-common/react'; +import { BoldPlugin, ItalicPlugin, CodePlugin } from '@udecode/plate-basic-marks/react'; +import { HeadingPlugin } from '@udecode/plate-heading/react'; +import { HEADING_KEYS } from '@udecode/plate-heading'; +import { SoftBreakPlugin, ExitBreakPlugin } from '@udecode/plate-break/react'; +import { ListPlugin } from '@udecode/plate-list/react'; +import { LinkPlugin } from '@udecode/plate-link/react'; +import { BlockquotePlugin } from '@udecode/plate-block-quote/react'; +import { TrailingBlockPlugin } from '@udecode/plate-trailing-block'; import { ClassNames } from '@emotion/react'; import { fonts, lengths, zIndex } from 'decap-cms-ui-default'; import { fromJS } from 'immutable'; import { editorStyleVars } from '../styles'; -import withProps from './withProps'; +import { markdownToSlate, slateToMarkdown } from '../serializers'; import Editor from './components/Editor'; import Toolbar from './components/Toolbar'; -import CodeLeaf from './components/Leaf/CodeLeaf'; import ParagraphElement from './components/Element/ParagraphElement'; +import withProps from './withProps'; +import CodeLeaf from './components/Leaf/CodeLeaf'; import HeadingElement from './components/Element/HeadingElement'; import ListElement from './components/Element/ListElement'; -import { markdownToSlate, slateToMarkdown } from '../serializers'; -import LinkElement from './components/Element/LinkElement'; import BlockquoteElement from './components/Element/BlockquoteElement'; -import createBlockquoteExtPlugin from './plugins/createBlockquoteExtPlugin'; +import LinkElement from './components/Element/LinkElement'; +import BlockquoteExtPlugin from './plugins/BlockquoteExtPlugin'; +import ShortcodePlugin from './plugins/ShortcodePlugin'; +// import ShortcodeElement from './components/Element/ShortcodeElement'; function visualEditorStyles({ minimal }) { return ` @@ -91,21 +77,13 @@ function mergeMediaConfig(editorComponents, field) { const emptyValue = [ { id: '1', - type: 'p', + type: ParagraphPlugin.key, children: [{ text: '' }], }, ]; export default function VisualEditor(props) { - - const { - t, - field, - className, - isDisabled, - onChange, - getEditorComponents, - } = props; + const { t, field, className, isDisabled, onChange, getEditorComponents } = props; let editorComponents = getEditorComponents(); const codeBlockComponent = fromJS(editorComponents.find(({ type }) => type === 'code-block')); @@ -117,31 +95,74 @@ export default function VisualEditor(props) { mergeMediaConfig(editorComponents, field); - const plugins = createPlugins( - [ - createParagraphPlugin(), - createHeadingPlugin(), - createBoldPlugin(), - createItalicPlugin(), - createCodePlugin(), - createListPlugin(), - createLinkPlugin(), - createBlockquotePlugin(), - createBlockquoteExtPlugin(), - createSoftBreakPlugin({ - options: { - rules: [ - { hotkey: 'shift+enter' }, - { - hotkey: 'enter', - query: { - allow: [ELEMENT_BLOCKQUOTE], - }, + function handleBlockClick() { + console.log('handleBlockClick'); + } + + function handleLinkClick() { + console.log('handleLinkClick'); + } + + function handleToggleMode() { + console.log('handleToggleMode'); + } + + function handleChange({ value }) { + console.log('handleChange', value); + const mdValue = slateToMarkdown(value, {}, editorComponents); + onChange(mdValue); + } + + const initialValue = props.value ? markdownToSlate(props.value, {}) : emptyValue; + + console.log('pakaaka', ParagraphPlugin.key); + + const editor = usePlateEditor({ + override: { + components: { + [BoldPlugin.key]: withProps(PlateLeaf, { as: 'b' }), + [CodePlugin.key]: CodeLeaf, + [ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }), + [ParagraphPlugin.key]: ParagraphElement, + [BlockquotePlugin.key]: BlockquoteElement, + [LinkPlugin.key]: LinkElement, + [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }), + [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }), + [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }), + [HEADING_KEYS.h4]: withProps(HeadingElement, { variant: 'h4' }), + [HEADING_KEYS.h5]: withProps(HeadingElement, { variant: 'h5' }), + [HEADING_KEYS.h6]: withProps(HeadingElement, { variant: 'h6' }), + ['ul']: withProps(ListElement, { variant: 'ul' }), + ['ol']: withProps(ListElement, { variant: 'ol' }), + ['li']: withProps(ListElement, { variant: 'li' }), + }, + }, + plugins: [ + ParagraphPlugin, + HeadingPlugin, + BoldPlugin, + ItalicPlugin, + CodePlugin, + ListPlugin, + LinkPlugin, + BlockquotePlugin, + BlockquoteExtPlugin, + ShortcodePlugin, + TrailingBlockPlugin.configure({ + options: { type: 'p' }, + }), + SoftBreakPlugin.configure({ + rules: [ + { hotkey: 'shift+enter' }, + { + hotkey: 'enter', + query: { + allow: [BlockquotePlugin.key], }, - ], - }, + }, + ], }), - createExitBreakPlugin({ + ExitBreakPlugin.configure({ options: { rules: [ { @@ -156,7 +177,7 @@ export default function VisualEditor(props) { query: { start: true, end: true, - allow: KEYS_HEADING, + allow: Object.values(HEADING_KEYS), }, relative: true, level: 1, @@ -164,57 +185,9 @@ export default function VisualEditor(props) { ], }, }), - createTrailingBlockPlugin({ - options: { type: ELEMENT_PARAGRAPH }, - }), ], - { - components: { - [MARK_BOLD]: withProps(PlateLeaf, { as: 'b' }), - [MARK_CODE]: CodeLeaf, - [MARK_ITALIC]: withProps(PlateLeaf, { as: 'em' }), - [ELEMENT_PARAGRAPH]: ParagraphElement, - [ELEMENT_BLOCKQUOTE]: BlockquoteElement, - [ELEMENT_LINK]: LinkElement, - [ELEMENT_H1]: withProps(HeadingElement, { variant: 'h1' }), - [ELEMENT_H2]: withProps(HeadingElement, { variant: 'h2' }), - [ELEMENT_H3]: withProps(HeadingElement, { variant: 'h3' }), - [ELEMENT_H4]: withProps(HeadingElement, { variant: 'h4' }), - [ELEMENT_H5]: withProps(HeadingElement, { variant: 'h5' }), - [ELEMENT_H6]: withProps(HeadingElement, { variant: 'h6' }), - [ELEMENT_UL]: withProps(ListElement, { variant: 'ul' }), - [ELEMENT_OL]: withProps(ListElement, { variant: 'ol' }), - [ELEMENT_LI]: withProps(ListElement, { variant: 'li' }), - }, - }, - ); - - function handleBlockClick() { - console.log('handleBlockClick'); - } - - function handleLinkClick() { - console.log('handleLinkClick'); - } - - function handleToggleMode() { - console.log('handleToggleMode'); - } - - function handleInsertEditorComponent(a) { - console.log('handleInsertEditorComponent', a); - - - - } - - function handleChange(value) { - console.log('handleChange', value); - const mdValue = slateToMarkdown(value, {}); - onChange(mdValue); - } - - const initialValue = props.value ? markdownToSlate(props.value, {}) : emptyValue; + value: initialValue, + }); return ( @@ -227,7 +200,7 @@ export default function VisualEditor(props) { `, )} > - + false} isShowModeToggle={() => false} onChange={() => false} - onInsertEditorComponent={handleInsertEditorComponent} t={t} disabled={isDisabled} /> diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Editor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Editor.js index 8e2ee07eec11..4b2bc1da4e48 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Editor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Editor.js @@ -1,5 +1,5 @@ import React from 'react'; -import { PlateContent } from '@udecode/plate-common'; +import { PlateContent } from '@udecode/plate-common/react'; import { ClassNames } from '@emotion/react'; function Editor(props) { diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/BlockquoteElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/BlockquoteElement.js index dd825c7a8d42..1ab7a6a9115b 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/BlockquoteElement.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/BlockquoteElement.js @@ -1,5 +1,5 @@ import React from 'react'; -import { PlateElement } from '@udecode/plate-common'; +import { PlateElement } from '@udecode/plate-common/react'; import styled from '@emotion/styled'; import { colors } from 'decap-cms-ui-default'; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/HeadingElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/HeadingElement.js index bdeafeb6e73f..f6c81c7963c5 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/HeadingElement.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/HeadingElement.js @@ -1,5 +1,5 @@ import React from 'react'; -import { PlateElement } from '@udecode/plate-common'; +import { PlateElement } from '@udecode/plate-common/react'; import styled from '@emotion/styled'; const headingVariants = { diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/LinkElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/LinkElement.js index 39b1ce62ab35..747fa7fe277b 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/LinkElement.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/LinkElement.js @@ -1,6 +1,6 @@ import React from 'react'; -import { PlateElement, useElement } from '@udecode/plate-common'; -import { useLink } from '@udecode/plate-link'; +import { PlateElement, useElement } from '@udecode/plate-common/react'; +import { useLink } from '@udecode/plate-link/react'; import styled from '@emotion/styled'; const StyledA = styled.a` diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ListElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ListElement.js index 9acfb0658a35..1ec46fa31a5f 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ListElement.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ListElement.js @@ -1,6 +1,6 @@ import React from 'react'; import styled from '@emotion/styled'; -import { PlateElement } from '@udecode/plate-common'; +import { PlateElement } from '@udecode/plate-common/react'; const bottomMargin = '16px'; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ParagraphElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ParagraphElement.js index 35d545de20b2..895fa7258e12 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ParagraphElement.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ParagraphElement.js @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { PlateElement } from '@udecode/plate-common'; +import { PlateElement } from '@udecode/plate-common/react'; const bottomMargin = '16px'; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ShortcodeElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ShortcodeElement.js new file mode 100644 index 000000000000..47f1e4561cd3 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ShortcodeElement.js @@ -0,0 +1,100 @@ +import React, { useState } from 'react'; +import { insertElements, setNodes } from '@udecode/plate-common'; +import { findNodePath, ParagraphPlugin, PlateElement, useEditorRef } from '@udecode/plate-common/react'; +import styled from '@emotion/styled'; +import { fromJS } from 'immutable'; +import { omit } from 'lodash'; +import { css } from '@emotion/react'; +import { Range } from 'slate'; +import { zIndex } from 'decap-cms-ui-default'; + +import { useEditorContext } from '../../editorContext'; + +const StyledDiv = styled.div``; + +function InsertionPoint(props) { + return ( +
+ ); +} + +function ShortcodeElement(props) { + const editor = useEditorRef(); + const { element, dataKey = 'shortcodeData', children } = props; + const { editorControl: EditorControl, editorComponents } = useEditorContext(); + const plugin = editorComponents.get(element.id); + const fieldKeys = ['id', 'fromBlock', 'toBlock', 'toPreview', 'pattern', 'icon']; + + const field = fromJS(omit(plugin, fieldKeys)); + const [value, setValue] = useState(fromJS(element?.data[dataKey] ?? { id: '' })); + + const path = findNodePath(editor, element); + const isSelected = + editor.selection && + path && + Range.isRange(editor.selection) && + Range.includes(editor.selection, path); + const insertBefore = path[0] === 0; + + function handleChange(fieldName, value, metadata) { + const newProperties = { + data: { + ...element.data, + [dataKey]: value.toJS(), + metadata, + }, + }; + setNodes(editor, newProperties, { + at: path, + }); + setValue(value); + } + + function handleInsertBefore() { + console.log('path', path); + insertElements( + editor, + { type: ParagraphPlugin.key, children: [{ text: '' }] }, + { at: path, select: true }, + ); + } + + return ( + <> + + + {insertBefore && } + {}} + isNewEditorComponent={element.data?.shortcodeNew} + isSelected={isSelected} + /> + {children} + + + + ); +} + +export default ShortcodeElement; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Leaf/CodeLeaf.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Leaf/CodeLeaf.js index 6a4e01a0b5ff..5310b2dc0703 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Leaf/CodeLeaf.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Leaf/CodeLeaf.js @@ -1,5 +1,5 @@ import React from 'react'; -import { PlateLeaf } from '@udecode/plate-common'; +import { PlateLeaf } from '@udecode/plate-common/react'; import styled from '@emotion/styled'; import { colors, lengths } from 'decap-cms-ui-default'; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js index 101429018e47..f00990ad2865 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js @@ -1,24 +1,24 @@ import React from 'react'; -import { - useEditorRef, - focusEditor, - useEditorSelector, - findNode, -} from '@udecode/plate-common'; +import { findNode } from '@udecode/plate-common'; +import { useEditorRef, focusEditor, useEditorSelector } from '@udecode/plate-common/react'; import { unwrapList } from '@udecode/plate-list'; import { toggleWrapNodes } from '@udecode/slate-utils'; -import { ELEMENT_BLOCKQUOTE } from '@udecode/plate-block-quote'; +import { BlockquotePlugin } from '@udecode/plate-block-quote/react'; + import ToolbarButton from './ToolbarButton'; function BlockquoteToolbarButton(props) { const editor = useEditorRef(); - const pressed = useEditorSelector(editor => !!findNode(editor, { match: { type: ELEMENT_BLOCKQUOTE } }), []); + const pressed = useEditorSelector( + editor => !!findNode(editor, { match: { type: BlockquotePlugin.key } }), + [], + ); function handleClick() { unwrapList(editor); - toggleWrapNodes(editor, ELEMENT_BLOCKQUOTE); + toggleWrapNodes(editor, BlockquotePlugin.key); focusEditor(editor); } diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/EditorComponentsToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/EditorComponentsToolbarButton.js index 84bdc0fed688..7a9acfcc900b 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/EditorComponentsToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/EditorComponentsToolbarButton.js @@ -1,9 +1,11 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; import styled from '@emotion/styled'; import { Dropdown, DropdownButton, DropdownItem } from 'decap-cms-ui-default'; import { List } from 'immutable'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { insertElements, isSelectionAtBlockEnd, isSelectionAtBlockStart, setBlockAboveNode } from '@udecode/plate-common'; +import { useEditorRef } from '@udecode/plate-common/react'; import ToolbarButton from './ToolbarButton'; @@ -12,10 +14,46 @@ const ToolbarDropdownWrapper = styled.div` position: relative; `; -function EditorComponentsToolbarButton({ disabled, editorComponents, allowedEditorComponents, t, onChange }) { - function handleChange(optionKey) { - onChange(optionKey) - } +function EditorComponentsToolbarButton({ disabled, editorComponents, allowedEditorComponents, t }) { + const editor = useEditorRef(); + + + const handleChange = useCallback( + plugin => { + + const defaultValues = plugin.fields + .toMap() + .mapKeys((_, field) => field.get('name')) + .filter(field => field.has('default')) + .map(field => field.get('default')); + + if (isSelectionAtBlockEnd(editor) && isSelectionAtBlockStart(editor)) { + setBlockAboveNode(editor, { + children: [{ text: '' }], + type: 'shortcode', + id: plugin.id, + data: { + shortcode: plugin.id, + shortcodeNew: true, + shortcodeData: defaultValues.toJS(), + }, + }) + return; + } + + insertElements(editor, { + children: [{ text: '' }], + type: 'shortcode', + id: plugin.id, + data: { + shortcode: plugin.id, + shortcodeNew: true, + shortcodeData: defaultValues.toJS(), + }, + }); + }, + [editor], + ); const editorComponentOptions = editorComponents ? editorComponents @@ -51,7 +89,7 @@ function EditorComponentsToolbarButton({ disabled, editorComponents, allowedEdit label={option.label} className={''} onMouseDown={e => e.preventDefault()} - onClick={() => handleChange(option.id)} + onClick={() => handleChange(option)} /> ))} @@ -64,7 +102,6 @@ function EditorComponentsToolbarButton({ disabled, editorComponents, allowedEdit EditorComponentsToolbarButton.propTypes = { editorComponents: ImmutablePropTypes.map, allowedEditorComponents: ImmutablePropTypes.list, - onChange: PropTypes.func, disabled: PropTypes.bool, t: PropTypes.func.isRequired, }; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js index 8b0058402e69..2571bb82714e 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js @@ -1,17 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { - focusEditor, - getBlockAbove, - isSelectionExpanded, - toggleNodeType, - useEditorRef, - useEditorSelector, -} from '@udecode/plate-common'; import styled from '@emotion/styled'; -import { ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph'; import { unwrapList } from '@udecode/plate-list'; import { Dropdown, DropdownButton, DropdownItem } from 'decap-cms-ui-default'; +import { focusEditor, ParagraphPlugin, useEditorRef, useEditorSelector } from '@udecode/plate-common/react'; +import { getBlockAbove, isSelectionExpanded, toggleBlock } from '@udecode/plate-common'; import ToolbarButton from './ToolbarButton'; @@ -34,21 +27,22 @@ function HeadingToolbarButton({ disabled, isVisible, t }) { const value = useEditorSelector(editor => { if (!isSelectionExpanded(editor)) { - const entry = getBlockAbove(editor) + const entry = getBlockAbove(editor); if (entry) { return entry[0].type; } } - return ELEMENT_PARAGRAPH; + return ParagraphPlugin.key; }, []); + console.log('HeadingToolbarButton', value); function handleChange(optionKey) { unwrapList(editor); - toggleNodeType(editor, { activeType: optionKey }); + toggleBlock(editor, { type: optionKey }); focusEditor(editor); } diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/LinkToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/LinkToolbarButton.js index 8301416426e8..7fd1d42f5313 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/LinkToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/LinkToolbarButton.js @@ -1,11 +1,7 @@ import React from 'react'; -import { - useLinkToolbarButton, - useLinkToolbarButtonState, - upsertLink, - unwrapLink, -} from '@udecode/plate-link'; -import { useEditorRef } from '@udecode/plate-common'; +import { useLinkToolbarButton, useLinkToolbarButtonState } from '@udecode/plate-link/react'; +import { upsertLink, unwrapLink } from '@udecode/plate-link'; +import { useEditorRef } from '@udecode/plate-common/react'; import ToolbarButton from './ToolbarButton'; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/ListToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/ListToolbarButton.js index 42901cf1c256..ef48cc1f06c2 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/ListToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/ListToolbarButton.js @@ -1,5 +1,5 @@ import React from 'react'; -import { useListToolbarButton, useListToolbarButtonState } from '@udecode/plate-list'; +import { useListToolbarButton, useListToolbarButtonState } from '@udecode/plate-list/react'; import ToolbarButton from './ToolbarButton'; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/MarkToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/MarkToolbarButton.js index af08b7949d92..13a943313e6f 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/MarkToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/MarkToolbarButton.js @@ -1,5 +1,5 @@ import React from 'react'; -import { useMarkToolbarButton, useMarkToolbarButtonState } from '@udecode/plate-common'; +import { useMarkToolbarButton, useMarkToolbarButtonState } from '@udecode/plate-common/react'; import ToolbarButton from './ToolbarButton'; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js index 6ef68096ed4d..63fc6ee4f500 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import styled from '@emotion/styled'; import { List } from 'immutable'; import { colors, transitions } from 'decap-cms-ui-default'; -import { MARK_BOLD, MARK_CODE, MARK_ITALIC } from '@udecode/plate-basic-marks'; +import { BoldPlugin, ItalicPlugin, CodePlugin } from '@udecode/plate-basic-marks/react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import MarkToolbarButton from './MarkToolbarButton'; @@ -25,7 +25,7 @@ const ToolbarContainer = styled.div` `; function Toolbar(props) { - const { disabled, t, editorComponents, allowedEditorComponents, onInsertEditorComponent } = props; + const { disabled, t, editorComponents, allowedEditorComponents } = props; function isVisible(button) { const { buttons } = props; @@ -38,7 +38,7 @@ function Toolbar(props) { {isVisible('bold') && (
@@ -108,7 +107,6 @@ Toolbar.propTypes = { disabled: PropTypes.bool, editorComponents: ImmutablePropTypes.map, allowedEditorComponents: ImmutablePropTypes.list, - onInsertEditorComponent: PropTypes.func, t: PropTypes.func.isRequired, }; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/editorContext.js b/packages/decap-cms-widget-richtext/src/RichtextControl/editorContext.js new file mode 100644 index 000000000000..fd0c5c26a1eb --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/editorContext.js @@ -0,0 +1,16 @@ +import React, { createContext, useContext } from 'react'; + +const EditorContext = createContext(null); + +export function useEditorContext() { + return useContext(EditorContext); +} + +export function EditorProvider({ children, editorControl, editorComponents }) { + const value = { editorControl, editorComponents }; + return ( + + {children} + + ); +} diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/BlockquoteExtPlugin.js b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/BlockquoteExtPlugin.js new file mode 100644 index 000000000000..27b1dce3c798 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/BlockquoteExtPlugin.js @@ -0,0 +1,58 @@ +import { BlockquotePlugin } from '@udecode/plate-block-quote/react'; +import { + getBlockAbove, + isAncestorEmpty, + unwrapNodes, + isFirstChild, + isSelectionAtBlockStart, +} from '@udecode/plate-common'; +import { createPlatePlugin, Key } from '@udecode/plate-common/react'; + +export const KEY_BLOCKQUOTE_EXIT_BREAK = 'blockquoteExitBreakPlugin'; + +function isWithinBlockquote(editor, entry) { + const blockAbove = getBlockAbove(editor, { at: entry[1] }); + return blockAbove?.[0]?.type === BlockquotePlugin.key; +} + +function queryNode(editor, entry, { empty, first, start }) { + return ( + (!empty || isAncestorEmpty(editor, entry[0])) && + (!first || isFirstChild(entry[1])) && + (!start || isSelectionAtBlockStart(editor)) + ); +} + +function unwrap(editor) { + unwrapNodes(editor, { split: true, match: n => n.type === BlockquotePlugin.key }); + return true; +} + +function keyDownHandler({ editor, event, query }) { + const entry = getBlockAbove(editor); + if (!entry) return; + + if (isWithinBlockquote(editor, entry) && queryNode(editor, entry, query) && unwrap(editor)) { + event.preventDefault(); + event.stopPropagation(); + } +} + +const BlockquoteExtPlugin = createPlatePlugin({ + key: KEY_BLOCKQUOTE_EXIT_BREAK, + node: { isElement: true }, +}).extend(() => ({ + shortcuts: { + blockquoteEnter: { + handler: handlerProps => keyDownHandler({ ...handlerProps, query: { empty: true } }), + keys: [[Key.Enter]], + }, + blockquoteBackspace: { + handler: handlerProps => + keyDownHandler({ ...handlerProps, query: { first: true, start: true } }), + keys: [[Key.Backspace]], + }, + }, +})); + +export default BlockquoteExtPlugin; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ShortcodePlugin.js b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ShortcodePlugin.js new file mode 100644 index 000000000000..e559ed2f0312 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ShortcodePlugin.js @@ -0,0 +1,16 @@ +import { createSlatePlugin } from '@udecode/plate-common'; +import { toPlatePlugin } from '@udecode/plate-common/react'; + +import ShortcodeElement from '../components/Element/ShortcodeElement'; + +const plugin = createSlatePlugin({ + key: 'shortcode', + node: { + isElement: true, + isVoid: true, + component: ShortcodeElement, + }, +}); +const ShortcodePlugin = toPlatePlugin(plugin); + +export default ShortcodePlugin; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/createBlockquoteExtPlugin.js b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/createBlockquoteExtPlugin.js deleted file mode 100644 index 403d958e3128..000000000000 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/createBlockquoteExtPlugin.js +++ /dev/null @@ -1,66 +0,0 @@ -import { - createPluginFactory, - getBlockAbove, - isHotkey, - isAncestorEmpty, - unwrapNodes, - isFirstChild, - isSelectionAtBlockStart, -} from '@udecode/plate-common'; -import { ELEMENT_BLOCKQUOTE } from '@udecode/plate-block-quote'; - -export const KEY_BLOCKQUOTE_EXIT_BREAK = 'blockquoteExitBreakPlugin'; - -function isWithinBlockquote(editor, entry) { - const blockAbove = getBlockAbove(editor, { at: entry[1] }); - return blockAbove?.[0]?.type === ELEMENT_BLOCKQUOTE; -} - -function queryNode(editor, entry, { empty, first, start }) { - return ( - (!empty || isAncestorEmpty(editor, entry[0])) && - (!first || isFirstChild(entry[1])) && - (!start || isSelectionAtBlockStart(editor)) - ); -} - -function unwrap(editor) { - unwrapNodes(editor, { split: true, match: n => n.type === ELEMENT_BLOCKQUOTE }); - return true; -} - -function onKeyDownBlockquoteExitBreak(editor, { options: { rules } }) { - return event => { - if (event.defaultPrevented) return; - - const entry = getBlockAbove(editor); - if (!entry) return; - - rules.forEach(({ hotkey, query }) => { - if ( - isHotkey(hotkey, event) && - isWithinBlockquote(editor, entry) && - queryNode(editor, entry, query) && - unwrap(editor) - ) { - event.preventDefault(); - event.stopPropagation(); - } - }); - }; -} - -const createBlockquoteExtPlugin = createPluginFactory({ - key: KEY_BLOCKQUOTE_EXIT_BREAK, - handlers: { - onKeyDown: onKeyDownBlockquoteExitBreak, - }, - options: { - rules: [ - { hotkey: 'enter', query: { empty: true } }, - { hotkey: 'backspace', query: { first: true, start: true } }, - ], - }, -}); - -export default createBlockquoteExtPlugin; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/withProps.js b/packages/decap-cms-widget-richtext/src/RichtextControl/withProps.js index f3bdc5a2f2f4..8639a615580b 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/withProps.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/withProps.js @@ -1,11 +1,9 @@ import React from 'react'; -function withProps(Component, defaultProps) { +export default function withProps(Component, defaultProps) { const ComponentWithClassName = Component; return React.forwardRef(function ExtendComponent(props, ref) { return ; }); } - -export default withProps; diff --git a/packages/decap-cms-widget-richtext/src/serializers/index.js b/packages/decap-cms-widget-richtext/src/serializers/index.js index c58d561b58b4..f187d0c5d9c0 100644 --- a/packages/decap-cms-widget-richtext/src/serializers/index.js +++ b/packages/decap-cms-widget-richtext/src/serializers/index.js @@ -95,7 +95,7 @@ function markdownToRemarkRemoveTokenizers({ inlineTokenizers }) { /** * Serialize an MDAST to a Markdown string. */ -export function remarkToMarkdown(obj, remarkPlugins) { +export function remarkToMarkdown(obj, remarkPlugins, editorComponents) { /** * Rewrite the remark-stringify text visitor to simply return the text value, * without encoding or escaping any characters. This means we're completely @@ -133,7 +133,7 @@ export function remarkToMarkdown(obj, remarkPlugins) { .use(remarkStripTrailingBreaks) .use(remarkToMarkdownPlugin) .use(remarkAllowAllText) - .use(createRemarkShortcodeStringifier({ plugins: Map() })) + .use(createRemarkShortcodeStringifier({ plugins: editorComponents })) .use(remarkPlugins); /** @@ -220,8 +220,11 @@ export function markdownToSlate(markdown, { voidCodeBlock, remarkPlugins = [] } * MDAST. The conversion is manual because Unified can only operate on Unist * trees. */ -export function slateToMarkdown(raw, { voidCodeBlock, remarkPlugins = [] } = {}) { - const mdast = slateToRemark(raw, { voidCodeBlock }); - const markdown = remarkToMarkdown(mdast, remarkPlugins); +export function slateToMarkdown(raw, { voidCodeBlock, remarkPlugins = [] } = {}, editorComponents) { + console.log('new raw', raw); + const mdast = slateToRemark(raw, { voidCodeBlock }, editorComponents); + console.log('new mdast', mdast); + const markdown = remarkToMarkdown(mdast, remarkPlugins, editorComponents); + console.log('new md', markdown); return markdown; } From 86fcc3944a8b98de7d5bb56cbda9542dc7fc0192 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Wed, 23 Oct 2024 10:28:40 +0200 Subject: [PATCH 14/43] feat(richtext): clean up some logs --- .../src/RichtextControl/VisualEditor.js | 2 -- .../src/RichtextControl/components/Element/ShortcodeElement.js | 1 - .../RichtextControl/components/Toolbar/HeadingToolbarButton.js | 3 --- 3 files changed, 6 deletions(-) diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index 18baf33d175f..e058a1778f43 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -115,8 +115,6 @@ export default function VisualEditor(props) { const initialValue = props.value ? markdownToSlate(props.value, {}) : emptyValue; - console.log('pakaaka', ParagraphPlugin.key); - const editor = usePlateEditor({ override: { components: { diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ShortcodeElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ShortcodeElement.js index 47f1e4561cd3..68c7912ef768 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ShortcodeElement.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ShortcodeElement.js @@ -60,7 +60,6 @@ function ShortcodeElement(props) { } function handleInsertBefore() { - console.log('path', path); insertElements( editor, { type: ParagraphPlugin.key, children: [{ text: '' }] }, diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js index 2571bb82714e..6cc2896dc7ca 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js @@ -37,9 +37,6 @@ function HeadingToolbarButton({ disabled, isVisible, t }) { return ParagraphPlugin.key; }, []); - - console.log('HeadingToolbarButton', value); - function handleChange(optionKey) { unwrapList(editor); toggleBlock(editor, { type: optionKey }); From 4802720a6cdac9b6f5fb4a7d06132a64c1dfa193 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Wed, 1 Oct 2025 08:00:56 +0000 Subject: [PATCH 15/43] chore(): update slate to 0.110 --- package-lock.json | 95 +++++++++---------- .../decap-cms-widget-markdown/package.json | 8 +- .../src/MarkdownControl/VisualEditor.js | 2 +- 3 files changed, 51 insertions(+), 54 deletions(-) diff --git a/package-lock.json b/package-lock.json index e8a5da98bf4d..532fe5fbabef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7705,12 +7705,6 @@ "@types/node": "*" } }, - "node_modules/@types/is-hotkey": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.10.tgz", - "integrity": "sha512-RvC8KMw5BCac1NvRRyaHgMMEtBaZ6wh0pyPTBu7izn4Sj/AX9Y4aXU5c7rX8PnM/knsuUpC1IeoBkANtxBypsQ==", - "license": "MIT" - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -7792,6 +7786,7 @@ "version": "4.17.20", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true, "license": "MIT" }, "node_modules/@types/mdast": { @@ -12290,9 +12285,9 @@ "license": "MIT" }, "node_modules/compute-scroll-into-view": { - "version": "1.0.20", - "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", - "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", "license": "MIT" }, "node_modules/concat-map": { @@ -33456,12 +33451,12 @@ "license": "MIT" }, "node_modules/scroll-into-view-if-needed": { - "version": "2.2.31", - "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz", - "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", "license": "MIT", "dependencies": { - "compute-scroll-into-view": "^1.0.20" + "compute-scroll-into-view": "^3.0.2" } }, "node_modules/section-matter": { @@ -34038,12 +34033,12 @@ } }, "node_modules/slate": { - "version": "0.91.4", - "resolved": "https://registry.npmjs.org/slate/-/slate-0.91.4.tgz", - "integrity": "sha512-aUJ3rpjrdi5SbJ5G1Qjr3arytfRkEStTmHjBfWq2A2Q8MybacIzkScSvGJjQkdTk3djCK9C9SEOt39sSeZFwTw==", + "version": "0.110.2", + "resolved": "https://registry.npmjs.org/slate/-/slate-0.110.2.tgz", + "integrity": "sha512-4xGULnyMCiEQ0Ml7JAC1A6HVE6MNpPJU7Eq4cXh1LxlrR0dFXC3XC+rNfQtUJ7chHoPkws57x7DDiWiZAt+PBA==", "license": "MIT", "dependencies": { - "immer": "^9.0.6", + "immer": "^10.0.3", "is-plain-object": "^5.0.0", "tiny-warning": "^1.0.3" } @@ -34061,9 +34056,9 @@ } }, "node_modules/slate-history": { - "version": "0.93.0", - "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.93.0.tgz", - "integrity": "sha512-Gr1GMGPipRuxIz41jD2/rbvzPj8eyar56TVMyJBvBeIpQSSjNISssvGNDYfJlSWM8eaRqf6DAcxMKzsLCYeX6g==", + "version": "0.100.0", + "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.100.0.tgz", + "integrity": "sha512-x5rUuWLNtH97hs9PrFovGgt3Qc5zkTm/5mcUB+0NR/TK923eLax4HsL6xACLHMs245nI6aJElyM1y6hN0y5W/Q==", "license": "MIT", "dependencies": { "is-plain-object": "^5.0.0" @@ -34073,9 +34068,9 @@ } }, "node_modules/slate-hyperscript": { - "version": "0.77.0", - "resolved": "https://registry.npmjs.org/slate-hyperscript/-/slate-hyperscript-0.77.0.tgz", - "integrity": "sha512-M6uRpttwKnosniQORNPYQABHQ9XWC7qaSr/127LWWPjTOR5MSSwrHGrghN81BhZVqpICHrI7jkPA2813cWdHNA==", + "version": "0.100.0", + "resolved": "https://registry.npmjs.org/slate-hyperscript/-/slate-hyperscript-0.100.0.tgz", + "integrity": "sha512-fb2KdAYg6RkrQGlqaIi4wdqz3oa0S4zKNBJlbnJbNOwa23+9FLD6oPVx9zUGqCSIpy+HIpOeqXrg0Kzwh/Ii4A==", "license": "MIT", "dependencies": { "is-plain-object": "^5.0.0" @@ -34095,37 +34090,29 @@ } }, "node_modules/slate-react": { - "version": "0.91.11", - "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.91.11.tgz", - "integrity": "sha512-2nS29rc2kuTTJrEUOXGyTkFATmTEw/R9KuUXadUYiz+UVwuFOUMnBKuwJWyuIBOsFipS+06SkIayEf5CKdARRQ==", + "version": "0.110.3", + "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.110.3.tgz", + "integrity": "sha512-AS8PPjwmsFS3Lq0MOEegLVlFoxhyos68G6zz2nW4sh3WeTXV7pX0exnwtY1a/docn+J3LGQO11aZXTenPXA/kg==", "license": "MIT", "dependencies": { "@juggle/resize-observer": "^3.4.0", - "@types/is-hotkey": "^0.1.1", - "@types/lodash": "^4.14.149", - "direction": "^1.0.3", - "is-hotkey": "^0.1.6", + "direction": "^1.0.4", + "is-hotkey": "^0.2.0", "is-plain-object": "^5.0.0", - "lodash": "^4.17.4", - "scroll-into-view-if-needed": "^2.2.20", - "tiny-invariant": "1.0.6" + "lodash": "^4.17.21", + "scroll-into-view-if-needed": "^3.1.0", + "tiny-invariant": "1.3.1" }, "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.65.3" + "react": ">=18.2.0", + "react-dom": ">=18.2.0", + "slate": ">=0.99.0" } }, - "node_modules/slate-react/node_modules/is-hotkey": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.1.8.tgz", - "integrity": "sha512-qs3NZ1INIS+H+yeo7cD9pDfwYV/jqRh1JG9S9zYrNudkoUQg7OL7ziXqRKu+InFjUIDoP2o6HIkLYMh1pcWgyQ==", - "license": "MIT" - }, "node_modules/slate-react/node_modules/tiny-invariant": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz", - "integrity": "sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==", "license": "MIT" }, "node_modules/slate-soft-break": { @@ -34138,6 +34125,16 @@ "slate-react": ">=0.19.3" } }, + "node_modules/slate/node_modules/immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/slice-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", @@ -39882,12 +39879,12 @@ "remark-slate": "^1.8.6", "remark-slate-transformer": "^0.7.4", "remark-stringify": "^6.0.4", - "slate": "^0.91.1", + "slate": "^0.110.2", "slate-base64-serializer": "^0.2.107", - "slate-history": "^0.93.0", - "slate-hyperscript": "^0.77.0", + "slate-history": "^0.100.0", + "slate-hyperscript": "^0.100.0", "slate-plain-serializer": "^0.7.1", - "slate-react": "^0.91.2", + "slate-react": "^0.110.2", "slate-soft-break": "^0.9.0", "unified": "^9.2.0", "unist-builder": "^1.0.3", diff --git a/packages/decap-cms-widget-markdown/package.json b/packages/decap-cms-widget-markdown/package.json index 5fac76dd09eb..8d41eea74699 100644 --- a/packages/decap-cms-widget-markdown/package.json +++ b/packages/decap-cms-widget-markdown/package.json @@ -35,12 +35,12 @@ "remark-slate": "^1.8.6", "remark-slate-transformer": "^0.7.4", "remark-stringify": "^6.0.4", - "slate": "^0.91.1", + "slate": "^0.110.2", "slate-base64-serializer": "^0.2.107", - "slate-history": "^0.93.0", - "slate-hyperscript": "^0.77.0", + "slate-history": "^0.100.0", + "slate-hyperscript": "^0.100.0", "slate-plain-serializer": "^0.7.1", - "slate-react": "^0.91.2", + "slate-react": "^0.110.2", "slate-soft-break": "^0.9.0", "unified": "^9.2.0", "unist-builder": "^1.0.3", diff --git a/packages/decap-cms-widget-markdown/src/MarkdownControl/VisualEditor.js b/packages/decap-cms-widget-markdown/src/MarkdownControl/VisualEditor.js index 2eccff0c2d07..f3f632ebc789 100644 --- a/packages/decap-cms-widget-markdown/src/MarkdownControl/VisualEditor.js +++ b/packages/decap-cms-widget-markdown/src/MarkdownControl/VisualEditor.js @@ -222,7 +222,7 @@ function Editor(props) { position: relative; `} > - + { Date: Wed, 1 Oct 2025 08:32:51 +0000 Subject: [PATCH 16/43] chore: update slate and related dependencies to latest versions --- package-lock.json | 56 +++++++++++++------ .../decap-cms-widget-markdown/package.json | 9 +-- 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 532fe5fbabef..cd35e9d83b89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34033,13 +34033,12 @@ } }, "node_modules/slate": { - "version": "0.110.2", - "resolved": "https://registry.npmjs.org/slate/-/slate-0.110.2.tgz", - "integrity": "sha512-4xGULnyMCiEQ0Ml7JAC1A6HVE6MNpPJU7Eq4cXh1LxlrR0dFXC3XC+rNfQtUJ7chHoPkws57x7DDiWiZAt+PBA==", + "version": "0.118.1", + "resolved": "https://registry.npmjs.org/slate/-/slate-0.118.1.tgz", + "integrity": "sha512-6H1DNgnSwAFhq/pIgf+tLvjNzH912M5XrKKhP9Frmbds2zFXdSJ6L/uFNyVKxQIkPzGWPD0m+wdDfmEuGFH5Tg==", "license": "MIT", "dependencies": { "immer": "^10.0.3", - "is-plain-object": "^5.0.0", "tiny-warning": "^1.0.3" } }, @@ -34055,10 +34054,34 @@ "slate": ">=0.32.0 <0.50.0" } }, + "node_modules/slate-dom": { + "version": "0.118.1", + "resolved": "https://registry.npmjs.org/slate-dom/-/slate-dom-0.118.1.tgz", + "integrity": "sha512-D6J0DF9qdJrXnRDVhYZfHzzpVxzqKRKFfS0Wcin2q0UC+OnQZ0lbCGJobatVbisOlbSe7dYFHBp9OZ6v1lEcbQ==", + "license": "MIT", + "dependencies": { + "@juggle/resize-observer": "^3.4.0", + "direction": "^1.0.4", + "is-hotkey": "^0.2.0", + "is-plain-object": "^5.0.0", + "lodash": "^4.17.21", + "scroll-into-view-if-needed": "^3.1.0", + "tiny-invariant": "1.3.1" + }, + "peerDependencies": { + "slate": ">=0.99.0" + } + }, + "node_modules/slate-dom/node_modules/tiny-invariant": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==", + "license": "MIT" + }, "node_modules/slate-history": { - "version": "0.100.0", - "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.100.0.tgz", - "integrity": "sha512-x5rUuWLNtH97hs9PrFovGgt3Qc5zkTm/5mcUB+0NR/TK923eLax4HsL6xACLHMs245nI6aJElyM1y6hN0y5W/Q==", + "version": "0.113.1", + "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.113.1.tgz", + "integrity": "sha512-J9NSJ+UG2GxoW0lw5mloaKcN0JI0x2IA5M5FxyGiInpn+QEutxT1WK7S/JneZCMFJBoHs1uu7S7e6pxQjubHmQ==", "license": "MIT", "dependencies": { "is-plain-object": "^5.0.0" @@ -34090,15 +34113,14 @@ } }, "node_modules/slate-react": { - "version": "0.110.3", - "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.110.3.tgz", - "integrity": "sha512-AS8PPjwmsFS3Lq0MOEegLVlFoxhyos68G6zz2nW4sh3WeTXV7pX0exnwtY1a/docn+J3LGQO11aZXTenPXA/kg==", + "version": "0.117.4", + "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.117.4.tgz", + "integrity": "sha512-9ckilyUzQS1VHJnstIpgInhcWnTDgv2Cd7m1HOQVl3zasChoapPSMftzT/wl/48grZaZYZIi4xVuzGTcFRUWFg==", "license": "MIT", "dependencies": { "@juggle/resize-observer": "^3.4.0", "direction": "^1.0.4", "is-hotkey": "^0.2.0", - "is-plain-object": "^5.0.0", "lodash": "^4.17.21", "scroll-into-view-if-needed": "^3.1.0", "tiny-invariant": "1.3.1" @@ -34106,7 +34128,8 @@ "peerDependencies": { "react": ">=18.2.0", "react-dom": ">=18.2.0", - "slate": ">=0.99.0" + "slate": ">=0.114.0", + "slate-dom": ">=0.116.0" } }, "node_modules/slate-react/node_modules/tiny-invariant": { @@ -39879,12 +39902,13 @@ "remark-slate": "^1.8.6", "remark-slate-transformer": "^0.7.4", "remark-stringify": "^6.0.4", - "slate": "^0.110.2", + "slate": "^0.118.1", "slate-base64-serializer": "^0.2.107", - "slate-history": "^0.100.0", + "slate-dom": "^0.118.1", + "slate-history": "^0.113.1", "slate-hyperscript": "^0.100.0", - "slate-plain-serializer": "^0.7.1", - "slate-react": "^0.110.2", + "slate-plain-serializer": "^0.7.3", + "slate-react": "^0.117.4", "slate-soft-break": "^0.9.0", "unified": "^9.2.0", "unist-builder": "^1.0.3", diff --git a/packages/decap-cms-widget-markdown/package.json b/packages/decap-cms-widget-markdown/package.json index 8d41eea74699..809d975a8d08 100644 --- a/packages/decap-cms-widget-markdown/package.json +++ b/packages/decap-cms-widget-markdown/package.json @@ -35,12 +35,13 @@ "remark-slate": "^1.8.6", "remark-slate-transformer": "^0.7.4", "remark-stringify": "^6.0.4", - "slate": "^0.110.2", + "slate": "^0.118.1", "slate-base64-serializer": "^0.2.107", - "slate-history": "^0.100.0", + "slate-dom": "^0.118.1", + "slate-history": "^0.113.1", "slate-hyperscript": "^0.100.0", - "slate-plain-serializer": "^0.7.1", - "slate-react": "^0.110.2", + "slate-plain-serializer": "^0.7.3", + "slate-react": "^0.117.4", "slate-soft-break": "^0.9.0", "unified": "^9.2.0", "unist-builder": "^1.0.3", From f0fb7a8b02e92080a8f27fefd4ed34f8bfe77ef8 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Wed, 1 Oct 2025 13:12:56 +0000 Subject: [PATCH 17/43] chore: change remark versions to the ones that work with our tokenizer implementation --- package-lock.json | 113 ++++++++++++++---- .../decap-cms-widget-richtext/package.json | 6 +- 2 files changed, 96 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index fe18f13f9cd5..4c2820d37c74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11814,6 +11814,19 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detab": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detab/-/detab-2.0.4.tgz", + "integrity": "sha512-8zdsQA5bIkoRECvCrNKPla84lyoR7DSAyf7p0YgXzBO9PDJx8KntPUay7NS6yp+KdxdVtiE5SpHKtbp2ZQyA9g==", + "license": "MIT", + "dependencies": { + "repeat-string": "^1.5.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/detect-indent": { "version": "5.0.0", "license": "MIT", @@ -32376,6 +32389,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-location": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.6.tgz", + "integrity": "sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vfile-message": { "version": "2.0.4", "license": "MIT", @@ -34282,17 +34305,6 @@ "react-immutable-proptypes": "^2.1.0" } }, - "packages/decap-cms-widget-markdown/node_modules/detab": { - "version": "2.0.4", - "license": "MIT", - "dependencies": { - "repeat-string": "^1.5.4" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "packages/decap-cms-widget-markdown/node_modules/hast-util-to-html": { "version": "7.1.3", "license": "MIT", @@ -34395,14 +34407,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "packages/decap-cms-widget-markdown/node_modules/vfile-location": { - "version": "2.0.6", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "packages/decap-cms-widget-number": { "version": "3.2.0", "license": "MIT", @@ -34463,6 +34467,8 @@ "@udecode/plate-trailing-block": "^39.0.0", "class-variance-authority": "^0.7.0", "lucide-react": "^0.331.0", + "remark-parse": "^6.0.3", + "remark-rehype": "^4.0.0", "slate": "^0.110.2", "slate-history": "^0.100.0", "slate-hyperscript": "^0.100.0", @@ -34476,8 +34482,8 @@ "immutable": "^3.7.6", "lodash": "^4.17.11", "prop-types": "^15.7.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", "react-immutable-proptypes": "^2.1.0" } }, @@ -34491,6 +34497,71 @@ "url": "https://opencollective.com/immer" } }, + "packages/decap-cms-widget-richtext/node_modules/mdast-util-to-hast": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-4.0.0.tgz", + "integrity": "sha512-yOTZSxR1aPvWRUxVeLaLZ1sCYrK87x2Wusp1bDM/Ao2jETBhYUKITI3nHvgy+HkZW54HuCAhHnS0mTcbECD5Ig==", + "license": "MIT", + "dependencies": { + "collapse-white-space": "^1.0.0", + "detab": "^2.0.0", + "mdast-util-definitions": "^1.2.0", + "mdurl": "^1.0.1", + "trim": "0.0.1", + "trim-lines": "^1.0.0", + "unist-builder": "^1.0.1", + "unist-util-generated": "^1.1.0", + "unist-util-position": "^3.0.0", + "unist-util-visit": "^1.1.0", + "xtend": "^4.0.1" + } + }, + "packages/decap-cms-widget-richtext/node_modules/parse-entities": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz", + "integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==", + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + } + }, + "packages/decap-cms-widget-richtext/node_modules/remark-parse": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-6.0.3.tgz", + "integrity": "sha512-QbDXWN4HfKTUC0hHa4teU463KclLAnwpn/FBn87j9cKYJWWawbiLgMfP2Q4XwhxxuuuOxHlw+pSN0OKuJwyVvg==", + "license": "MIT", + "dependencies": { + "collapse-white-space": "^1.0.2", + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-whitespace-character": "^1.0.0", + "is-word-character": "^1.0.0", + "markdown-escapes": "^1.0.0", + "parse-entities": "^1.1.0", + "repeat-string": "^1.5.4", + "state-toggle": "^1.0.0", + "trim": "0.0.1", + "trim-trailing-lines": "^1.0.0", + "unherit": "^1.0.4", + "unist-util-remove-position": "^1.0.0", + "vfile-location": "^2.0.0", + "xtend": "^4.0.1" + } + }, + "packages/decap-cms-widget-richtext/node_modules/remark-rehype": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-4.0.1.tgz", + "integrity": "sha512-k1GzhtRhXr1sZjX86OS7H4asQu5uOM9Tro//SpOdRaxax6o43mr7M7Np20ubJ+GM6eYjlEHtPv1rDN2hXs2plw==", + "license": "MIT", + "dependencies": { + "mdast-util-to-hast": "^4.0.0" + } + }, "packages/decap-cms-widget-richtext/node_modules/slate": { "version": "0.110.2", "resolved": "https://registry.npmjs.org/slate/-/slate-0.110.2.tgz", diff --git a/packages/decap-cms-widget-richtext/package.json b/packages/decap-cms-widget-richtext/package.json index 3bb4d42bb01e..e9a38644f66b 100644 --- a/packages/decap-cms-widget-richtext/package.json +++ b/packages/decap-cms-widget-richtext/package.json @@ -36,6 +36,8 @@ "slate-history": "^0.100.0", "slate-hyperscript": "^0.100.0", "slate-react": "^0.110.2", + "remark-parse": "^6.0.3", + "remark-rehype": "^4.0.0", "unified": "^9.0.0" }, "peerDependencies": { @@ -45,8 +47,8 @@ "immutable": "^3.7.6", "lodash": "^4.17.11", "prop-types": "^15.7.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", "react-immutable-proptypes": "^2.1.0" } } From db48a9a18c4e25853c19a5200873993cf431c7f3 Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 8 Oct 2025 15:12:54 +0200 Subject: [PATCH 18/43] feat(widget-richtext): update plate to v50, temporary disable editor components --- package-lock.json | 930 ++++++++---------- .../decap-cms-widget-richtext/package.json | 18 +- .../src/RichtextControl/VisualEditor.js | 105 +- .../src/RichtextControl/components/Editor.js | 2 +- .../components/Element/BlockquoteElement.js | 2 +- .../components/Element/HeadingElement.js | 15 +- .../components/Element/LinkElement.js | 8 +- .../components/Element/ListElement.js | 16 +- .../components/Element/ParagraphElement.js | 2 +- .../components/Element/ShortcodeElement.js | 10 +- .../components/Leaf/CodeLeaf.js | 2 +- .../Toolbar/BlockquoteToolbarButton.js | 15 +- .../Toolbar/EditorComponentsToolbarButton.js | 29 +- .../Toolbar/HeadingToolbarButton.js | 13 +- .../components/Toolbar/LinkToolbarButton.js | 6 +- .../components/Toolbar/ListToolbarButton.js | 2 +- .../components/Toolbar/MarkToolbarButton.js | 2 +- .../components/Toolbar/Toolbar.js | 2 +- .../plugins/BlockquoteExtPlugin.js | 58 -- .../plugins/ExtendedBlockquotePlugin.js | 52 + .../plugins/ShortcodePlugin.js | 4 +- 21 files changed, 571 insertions(+), 722 deletions(-) delete mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/plugins/BlockquoteExtPlugin.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ExtendedBlockquotePlugin.js diff --git a/package-lock.json b/package-lock.json index 4c2820d37c74..2870af1b303c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2760,21 +2760,6 @@ "@floating-ui/utils": "^0.2.10" } }, - "node_modules/@floating-ui/react": { - "version": "0.26.28", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", - "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.1.2", - "@floating-ui/utils": "^0.2.8", - "tabbable": "^6.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, "node_modules/@floating-ui/react-dom": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", @@ -5192,6 +5177,349 @@ "node": ">=14" } }, + "node_modules/@platejs/basic-nodes": { + "version": "49.0.0", + "resolved": "https://registry.npmjs.org/@platejs/basic-nodes/-/basic-nodes-49.0.0.tgz", + "integrity": "sha512-l7MbW1Oy2uyRRYOiWVGxTNs8nIvBM/EWjZZFEmtOZcpzXrSY6c3cZd0KAA6u0Z+NLLbLABLJNxuYFBZ5ARvBsg==", + "license": "MIT", + "peerDependencies": { + "platejs": ">=49.0.0", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@platejs/core": { + "version": "49.2.21", + "resolved": "https://registry.npmjs.org/@platejs/core/-/core-49.2.21.tgz", + "integrity": "sha512-RjipAmkWM8UqDYQKP+T0uTX+QZJdkIuvwpXwJowXG6YhAeiJuIgBv/7hyMmZP1+1cZPCUj6JW0FBBTK4gmD0oA==", + "license": "MIT", + "dependencies": { + "@platejs/slate": "49.2.21", + "@udecode/react-hotkeys": "37.0.0", + "@udecode/react-utils": "49.0.15", + "@udecode/utils": "47.2.7", + "clsx": "^2.1.1", + "html-entities": "^2.6.0", + "is-hotkey": "^0.2.0", + "jotai": "~2.8.4", + "jotai-optics": "0.4.0", + "jotai-x": "2.3.3", + "lodash": "^4.17.21", + "nanoid": "^5.1.5", + "optics-ts": "2.4.1", + "slate": "0.118.1", + "slate-dom": "0.118.1", + "slate-hyperscript": "0.115.0", + "slate-react": "0.117.4", + "use-deep-compare": "^1.3.0", + "zustand": "^5.0.5", + "zustand-x": "6.1.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@platejs/core/node_modules/@udecode/react-utils": { + "version": "49.0.15", + "resolved": "https://registry.npmjs.org/@udecode/react-utils/-/react-utils-49.0.15.tgz", + "integrity": "sha512-ra9e0WyECZEnOLyW1nf4pqGBBTLcktHfFhL+qlr7woMAwmNHs7HLbw/khoKfpSFt2RgjieE+QhawT6haFQAuhA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "^1.2.3", + "@udecode/utils": "47.2.7", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@platejs/core/node_modules/@udecode/utils": { + "version": "47.2.7", + "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-47.2.7.tgz", + "integrity": "sha512-tQ8tIcdW+ZqWWrDgyf/moTLWtcErcHxaOfuCD/6qIL5hCq+jZm67nGHQToOT4Czti5Jr7CDPMgr8lYpdTEZcew==", + "license": "MIT" + }, + "node_modules/@platejs/core/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@platejs/core/node_modules/immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@platejs/core/node_modules/jotai-x": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/jotai-x/-/jotai-x-2.3.3.tgz", + "integrity": "sha512-ZeSPjf77VINlJ0HyMfYcPv/9psjB0CtJIZP6S+s/eefaO/9+U37M9Jx5dWmILgTe8hAol99EbAv6DDrHobOucA==", + "license": "MIT", + "peerDependencies": { + "@types/react": ">=17.0.0", + "jotai": ">=2.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@platejs/core/node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@platejs/core/node_modules/slate-hyperscript": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/slate-hyperscript/-/slate-hyperscript-0.115.0.tgz", + "integrity": "sha512-aaQ1XSfUhw0Lf4cwVLeNFYnnPsC9iX9aEmKvT5PAaGTNVe1LaBCAXB+CFuqp7YPExPj9hYuS5CsIu8dAh9JX2w==", + "license": "MIT", + "peerDependencies": { + "slate": ">=0.114.3" + } + }, + "node_modules/@platejs/core/node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@platejs/core/node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, + "node_modules/@platejs/core/node_modules/zustand-x": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/zustand-x/-/zustand-x-6.1.0.tgz", + "integrity": "sha512-lW1Fs29bLCrerWDa3lZLPuEn+ZkbSGzXdwdImKLJUtI2OqlDjpcFac5WTzCPs2ul/igwXFnGiKH1mdn+1Pl2mw==", + "license": "MIT", + "dependencies": { + "immer": "^10.0.3", + "lodash.mapvalues": "^4.6.0", + "mutative": "1.1.0", + "react-tracked": "^1.7.11", + "use-sync-external-store": "1.4.0" + }, + "peerDependencies": { + "zustand": ">=5.0.2" + } + }, + "node_modules/@platejs/floating": { + "version": "50.3.2", + "resolved": "https://registry.npmjs.org/@platejs/floating/-/floating-50.3.2.tgz", + "integrity": "sha512-XAA9NDjz4IVwa8EzVmwBLL5mdaHoW3+UNoKqvZ+p9IAJ0xsawaRDH4CFOEdLRTtMM8V6LT4LpZG29+iI1Hh8RQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.1", + "@floating-ui/react": "^0.27.12" + }, + "peerDependencies": { + "platejs": ">=49.2.21", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@platejs/floating/node_modules/@floating-ui/react": { + "version": "0.27.16", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz", + "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.6", + "@floating-ui/utils": "^0.2.10", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@platejs/indent": { + "version": "49.0.0", + "resolved": "https://registry.npmjs.org/@platejs/indent/-/indent-49.0.0.tgz", + "integrity": "sha512-CXgZEjpcHpvdSqqgjOmtfMMPBRhYDUQuYBFd+qmJovpfdfU9NBuNy0cv3JzSVyfQPvnUC7OEvQpwHHHsBsERyA==", + "license": "MIT", + "peerDependencies": { + "platejs": ">=49.0.0", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@platejs/link": { + "version": "50.3.2", + "resolved": "https://registry.npmjs.org/@platejs/link/-/link-50.3.2.tgz", + "integrity": "sha512-b6Pb1Rp5Uzwv02h8gS10sATyz6mVe/KTM60iXrssZkCHT+mojIc4HWgvD7vgLFLE21aU978n4kCIt5djEP6oLg==", + "license": "MIT", + "dependencies": { + "@platejs/floating": "50.3.2" + }, + "peerDependencies": { + "platejs": ">=49.2.21", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@platejs/list": { + "version": "50.2.0", + "resolved": "https://registry.npmjs.org/@platejs/list/-/list-50.2.0.tgz", + "integrity": "sha512-35kO7kTkeT4GFBoCUO2FRyLI5njxfv1LSZcnx+0CxSl6FwwdQkceDItiAnMLUDrO62e1afAu3+GIaS8AwJuDEw==", + "license": "MIT", + "dependencies": { + "@platejs/indent": "49.0.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "platejs": ">=49.2.21", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@platejs/list-classic": { + "version": "49.1.0", + "resolved": "https://registry.npmjs.org/@platejs/list-classic/-/list-classic-49.1.0.tgz", + "integrity": "sha512-ktN518ecE7oHUWBtHzX4TSmfXPKw1fYMOWinmVwv/uAYqnfwqWjrpO/FvnPENtN/WkLiisHO1aBe6KlXGlqadw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "platejs": ">=49.0.19", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@platejs/list/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@platejs/slate": { + "version": "49.2.21", + "resolved": "https://registry.npmjs.org/@platejs/slate/-/slate-49.2.21.tgz", + "integrity": "sha512-lvkdKRz18qxbuX8N8uXWHAGCkxPG2dldJmSSqFZkx8gCuIYGDGTWsAqKm9vdvpxVBHJ6j3cH4YiTQRRRNzKBpA==", + "license": "MIT", + "dependencies": { + "@udecode/utils": "47.2.7", + "is-plain-object": "^5.0.0", + "lodash": "^4.17.21", + "slate": "0.118.1", + "slate-dom": "0.118.1" + } + }, + "node_modules/@platejs/slate/node_modules/@udecode/utils": { + "version": "47.2.7", + "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-47.2.7.tgz", + "integrity": "sha512-tQ8tIcdW+ZqWWrDgyf/moTLWtcErcHxaOfuCD/6qIL5hCq+jZm67nGHQToOT4Czti5Jr7CDPMgr8lYpdTEZcew==", + "license": "MIT" + }, + "node_modules/@platejs/utils": { + "version": "49.2.21", + "resolved": "https://registry.npmjs.org/@platejs/utils/-/utils-49.2.21.tgz", + "integrity": "sha512-SbgXptdhcMP7mIiE88cwZpc2AC5+r/G4YDjBxnr4WabSE5DK0G2lL13zhopRoNKNRPqAKzpdIkpLg0Arc0DvKA==", + "license": "MIT", + "dependencies": { + "@platejs/core": "49.2.21", + "@platejs/slate": "49.2.21", + "@udecode/react-utils": "49.0.15", + "@udecode/utils": "47.2.7", + "clsx": "^2.1.1", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@platejs/utils/node_modules/@udecode/react-utils": { + "version": "49.0.15", + "resolved": "https://registry.npmjs.org/@udecode/react-utils/-/react-utils-49.0.15.tgz", + "integrity": "sha512-ra9e0WyECZEnOLyW1nf4pqGBBTLcktHfFhL+qlr7woMAwmNHs7HLbw/khoKfpSFt2RgjieE+QhawT6haFQAuhA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "^1.2.3", + "@udecode/utils": "47.2.7", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@platejs/utils/node_modules/@udecode/utils": { + "version": "47.2.7", + "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-47.2.7.tgz", + "integrity": "sha512-tQ8tIcdW+ZqWWrDgyf/moTLWtcErcHxaOfuCD/6qIL5hCq+jZm67nGHQToOT4Czti5Jr7CDPMgr8lYpdTEZcew==", + "license": "MIT" + }, + "node_modules/@platejs/utils/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -6708,285 +7036,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@udecode/plate-basic-marks": { - "version": "39.0.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-basic-marks/-/plate-basic-marks-39.0.0.tgz", - "integrity": "sha512-GtHFK1gwmhfnwl0Lf3xiRuNS832bNaelx5Sr/uzSVpH7Xo4p7Ssdxp+vc9LsiUrQcBQyuybvyYGbiY7i2o5DCA==", - "license": "MIT", - "peerDependencies": { - "@udecode/plate-common": ">=39.0.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.103.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.108.0" - } - }, - "node_modules/@udecode/plate-block-quote": { - "version": "39.0.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-block-quote/-/plate-block-quote-39.0.0.tgz", - "integrity": "sha512-5TxkIQFvYxX6CEOM0dtBnM/SX70kqXFHlz6ncEYC9hJnaNTpI6jiUqBfx5S2schVpFgtaxhi/0x17oTsFHsFMQ==", - "license": "MIT", - "peerDependencies": { - "@udecode/plate-common": ">=39.0.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.103.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.108.0" - } - }, - "node_modules/@udecode/plate-break": { - "version": "39.0.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-break/-/plate-break-39.0.0.tgz", - "integrity": "sha512-4H4p9zuGBgC/K5YC9Kywgfz1KhImz2WMZZmub4YzZMddOf3iVuhOT1+KfP1GDiEWVzBcKH2R6iIxJ6rqWDsyGw==", - "license": "MIT", - "peerDependencies": { - "@udecode/plate-common": ">=39.0.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.103.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.108.0" - } - }, - "node_modules/@udecode/plate-common": { - "version": "39.2.21", - "resolved": "https://registry.npmjs.org/@udecode/plate-common/-/plate-common-39.2.21.tgz", - "integrity": "sha512-K+Hm5GEeO8YJJP1kRWErCbxDkOUmssnIR3xulcLEhIdUhoEEaojmNa2tfJ1H9mvmgM2JJgy5ZWcBqQES/KgwWg==", - "license": "MIT", - "dependencies": { - "@udecode/plate-core": "39.2.21", - "@udecode/plate-utils": "39.2.21", - "@udecode/react-hotkeys": "37.0.0", - "@udecode/react-utils": "39.0.0", - "@udecode/slate": "39.2.1", - "@udecode/slate-react": "39.2.1", - "@udecode/slate-utils": "39.2.20", - "@udecode/utils": "37.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.103.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.108.0" - } - }, - "node_modules/@udecode/plate-core": { - "version": "39.2.21", - "resolved": "https://registry.npmjs.org/@udecode/plate-core/-/plate-core-39.2.21.tgz", - "integrity": "sha512-/+KEW5M55xrTaqofcUFX3KZOAtRza9TQKQC7DNg867gHo0qFR8m8qKs5l83OAoWnzYkM4Lj+0Sirui2RCr40Wg==", - "license": "MIT", - "dependencies": { - "@udecode/react-hotkeys": "37.0.0", - "@udecode/react-utils": "39.0.0", - "@udecode/slate": "39.2.1", - "@udecode/slate-react": "39.2.1", - "@udecode/slate-utils": "39.2.20", - "@udecode/utils": "37.0.0", - "clsx": "^2.1.1", - "is-hotkey": "^0.2.0", - "jotai": "~2.8.4", - "jotai-optics": "0.4.0", - "jotai-x": "1.2.4", - "lodash": "^4.17.21", - "nanoid": "^3.3.7", - "optics-ts": "2.4.1", - "use-deep-compare": "^1.3.0", - "zustand": "^4.5.5", - "zustand-x": "^3.0.4" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.103.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.108.0" - } - }, - "node_modules/@udecode/plate-core/node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@udecode/plate-floating": { - "version": "39.1.6", - "resolved": "https://registry.npmjs.org/@udecode/plate-floating/-/plate-floating-39.1.6.tgz", - "integrity": "sha512-oHy8Zfs5JMNkk1Slnv6BR+4LYQV0oFTWinfkJY7vSDs+dzdnHvbSfnW7/TFxt/yPMvK8CA5qCTckfSIt8kQKQg==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.6.7", - "@floating-ui/react": "^0.26.23" - }, - "peerDependencies": { - "@udecode/plate-common": ">=39.1.4", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.103.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.108.0" - } - }, - "node_modules/@udecode/plate-heading": { - "version": "39.0.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-heading/-/plate-heading-39.0.0.tgz", - "integrity": "sha512-J7No90ttd/2sXbbRnHZ9EgNR//A91UZ57bb15quYE+rlGSyuk1eANzMX5vbmgWMIQ8O53vSqWC12clo9sVaVZA==", - "license": "MIT", - "peerDependencies": { - "@udecode/plate-common": ">=39.0.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.103.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.108.0" - } - }, - "node_modules/@udecode/plate-link": { - "version": "39.1.9", - "resolved": "https://registry.npmjs.org/@udecode/plate-link/-/plate-link-39.1.9.tgz", - "integrity": "sha512-GzPUFsXjceZ6truQm6lCRI+2iW0SwdoA0P8sJL4VqxDlSULT//Y4+m8Sh5d6MpxHuDjOeBGdOIq2g2Qqt3vokA==", - "license": "MIT", - "dependencies": { - "@udecode/plate-floating": "39.1.6", - "@udecode/plate-normalizers": "39.0.0" - }, - "peerDependencies": { - "@udecode/plate-common": ">=39.1.8", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.103.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.108.0" - } - }, - "node_modules/@udecode/plate-list": { - "version": "39.0.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-list/-/plate-list-39.0.0.tgz", - "integrity": "sha512-dwtW9r6vZOF0hvGaCsj5haNSB9GX4/RhpBI8gyFlGbOpUPqHdD38VWnFaOjrTQISt3X5CrLSmQJFI9Z7QLyawQ==", - "license": "MIT", - "dependencies": { - "@udecode/plate-reset-node": "39.0.0", - "lodash": "^4.17.21" - }, - "peerDependencies": { - "@udecode/plate-common": ">=39.0.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.103.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.108.0" - } - }, - "node_modules/@udecode/plate-normalizers": { - "version": "39.0.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-normalizers/-/plate-normalizers-39.0.0.tgz", - "integrity": "sha512-2awYYNcjbQovg0UUTMy6B6hTdva77BIyp2Ou8AlJbZqYHpN+Z/5JCNuRdkMBSDbDImRUsFo4I2jx0I9b6IG4fA==", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.21" - }, - "peerDependencies": { - "@udecode/plate-common": ">=39.0.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.103.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.108.0" - } - }, - "node_modules/@udecode/plate-paragraph": { - "version": "36.0.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-paragraph/-/plate-paragraph-36.0.0.tgz", - "integrity": "sha512-Zbm76VygSfj4hkP1kfjwaYZisvmF3XP79f1uVTieQfcx/16s+Ln4BCVasCCbS+PO94yjsujW+ww05bUzGqRxpA==", - "license": "MIT", - "peerDependencies": { - "@udecode/plate-common": ">=36.0.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.94.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.99.0" - } - }, - "node_modules/@udecode/plate-reset-node": { - "version": "39.0.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-reset-node/-/plate-reset-node-39.0.0.tgz", - "integrity": "sha512-x1utEQIzDaQRkAAuG4GJvm7pUpiPqS70K6no/2dCV3iXCQDi2eeeZsIB6fBk30yymFEXx9POPYNYRoujtH+Cbw==", - "license": "MIT", - "peerDependencies": { - "@udecode/plate-common": ">=39.0.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.103.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.108.0" - } - }, - "node_modules/@udecode/plate-trailing-block": { - "version": "39.0.0", - "resolved": "https://registry.npmjs.org/@udecode/plate-trailing-block/-/plate-trailing-block-39.0.0.tgz", - "integrity": "sha512-OXBzZ9pGFhJeSWtKrUIffMxsUV00pVOxPL4YRmn8i4nJxEBlLEgcfPK6vfU2WUczgBmYiM++W8y4rZOWBsdyqg==", - "license": "MIT", - "peerDependencies": { - "@udecode/plate-common": ">=39.0.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.103.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.108.0" - } - }, - "node_modules/@udecode/plate-utils": { - "version": "39.2.21", - "resolved": "https://registry.npmjs.org/@udecode/plate-utils/-/plate-utils-39.2.21.tgz", - "integrity": "sha512-EEjUAKdzCqso92wqrqetA0cqYObjSISvI/M3dOrZ4Aij4IaMj/f1WBP6nQoDdpjXXmLnVeWT6Px0Gbuma3EcpA==", - "license": "MIT", - "dependencies": { - "@udecode/plate-core": "39.2.21", - "@udecode/react-utils": "39.0.0", - "@udecode/slate": "39.2.1", - "@udecode/slate-react": "39.2.1", - "@udecode/slate-utils": "39.2.20", - "@udecode/utils": "37.0.0", - "clsx": "^2.1.1", - "lodash": "^4.17.21" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.103.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.110.0" - } - }, - "node_modules/@udecode/plate-utils/node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/@udecode/react-hotkeys": { "version": "37.0.0", "resolved": "https://registry.npmjs.org/@udecode/react-hotkeys/-/react-hotkeys-37.0.0.tgz", @@ -6997,83 +7046,6 @@ "react-dom": ">=16.8.0" } }, - "node_modules/@udecode/react-utils": { - "version": "39.0.0", - "resolved": "https://registry.npmjs.org/@udecode/react-utils/-/react-utils-39.0.0.tgz", - "integrity": "sha512-EoX6T7VmQe9bcR2bIqoobcsX66vo45XKt26rY4eJPWjaTys3yGdyD2iMDy/mEYFFh8ZOUC1V+sNw+XBwQOgyCw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "^1.1.0", - "@udecode/utils": "37.0.0", - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@udecode/react-utils/node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@udecode/slate": { - "version": "39.2.1", - "resolved": "https://registry.npmjs.org/@udecode/slate/-/slate-39.2.1.tgz", - "integrity": "sha512-TfBZKNyAr/If5cMBEdacitmn7nUfL81qD56Y5gn1fGsI5jBIR1e3XMA4jSWG1s3I7EK6/KN8xIas/lmX73b5RQ==", - "license": "MIT", - "dependencies": { - "@udecode/utils": "37.0.0", - "is-plain-object": "^5.0.0" - }, - "peerDependencies": { - "slate": ">=0.103.0", - "slate-history": ">=0.93.0" - } - }, - "node_modules/@udecode/slate-react": { - "version": "39.2.1", - "resolved": "https://registry.npmjs.org/@udecode/slate-react/-/slate-react-39.2.1.tgz", - "integrity": "sha512-19MiTlF8WWwTR23Um428DI09JzPDdlvDp0pcAL4TzaOB14nAjUvCUCNsojF21H0ovcJpEFEiStD76jkOuqKXbA==", - "license": "MIT", - "dependencies": { - "@udecode/react-utils": "39.0.0", - "@udecode/slate": "39.2.1", - "@udecode/utils": "37.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.103.0", - "slate-history": ">=0.93.0", - "slate-react": ">=0.108.0" - } - }, - "node_modules/@udecode/slate-utils": { - "version": "39.2.20", - "resolved": "https://registry.npmjs.org/@udecode/slate-utils/-/slate-utils-39.2.20.tgz", - "integrity": "sha512-hKI0UOzcvcRgT/iRD9ZzuZ4taQouAk7ER7nxQdsNbX2KKHyjuAyREuvVEBU0HOB3X6aeYEDEZUukDJwVKZIBVg==", - "license": "MIT", - "dependencies": { - "@udecode/slate": "39.2.1", - "@udecode/utils": "37.0.0", - "lodash": "^4.17.21" - }, - "peerDependencies": { - "slate": ">=0.103.0", - "slate-history": ">=0.93.0" - } - }, - "node_modules/@udecode/utils": { - "version": "37.0.0", - "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-37.0.0.tgz", - "integrity": "sha512-30ixi2pznIXyIqpFocX+X5Sj38js+wZ0RLY14eZv1C1zwWo5BxSuJfzpGQTvGcLPJnij019tEpmGH61QdDxtrQ==", - "license": "MIT" - }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "dev": true, @@ -15431,7 +15403,6 @@ }, "node_modules/html-entities": { "version": "2.6.0", - "dev": true, "funding": [ { "type": "github", @@ -18159,25 +18130,6 @@ "optics-ts": ">=2.0.0" } }, - "node_modules/jotai-x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/jotai-x/-/jotai-x-1.2.4.tgz", - "integrity": "sha512-FyLrAR/ZDtmaWgif4cNRuJvMam/RSFv+B11/p4T427ws/T+8WhZzwmULwNogG6ZbZq+v1XpH6f9aN1lYqY5dLg==", - "license": "MIT", - "peerDependencies": { - "@types/react": ">=17.0.0", - "jotai": ">=2.0.0", - "react": ">=17.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react": { - "optional": true - } - } - }, "node_modules/jquery": { "version": "3.7.1", "license": "MIT" @@ -21104,12 +21056,22 @@ "node": ">=8" } }, + "node_modules/mutative": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mutative/-/mutative-1.1.0.tgz", + "integrity": "sha512-2PJADREjOusk3iJkD3rXV2YjAxTuaLxdfqtqTEt6vcY07LtEBR1seHuBHXWEIuscqRDGvbauYPs+A4Rj/KTczQ==", + "license": "MIT", + "engines": { + "node": ">=14.0" + } + }, "node_modules/mute-stream": { "version": "0.0.8", "license": "ISC" }, "node_modules/nanoid": { "version": "3.3.11", + "dev": true, "funding": [ { "type": "github", @@ -26326,6 +26288,54 @@ "node": ">=10" } }, + "node_modules/platejs": { + "version": "49.2.21", + "resolved": "https://registry.npmjs.org/platejs/-/platejs-49.2.21.tgz", + "integrity": "sha512-JK3W4WxEGOW7W+GHMH1Wro46mVfkbKgFlj0DrajDv4HmmK7FQVu9IkVdeR97IbimM76GafoHglbqlJrLM7CYIw==", + "license": "MIT", + "dependencies": { + "@platejs/core": "49.2.21", + "@platejs/slate": "49.2.21", + "@platejs/utils": "49.2.21", + "@udecode/react-hotkeys": "37.0.0", + "@udecode/react-utils": "49.0.15", + "@udecode/utils": "47.2.7" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/platejs/node_modules/@udecode/react-utils": { + "version": "49.0.15", + "resolved": "https://registry.npmjs.org/@udecode/react-utils/-/react-utils-49.0.15.tgz", + "integrity": "sha512-ra9e0WyECZEnOLyW1nf4pqGBBTLcktHfFhL+qlr7woMAwmNHs7HLbw/khoKfpSFt2RgjieE+QhawT6haFQAuhA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "^1.2.3", + "@udecode/utils": "47.2.7", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/platejs/node_modules/@udecode/utils": { + "version": "47.2.7", + "resolved": "https://registry.npmjs.org/@udecode/utils/-/utils-47.2.7.tgz", + "integrity": "sha512-tQ8tIcdW+ZqWWrDgyf/moTLWtcErcHxaOfuCD/6qIL5hCq+jZm67nGHQToOT4Czti5Jr7CDPMgr8lYpdTEZcew==", + "license": "MIT" + }, + "node_modules/platejs/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/platform": { "version": "1.3.3", "dev": true, @@ -32236,15 +32246,6 @@ } } }, - "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/utf-8-validate": { "version": "5.0.10", "dev": true, @@ -33522,58 +33523,6 @@ "version": "1.14.1", "license": "0BSD" }, - "node_modules/zustand": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } - }, - "node_modules/zustand-x": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/zustand-x/-/zustand-x-3.0.4.tgz", - "integrity": "sha512-dVD8WUEpR/0mMdLah9j8i+r6PMAq9Ii2u+BX/9Bn4MHRt8sSnRQ90YMUlTVonZYAHGb2UHZwPpE2gMb8GtYDDw==", - "license": "MIT", - "dependencies": { - "immer": "^10.0.3", - "lodash.mapvalues": "^4.6.0", - "react-tracked": "^1.7.11" - }, - "peerDependencies": { - "zustand": ">=4.3.9" - } - }, - "node_modules/zustand-x/node_modules/immer": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", - "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, "node_modules/zwitch": { "version": "1.0.5", "license": "MIT", @@ -34456,23 +34405,15 @@ "version": "3.1.0", "license": "MIT", "dependencies": { - "@udecode/plate-basic-marks": "^39.0.0", - "@udecode/plate-block-quote": "^39.0.0", - "@udecode/plate-break": "^39.0.0", - "@udecode/plate-common": "^39.1.8", - "@udecode/plate-heading": "^39.0.0", - "@udecode/plate-link": "^39.1.9", - "@udecode/plate-list": "^39.0.0", - "@udecode/plate-paragraph": "^36.0.0", - "@udecode/plate-trailing-block": "^39.0.0", + "@platejs/basic-nodes": "^49.0.0", + "@platejs/link": "^50.2.7", + "@platejs/list": "^50.2.0", + "@platejs/list-classic": "^49.1.0", "class-variance-authority": "^0.7.0", "lucide-react": "^0.331.0", + "platejs": "^49.2.21", "remark-parse": "^6.0.3", "remark-rehype": "^4.0.0", - "slate": "^0.110.2", - "slate-history": "^0.100.0", - "slate-hyperscript": "^0.100.0", - "slate-react": "^0.110.2", "unified": "^9.0.0" }, "peerDependencies": { @@ -34487,16 +34428,6 @@ "react-immutable-proptypes": "^2.1.0" } }, - "packages/decap-cms-widget-richtext/node_modules/immer": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", - "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, "packages/decap-cms-widget-richtext/node_modules/mdast-util-to-hast": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-4.0.0.tgz", @@ -34562,55 +34493,6 @@ "mdast-util-to-hast": "^4.0.0" } }, - "packages/decap-cms-widget-richtext/node_modules/slate": { - "version": "0.110.2", - "resolved": "https://registry.npmjs.org/slate/-/slate-0.110.2.tgz", - "integrity": "sha512-4xGULnyMCiEQ0Ml7JAC1A6HVE6MNpPJU7Eq4cXh1LxlrR0dFXC3XC+rNfQtUJ7chHoPkws57x7DDiWiZAt+PBA==", - "license": "MIT", - "dependencies": { - "immer": "^10.0.3", - "is-plain-object": "^5.0.0", - "tiny-warning": "^1.0.3" - } - }, - "packages/decap-cms-widget-richtext/node_modules/slate-history": { - "version": "0.100.0", - "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.100.0.tgz", - "integrity": "sha512-x5rUuWLNtH97hs9PrFovGgt3Qc5zkTm/5mcUB+0NR/TK923eLax4HsL6xACLHMs245nI6aJElyM1y6hN0y5W/Q==", - "license": "MIT", - "dependencies": { - "is-plain-object": "^5.0.0" - }, - "peerDependencies": { - "slate": ">=0.65.3" - } - }, - "packages/decap-cms-widget-richtext/node_modules/slate-react": { - "version": "0.110.3", - "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.110.3.tgz", - "integrity": "sha512-AS8PPjwmsFS3Lq0MOEegLVlFoxhyos68G6zz2nW4sh3WeTXV7pX0exnwtY1a/docn+J3LGQO11aZXTenPXA/kg==", - "license": "MIT", - "dependencies": { - "@juggle/resize-observer": "^3.4.0", - "direction": "^1.0.4", - "is-hotkey": "^0.2.0", - "is-plain-object": "^5.0.0", - "lodash": "^4.17.21", - "scroll-into-view-if-needed": "^3.1.0", - "tiny-invariant": "1.3.1" - }, - "peerDependencies": { - "react": ">=18.2.0", - "react-dom": ">=18.2.0", - "slate": ">=0.99.0" - } - }, - "packages/decap-cms-widget-richtext/node_modules/tiny-invariant": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", - "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==", - "license": "MIT" - }, "packages/decap-cms-widget-select": { "version": "3.3.0", "license": "MIT", diff --git a/packages/decap-cms-widget-richtext/package.json b/packages/decap-cms-widget-richtext/package.json index e9a38644f66b..4da3be9dcc6e 100644 --- a/packages/decap-cms-widget-richtext/package.json +++ b/packages/decap-cms-widget-richtext/package.json @@ -21,21 +21,13 @@ "build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward --copy-files --extensions \".js,.jsx,.ts,.tsx\"" }, "dependencies": { - "@udecode/plate-basic-marks": "^39.0.0", - "@udecode/plate-block-quote": "^39.0.0", - "@udecode/plate-break": "^39.0.0", - "@udecode/plate-common": "^39.1.8", - "@udecode/plate-heading": "^39.0.0", - "@udecode/plate-link": "^39.1.9", - "@udecode/plate-list": "^39.0.0", - "@udecode/plate-paragraph": "^36.0.0", - "@udecode/plate-trailing-block": "^39.0.0", + "@platejs/basic-nodes": "^49.0.0", + "@platejs/link": "^50.2.7", + "@platejs/list": "^50.2.0", + "@platejs/list-classic": "^49.1.0", "class-variance-authority": "^0.7.0", "lucide-react": "^0.331.0", - "slate": "^0.110.2", - "slate-history": "^0.100.0", - "slate-hyperscript": "^0.100.0", - "slate-react": "^0.110.2", + "platejs": "^49.2.21", "remark-parse": "^6.0.3", "remark-rehype": "^4.0.0", "unified": "^9.0.0" diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index e058a1778f43..1768fb6e950e 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -1,13 +1,19 @@ import React from 'react'; -import { usePlateEditor, Plate, ParagraphPlugin, PlateLeaf } from '@udecode/plate-common/react'; -import { BoldPlugin, ItalicPlugin, CodePlugin } from '@udecode/plate-basic-marks/react'; -import { HeadingPlugin } from '@udecode/plate-heading/react'; -import { HEADING_KEYS } from '@udecode/plate-heading'; -import { SoftBreakPlugin, ExitBreakPlugin } from '@udecode/plate-break/react'; -import { ListPlugin } from '@udecode/plate-list/react'; -import { LinkPlugin } from '@udecode/plate-link/react'; -import { BlockquotePlugin } from '@udecode/plate-block-quote/react'; -import { TrailingBlockPlugin } from '@udecode/plate-trailing-block'; +import { KEYS, TrailingBlockPlugin } from 'platejs'; +import { + usePlateEditor, + Plate, + ParagraphPlugin, + PlateLeaf, +} from 'platejs/react'; +import { + BoldPlugin, + ItalicPlugin, + CodePlugin, + HeadingPlugin, +} from '@platejs/basic-nodes/react'; +import { ListPlugin } from '@platejs/list-classic/react'; +import { LinkPlugin } from '@platejs/link/react'; import { ClassNames } from '@emotion/react'; import { fonts, lengths, zIndex } from 'decap-cms-ui-default'; import { fromJS } from 'immutable'; @@ -23,8 +29,8 @@ import HeadingElement from './components/Element/HeadingElement'; import ListElement from './components/Element/ListElement'; import BlockquoteElement from './components/Element/BlockquoteElement'; import LinkElement from './components/Element/LinkElement'; -import BlockquoteExtPlugin from './plugins/BlockquoteExtPlugin'; -import ShortcodePlugin from './plugins/ShortcodePlugin'; +// import ShortcodePlugin from './plugins/ShortcodePlugin'; +import ExtendedBlockquotePlugin from './plugins/ExtendedBlockquotePlugin'; // import ShortcodeElement from './components/Element/ShortcodeElement'; function visualEditorStyles({ minimal }) { @@ -80,6 +86,22 @@ const emptyValue = [ type: ParagraphPlugin.key, children: [{ text: '' }], }, + // { + // children: [{ text: 'Title' }], + // type: 'h3', + // }, + // { + // children: [{ text: 'This is a quote.' }], + // type: 'blockquote', + // }, + // { + // children: [ + // { text: 'With some ' }, + // { bold: true, text: 'bold' }, + // { text: ' text for emphasis!' }, + // ], + // type: 'p', + // }, ]; export default function VisualEditor(props) { @@ -108,7 +130,7 @@ export default function VisualEditor(props) { } function handleChange({ value }) { - console.log('handleChange', value); + // console.log('handleChange', value); const mdValue = slateToMarkdown(value, {}, editorComponents); onChange(mdValue); } @@ -122,14 +144,12 @@ export default function VisualEditor(props) { [CodePlugin.key]: CodeLeaf, [ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }), [ParagraphPlugin.key]: ParagraphElement, - [BlockquotePlugin.key]: BlockquoteElement, - [LinkPlugin.key]: LinkElement, - [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }), - [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }), - [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }), - [HEADING_KEYS.h4]: withProps(HeadingElement, { variant: 'h4' }), - [HEADING_KEYS.h5]: withProps(HeadingElement, { variant: 'h5' }), - [HEADING_KEYS.h6]: withProps(HeadingElement, { variant: 'h6' }), + [KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }), + [KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }), + [KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }), + [KEYS.h4]: withProps(HeadingElement, { variant: 'h4' }), + [KEYS.h5]: withProps(HeadingElement, { variant: 'h5' }), + [KEYS.h6]: withProps(HeadingElement, { variant: 'h6' }), ['ul']: withProps(ListElement, { variant: 'ul' }), ['ol']: withProps(ListElement, { variant: 'ol' }), ['li']: withProps(ListElement, { variant: 'li' }), @@ -142,46 +162,15 @@ export default function VisualEditor(props) { ItalicPlugin, CodePlugin, ListPlugin, - LinkPlugin, - BlockquotePlugin, - BlockquoteExtPlugin, - ShortcodePlugin, - TrailingBlockPlugin.configure({ - options: { type: 'p' }, + LinkPlugin.configure({ + node: { component: LinkElement }, }), - SoftBreakPlugin.configure({ - rules: [ - { hotkey: 'shift+enter' }, - { - hotkey: 'enter', - query: { - allow: [BlockquotePlugin.key], - }, - }, - ], + ExtendedBlockquotePlugin.configure({ + node: { component: BlockquoteElement } }), - ExitBreakPlugin.configure({ - options: { - rules: [ - { - hotkey: 'mod+enter', - }, - { - hotkey: 'mod+shift+enter', - before: true, - }, - { - hotkey: 'enter', - query: { - start: true, - end: true, - allow: Object.values(HEADING_KEYS), - }, - relative: true, - level: 1, - }, - ], - }, + // ShortcodePlugin, + TrailingBlockPlugin.configure({ + options: { type: ParagraphPlugin.key }, }), ], value: initialValue, diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Editor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Editor.js index 4b2bc1da4e48..2d4f912f55f5 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Editor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Editor.js @@ -1,5 +1,5 @@ import React from 'react'; -import { PlateContent } from '@udecode/plate-common/react'; +import { PlateContent } from 'platejs/react'; import { ClassNames } from '@emotion/react'; function Editor(props) { diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/BlockquoteElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/BlockquoteElement.js index 1ab7a6a9115b..c99ce7aa14f0 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/BlockquoteElement.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/BlockquoteElement.js @@ -1,7 +1,7 @@ import React from 'react'; -import { PlateElement } from '@udecode/plate-common/react'; import styled from '@emotion/styled'; import { colors } from 'decap-cms-ui-default'; +import { PlateElement } from 'platejs/react'; const bottomMargin = '16px'; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/HeadingElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/HeadingElement.js index f6c81c7963c5..975b34800f41 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/HeadingElement.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/HeadingElement.js @@ -1,5 +1,5 @@ import React from 'react'; -import { PlateElement } from '@udecode/plate-common/react'; +import { PlateElement } from 'platejs/react'; import styled from '@emotion/styled'; const headingVariants = { @@ -28,7 +28,7 @@ const headingVariants = { }, }; -const StyledHeading = styled(PlateElement)` +const StyledHeading = styled.h1` font-weight: 700; line-height: 1; margin-top: ${props => (props.isFirstBlock ? '0' : headingVariants[props.variant].marginTop)}; @@ -38,13 +38,12 @@ const StyledHeading = styled(PlateElement)` function HeadingElement({ variant = 'h1', children, ...props }) { const { element, editor } = props; const isFirstBlock = element === editor.children[0]; - - const Element = StyledHeading.withComponent(variant); - return ( - - {children} - + + + {children} + + ); } diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/LinkElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/LinkElement.js index 747fa7fe277b..85d885812f5b 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/LinkElement.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/LinkElement.js @@ -1,7 +1,7 @@ import React from 'react'; -import { PlateElement, useElement } from '@udecode/plate-common/react'; -import { useLink } from '@udecode/plate-link/react'; import styled from '@emotion/styled'; +import { PlateLeaf, useElement } from 'platejs/react'; +import { useLink } from '@platejs/link/react'; const StyledA = styled.a` text-decoration: underline; @@ -12,9 +12,9 @@ function LinkElement({ children, ...rest }) { const element = useElement(); const { props } = useLink({ element }); return ( - + {children} - + ); } diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ListElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ListElement.js index 1ec46fa31a5f..3224890c1c8a 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ListElement.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ListElement.js @@ -1,26 +1,28 @@ import React from 'react'; import styled from '@emotion/styled'; -import { PlateElement } from '@udecode/plate-common/react'; +import { PlateElement } from 'platejs/react'; const bottomMargin = '16px'; -const StyledList = styled(PlateElement)` +const StyledList = styled.li` margin-bottom: ${bottomMargin}; padding-left: 30px; `; -const StyledListElement = styled(PlateElement)` +const StyledListElement = styled.ul` margin-top: 8px; margin-bottom: 8px; `; function ListElement({ children, variant, ...props }) { - const Element = (variant == 'li' ? StyledListElement : StyledList).withComponent(variant); + const Element = variant == 'li' ? StyledListElement : StyledList; return ( - - {children} - + + + {children} + + ); } diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ParagraphElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ParagraphElement.js index 895fa7258e12..f9a82f1c6436 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ParagraphElement.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ParagraphElement.js @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { PlateElement } from '@udecode/plate-common/react'; +import { PlateElement } from 'platejs/react'; const bottomMargin = '16px'; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ShortcodeElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ShortcodeElement.js index 68c7912ef768..f722351228ea 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ShortcodeElement.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ShortcodeElement.js @@ -1,12 +1,11 @@ import React, { useState } from 'react'; -import { insertElements, setNodes } from '@udecode/plate-common'; -import { findNodePath, ParagraphPlugin, PlateElement, useEditorRef } from '@udecode/plate-common/react'; import styled from '@emotion/styled'; import { fromJS } from 'immutable'; import { omit } from 'lodash'; import { css } from '@emotion/react'; -import { Range } from 'slate'; +import { Range, setNodes } from 'slate'; import { zIndex } from 'decap-cms-ui-default'; +import { ParagraphPlugin, PlateElement, useEditorRef } from 'platejs/react'; import { useEditorContext } from '../../editorContext'; @@ -37,7 +36,7 @@ function ShortcodeElement(props) { const field = fromJS(omit(plugin, fieldKeys)); const [value, setValue] = useState(fromJS(element?.data[dataKey] ?? { id: '' })); - const path = findNodePath(editor, element); + const path = editor.api.findPath(element); const isSelected = editor.selection && path && @@ -60,8 +59,7 @@ function ShortcodeElement(props) { } function handleInsertBefore() { - insertElements( - editor, + editor.tf.insertNodes( { type: ParagraphPlugin.key, children: [{ text: '' }] }, { at: path, select: true }, ); diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Leaf/CodeLeaf.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Leaf/CodeLeaf.js index 5310b2dc0703..37545a8a78a6 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Leaf/CodeLeaf.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Leaf/CodeLeaf.js @@ -1,7 +1,7 @@ import React from 'react'; -import { PlateLeaf } from '@udecode/plate-common/react'; import styled from '@emotion/styled'; import { colors, lengths } from 'decap-cms-ui-default'; +import { PlateLeaf } from 'platejs/react'; const StyledCode = styled.code` background-color: ${colors.background}; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js index f00990ad2865..ac70ac191f99 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/BlockquoteToolbarButton.js @@ -1,10 +1,7 @@ import React from 'react'; -import { findNode } from '@udecode/plate-common'; -import { useEditorRef, focusEditor, useEditorSelector } from '@udecode/plate-common/react'; -import { unwrapList } from '@udecode/plate-list'; -import { toggleWrapNodes } from '@udecode/slate-utils'; -import { BlockquotePlugin } from '@udecode/plate-block-quote/react'; - +import { useEditorRef, useEditorSelector } from 'platejs/react'; +import { BlockquotePlugin } from '@platejs/basic-nodes/react'; +import { unwrapList } from '@platejs/list-classic'; import ToolbarButton from './ToolbarButton'; @@ -12,14 +9,14 @@ function BlockquoteToolbarButton(props) { const editor = useEditorRef(); const pressed = useEditorSelector( - editor => !!findNode(editor, { match: { type: BlockquotePlugin.key } }), + editor => !!editor.api.node({ match: { type: BlockquotePlugin.key } }), [], ); function handleClick() { unwrapList(editor); - toggleWrapNodes(editor, BlockquotePlugin.key); - focusEditor(editor); + editor.tf.toggleBlock(BlockquotePlugin.key, { wrap: true }); + editor.tf.focus(); } return ; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/EditorComponentsToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/EditorComponentsToolbarButton.js index 7a9acfcc900b..43655bca47d1 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/EditorComponentsToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/EditorComponentsToolbarButton.js @@ -4,8 +4,7 @@ import styled from '@emotion/styled'; import { Dropdown, DropdownButton, DropdownItem } from 'decap-cms-ui-default'; import { List } from 'immutable'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { insertElements, isSelectionAtBlockEnd, isSelectionAtBlockStart, setBlockAboveNode } from '@udecode/plate-common'; -import { useEditorRef } from '@udecode/plate-common/react'; +import { useEditorRef } from 'platejs/react'; import ToolbarButton from './ToolbarButton'; @@ -17,31 +16,29 @@ const ToolbarDropdownWrapper = styled.div` function EditorComponentsToolbarButton({ disabled, editorComponents, allowedEditorComponents, t }) { const editor = useEditorRef(); - const handleChange = useCallback( plugin => { - const defaultValues = plugin.fields .toMap() .mapKeys((_, field) => field.get('name')) .filter(field => field.has('default')) .map(field => field.get('default')); - if (isSelectionAtBlockEnd(editor) && isSelectionAtBlockStart(editor)) { - setBlockAboveNode(editor, { - children: [{ text: '' }], - type: 'shortcode', - id: plugin.id, - data: { - shortcode: plugin.id, - shortcodeNew: true, - shortcodeData: defaultValues.toJS(), - }, - }) + if (editor.api.isAt({ end: true }) && editor.api.isAt({ start: true })) { + // setBlockAboveNode(editor, { // find alternative for removed setBlockAboveNode + // children: [{ text: '' }], + // type: 'shortcode', + // id: plugin.id, + // data: { + // shortcode: plugin.id, + // shortcodeNew: true, + // shortcodeData: defaultValues.toJS(), + // }, + // }) return; } - insertElements(editor, { + editor.tf.insertNodes({ children: [{ text: '' }], type: 'shortcode', id: plugin.id, diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js index 6cc2896dc7ca..14216e24bd4a 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/HeadingToolbarButton.js @@ -1,10 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled from '@emotion/styled'; -import { unwrapList } from '@udecode/plate-list'; +import { unwrapList } from '@platejs/list-classic'; import { Dropdown, DropdownButton, DropdownItem } from 'decap-cms-ui-default'; -import { focusEditor, ParagraphPlugin, useEditorRef, useEditorSelector } from '@udecode/plate-common/react'; -import { getBlockAbove, isSelectionExpanded, toggleBlock } from '@udecode/plate-common'; +import { ParagraphPlugin, useEditorRef, useEditorSelector } from 'platejs/react'; import ToolbarButton from './ToolbarButton'; @@ -26,8 +25,8 @@ function HeadingToolbarButton({ disabled, isVisible, t }) { const editor = useEditorRef(); const value = useEditorSelector(editor => { - if (!isSelectionExpanded(editor)) { - const entry = getBlockAbove(editor); + if (!editor.api.isExpanded()) { + const entry = editor.api.block(); if (entry) { return entry[0].type; @@ -39,8 +38,8 @@ function HeadingToolbarButton({ disabled, isVisible, t }) { function handleChange(optionKey) { unwrapList(editor); - toggleBlock(editor, { type: optionKey }); - focusEditor(editor); + editor.tf.toggleBlock(optionKey); + editor.tf.focus(); } return ( diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/LinkToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/LinkToolbarButton.js index 7fd1d42f5313..5b11df72bc6b 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/LinkToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/LinkToolbarButton.js @@ -1,7 +1,7 @@ import React from 'react'; -import { useLinkToolbarButton, useLinkToolbarButtonState } from '@udecode/plate-link/react'; -import { upsertLink, unwrapLink } from '@udecode/plate-link'; -import { useEditorRef } from '@udecode/plate-common/react'; +import { useEditorRef } from 'platejs/react'; +import { useLinkToolbarButton, useLinkToolbarButtonState } from '@platejs/link/react'; +import { unwrapLink, upsertLink } from '@platejs/link'; import ToolbarButton from './ToolbarButton'; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/ListToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/ListToolbarButton.js index ef48cc1f06c2..730c7918f446 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/ListToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/ListToolbarButton.js @@ -1,5 +1,5 @@ import React from 'react'; -import { useListToolbarButton, useListToolbarButtonState } from '@udecode/plate-list/react'; +import { useListToolbarButton, useListToolbarButtonState } from '@platejs/list-classic/react'; import ToolbarButton from './ToolbarButton'; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/MarkToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/MarkToolbarButton.js index 13a943313e6f..f594ce99630f 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/MarkToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/MarkToolbarButton.js @@ -1,5 +1,5 @@ import React from 'react'; -import { useMarkToolbarButton, useMarkToolbarButtonState } from '@udecode/plate-common/react'; +import { useMarkToolbarButton, useMarkToolbarButtonState } from 'platejs/react'; import ToolbarButton from './ToolbarButton'; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js index 63fc6ee4f500..7c3c63c8ecfe 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/Toolbar.js @@ -3,8 +3,8 @@ import PropTypes from 'prop-types'; import styled from '@emotion/styled'; import { List } from 'immutable'; import { colors, transitions } from 'decap-cms-ui-default'; -import { BoldPlugin, ItalicPlugin, CodePlugin } from '@udecode/plate-basic-marks/react'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { BoldPlugin, CodePlugin, ItalicPlugin } from '@platejs/basic-nodes/react'; import MarkToolbarButton from './MarkToolbarButton'; import HeadingToolbarButton from './HeadingToolbarButton'; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/BlockquoteExtPlugin.js b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/BlockquoteExtPlugin.js deleted file mode 100644 index 27b1dce3c798..000000000000 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/BlockquoteExtPlugin.js +++ /dev/null @@ -1,58 +0,0 @@ -import { BlockquotePlugin } from '@udecode/plate-block-quote/react'; -import { - getBlockAbove, - isAncestorEmpty, - unwrapNodes, - isFirstChild, - isSelectionAtBlockStart, -} from '@udecode/plate-common'; -import { createPlatePlugin, Key } from '@udecode/plate-common/react'; - -export const KEY_BLOCKQUOTE_EXIT_BREAK = 'blockquoteExitBreakPlugin'; - -function isWithinBlockquote(editor, entry) { - const blockAbove = getBlockAbove(editor, { at: entry[1] }); - return blockAbove?.[0]?.type === BlockquotePlugin.key; -} - -function queryNode(editor, entry, { empty, first, start }) { - return ( - (!empty || isAncestorEmpty(editor, entry[0])) && - (!first || isFirstChild(entry[1])) && - (!start || isSelectionAtBlockStart(editor)) - ); -} - -function unwrap(editor) { - unwrapNodes(editor, { split: true, match: n => n.type === BlockquotePlugin.key }); - return true; -} - -function keyDownHandler({ editor, event, query }) { - const entry = getBlockAbove(editor); - if (!entry) return; - - if (isWithinBlockquote(editor, entry) && queryNode(editor, entry, query) && unwrap(editor)) { - event.preventDefault(); - event.stopPropagation(); - } -} - -const BlockquoteExtPlugin = createPlatePlugin({ - key: KEY_BLOCKQUOTE_EXIT_BREAK, - node: { isElement: true }, -}).extend(() => ({ - shortcuts: { - blockquoteEnter: { - handler: handlerProps => keyDownHandler({ ...handlerProps, query: { empty: true } }), - keys: [[Key.Enter]], - }, - blockquoteBackspace: { - handler: handlerProps => - keyDownHandler({ ...handlerProps, query: { first: true, start: true } }), - keys: [[Key.Backspace]], - }, - }, -})); - -export default BlockquoteExtPlugin; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ExtendedBlockquotePlugin.js b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ExtendedBlockquotePlugin.js new file mode 100644 index 000000000000..2c034399bcc3 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ExtendedBlockquotePlugin.js @@ -0,0 +1,52 @@ +import { BlockquotePlugin } from '@platejs/basic-nodes/react'; +import { PathApi } from 'platejs'; +import { createPlatePlugin, Key } from 'platejs/react'; + +function isWithinBlockquote(editor, entry) { + const blockAbove = editor.api.block({ at: entry[1], above: true }); + return blockAbove?.[0]?.type === BlockquotePlugin.key; +} + +function queryNode(editor, entry, { empty, first, start, collapsed }) { + console.log('collapsed', editor.api.isCollapsed()); + return ( + (!empty || editor.api.isEmpty(entry[1])) && + (!first || !PathApi.hasPrevious(entry[1])) && + (!start || editor.api.isAt({ start: true })) && + (!collapsed || editor.api.isCollapsed()) + ); +} + +function unwrap(editor) { + editor.tf.unwrapNodes({ split: true, match: n => n.type === BlockquotePlugin.key }); +} + +const ExtendedBlockquotePlugin = createPlatePlugin({ + key: 'blockquote', + plugins: [BlockquotePlugin], +}).extendPlugin(BlockquotePlugin, { + node: { isElement: true }, + handlers: { + onKeyDown: ({ editor, event }) => { + const entry = editor.api.block(); + if (!entry) return; + if (!isWithinBlockquote(editor, entry)) return; + + const rules = [ + { key: Key.Enter, query: { empty: true } }, + { key: Key.Backspace, query: { first: true, start: true, collapsed: true } }, + ]; + + for (const rule of rules) { + if (event.key === rule.key && queryNode(editor, entry, rule.query)) { + unwrap(editor); + event.preventDefault(); + event.stopPropagation(); + break; + } + } + }, + }, +}); + +export default ExtendedBlockquotePlugin; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ShortcodePlugin.js b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ShortcodePlugin.js index e559ed2f0312..7a7e03450334 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ShortcodePlugin.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ShortcodePlugin.js @@ -1,5 +1,5 @@ -import { createSlatePlugin } from '@udecode/plate-common'; -import { toPlatePlugin } from '@udecode/plate-common/react'; +import { createSlatePlugin } from 'platejs'; +import { toPlatePlugin } from 'platejs/react'; import ShortcodeElement from '../components/Element/ShortcodeElement'; From 63b5a642e7f5f4baa055f9d9c5e148ebe48b66a8 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Wed, 15 Oct 2025 11:03:38 +0200 Subject: [PATCH 19/43] feat(widget-richtext): add shortcode plugin for editor components --- dev-test/index.html | 5 +- package-lock.json | 57 ++++++++++++++ .../EditorPreviewPane/EditorPreviewContent.js | 1 + .../EditorPreviewPane/EditorPreviewPane.js | 3 + .../src/MarkdownPreview.js | 2 +- .../decap-cms-widget-richtext/package.json | 6 ++ .../src/RichtextControl.js | 2 +- .../src/RichtextControl/VisualEditor.js | 45 +++-------- .../components/Element/ShortcodeElement.js | 78 +++++++++++-------- .../Toolbar/EditorComponentsToolbarButton.js | 38 ++++----- .../src/RichtextPreview.js | 17 +++- .../src/serializers/index.js | 16 ++-- 12 files changed, 166 insertions(+), 104 deletions(-) diff --git a/dev-test/index.html b/dev-test/index.html index 8147bcc0118d..89601682e391 100644 --- a/dev-test/index.html +++ b/dev-test/index.html @@ -125,7 +125,10 @@ h('p', {}, h('small', {}, "Written " + entry.getIn(['data', 'date'])) ), - h('div', {"className": "text"}, this.props.widgetFor('body')) + h('h2', {}, "Richtext Widget"), + h('div', {"className": "text"}, this.props.widgetFor('body')), + h('h2', {}, "Markdown Widget"), + h('div', {"className": "text"}, this.props.widgetFor('bodyold')) ); } }); diff --git a/package-lock.json b/package-lock.json index 2870af1b303c..27a1698f3520 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34412,8 +34412,14 @@ "class-variance-authority": "^0.7.0", "lucide-react": "^0.331.0", "platejs": "^49.2.21", + "rehype-parse": "^6.0.0", + "rehype-remark": "^8.0.0", + "rehype-stringify": "^7.0.0", "remark-parse": "^6.0.3", "remark-rehype": "^4.0.0", + "remark-slate": "^1.8.6", + "remark-slate-transformer": "^0.7.4", + "remark-stringify": "^6.0.4", "unified": "^9.0.0" }, "peerDependencies": { @@ -34428,6 +34434,28 @@ "react-immutable-proptypes": "^2.1.0" } }, + "packages/decap-cms-widget-richtext/node_modules/hast-util-to-html": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-7.1.3.tgz", + "integrity": "sha512-yk2+1p3EJTEE9ZEUkgHsUSVhIpCsL/bvT8E5GzmWc+N1Po5gBw+0F8bo7dpxXR0nu0bQVxVZGX2lBGF21CmeDw==", + "license": "MIT", + "dependencies": { + "ccount": "^1.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-is-element": "^1.0.0", + "hast-util-whitespace": "^1.0.0", + "html-void-elements": "^1.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0", + "stringify-entities": "^3.0.1", + "unist-util-is": "^4.0.0", + "xtend": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "packages/decap-cms-widget-richtext/node_modules/mdast-util-to-hast": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-4.0.0.tgz", @@ -34461,6 +34489,20 @@ "is-hexadecimal": "^1.0.0" } }, + "packages/decap-cms-widget-richtext/node_modules/rehype-stringify": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-7.0.0.tgz", + "integrity": "sha512-u3dQI7mIWN2X1H0MBFPva425HbkXgB+M39C9SM5leUS2kh5hHUn2SxQs2c2yZN5eIHipoLMojC0NP5e8fptxvQ==", + "license": "MIT", + "dependencies": { + "hast-util-to-html": "^7.0.0", + "xtend": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "packages/decap-cms-widget-richtext/node_modules/remark-parse": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-6.0.3.tgz", @@ -34493,6 +34535,21 @@ "mdast-util-to-hast": "^4.0.0" } }, + "packages/decap-cms-widget-richtext/node_modules/stringify-entities": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-3.1.0.tgz", + "integrity": "sha512-3FP+jGMmMV/ffZs86MoghGqAoqXAdxLrJP4GUdrDN1aIScYih5tuIO3eF4To5AJZ79KDZ8Fpdy7QJnK8SsL1Vg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "packages/decap-cms-widget-select": { "version": "3.3.0", "license": "MIT", diff --git a/packages/decap-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewContent.js b/packages/decap-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewContent.js index beb765b142b4..e7c1088364b8 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewContent.js +++ b/packages/decap-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewContent.js @@ -67,6 +67,7 @@ class PreviewContent extends React.Component { PreviewContent.propTypes = { previewComponent: PropTypes.func.isRequired, + getEditorComponents: PropTypes.func, previewProps: PropTypes.object, onFieldClick: PropTypes.func, }; diff --git a/packages/decap-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js b/packages/decap-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js index 3cc025fd705d..b407144fe618 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js +++ b/packages/decap-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js @@ -13,6 +13,7 @@ import { getPreviewTemplate, getPreviewStyles, getRemarkPlugins, + getEditorComponents, } from '../../../lib/registry'; import { getAllEntries, tryLoadEntry } from '../../../actions/entries'; import { ErrorBoundary } from '../../UI'; @@ -57,6 +58,7 @@ export class PreviewPane extends React.Component { fieldsMetaData={metadata} resolveWidget={resolveWidget} getRemarkPlugins={getRemarkPlugins} + getEditorComponents={getEditorComponents} /> ); }; @@ -261,6 +263,7 @@ export class PreviewPane extends React.Component { this.widgetFor(name, fields, values, fieldsMetaData), widgetsFor: this.widgetsFor, getCollection: this.getCollection, + getEditorComponents, }; const styleEls = getPreviewStyles().map((style, i) => { diff --git a/packages/decap-cms-widget-markdown/src/MarkdownPreview.js b/packages/decap-cms-widget-markdown/src/MarkdownPreview.js index 5378fc06ae0c..3f779e956913 100644 --- a/packages/decap-cms-widget-markdown/src/MarkdownPreview.js +++ b/packages/decap-cms-widget-markdown/src/MarkdownPreview.js @@ -21,7 +21,7 @@ class MarkdownPreview extends React.Component { if (value === null) { return null; } - + const html = markdownToHtml(value, { getAsset, resolveWidget }, getRemarkPlugins?.()); const toRender = field?.get('sanitize_preview', false) ? DOMPurify.sanitize(html) : html; diff --git a/packages/decap-cms-widget-richtext/package.json b/packages/decap-cms-widget-richtext/package.json index 4da3be9dcc6e..4486a8cf7812 100644 --- a/packages/decap-cms-widget-richtext/package.json +++ b/packages/decap-cms-widget-richtext/package.json @@ -28,8 +28,14 @@ "class-variance-authority": "^0.7.0", "lucide-react": "^0.331.0", "platejs": "^49.2.21", + "rehype-parse": "^6.0.0", + "rehype-remark": "^8.0.0", + "rehype-stringify": "^7.0.0", "remark-parse": "^6.0.3", "remark-rehype": "^4.0.0", + "remark-slate": "^1.8.6", + "remark-slate-transformer": "^0.7.4", + "remark-stringify": "^6.0.4", "unified": "^9.0.0" }, "peerDependencies": { diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl.js b/packages/decap-cms-widget-richtext/src/RichtextControl.js index d9703a9c08bb..35a28cb0166f 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl.js @@ -5,7 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import VisualEditor from './RichtextControl/VisualEditor'; import { EditorProvider } from './RichtextControl/editorContext'; -export default class MarkdownControl extends React.Component { +export default class RichtextControl extends React.Component { static propTypes = { onChange: PropTypes.func.isRequired, onAddAsset: PropTypes.func.isRequired, diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index 1768fb6e950e..7cf492b17fb3 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -1,17 +1,7 @@ import React from 'react'; -import { KEYS, TrailingBlockPlugin } from 'platejs'; -import { - usePlateEditor, - Plate, - ParagraphPlugin, - PlateLeaf, -} from 'platejs/react'; -import { - BoldPlugin, - ItalicPlugin, - CodePlugin, - HeadingPlugin, -} from '@platejs/basic-nodes/react'; +import { KEYS } from 'platejs'; +import { usePlateEditor, Plate, ParagraphPlugin, PlateLeaf } from 'platejs/react'; +import { BoldPlugin, ItalicPlugin, CodePlugin, HeadingPlugin } from '@platejs/basic-nodes/react'; import { ListPlugin } from '@platejs/list-classic/react'; import { LinkPlugin } from '@platejs/link/react'; import { ClassNames } from '@emotion/react'; @@ -29,8 +19,8 @@ import HeadingElement from './components/Element/HeadingElement'; import ListElement from './components/Element/ListElement'; import BlockquoteElement from './components/Element/BlockquoteElement'; import LinkElement from './components/Element/LinkElement'; -// import ShortcodePlugin from './plugins/ShortcodePlugin'; import ExtendedBlockquotePlugin from './plugins/ExtendedBlockquotePlugin'; +import ShortcodePlugin from './plugins/ShortcodePlugin'; // import ShortcodeElement from './components/Element/ShortcodeElement'; function visualEditorStyles({ minimal }) { @@ -86,22 +76,6 @@ const emptyValue = [ type: ParagraphPlugin.key, children: [{ text: '' }], }, - // { - // children: [{ text: 'Title' }], - // type: 'h3', - // }, - // { - // children: [{ text: 'This is a quote.' }], - // type: 'blockquote', - // }, - // { - // children: [ - // { text: 'With some ' }, - // { bold: true, text: 'bold' }, - // { text: ' text for emphasis!' }, - // ], - // type: 'p', - // }, ]; export default function VisualEditor(props) { @@ -135,7 +109,9 @@ export default function VisualEditor(props) { onChange(mdValue); } - const initialValue = props.value ? markdownToSlate(props.value, {}) : emptyValue; + const initialValue = props.value + ? markdownToSlate(props.value, { editorComponents }) + : emptyValue; const editor = usePlateEditor({ override: { @@ -166,12 +142,9 @@ export default function VisualEditor(props) { node: { component: LinkElement }, }), ExtendedBlockquotePlugin.configure({ - node: { component: BlockquoteElement } - }), - // ShortcodePlugin, - TrailingBlockPlugin.configure({ - options: { type: ParagraphPlugin.key }, + node: { component: BlockquoteElement }, }), + ShortcodePlugin, ], value: initialValue, }); diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ShortcodeElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ShortcodeElement.js index f722351228ea..1008f43f383b 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ShortcodeElement.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ShortcodeElement.js @@ -5,7 +5,13 @@ import { omit } from 'lodash'; import { css } from '@emotion/react'; import { Range, setNodes } from 'slate'; import { zIndex } from 'decap-cms-ui-default'; -import { ParagraphPlugin, PlateElement, useEditorRef } from 'platejs/react'; +import { + ParagraphPlugin, + PlateElement, + useEditorRef, + useEditorSelection, + useEditorState, +} from 'platejs/react'; import { useEditorContext } from '../../editorContext'; @@ -28,21 +34,22 @@ function InsertionPoint(props) { function ShortcodeElement(props) { const editor = useEditorRef(); - const { element, dataKey = 'shortcodeData', children } = props; + const editorState = useEditorState(); + const { attributes, element, dataKey = 'shortcodeData', children } = props; const { editorControl: EditorControl, editorComponents } = useEditorContext(); - const plugin = editorComponents.get(element.id); + const plugin = editorComponents.get(element.data.shortcode); const fieldKeys = ['id', 'fromBlock', 'toBlock', 'toPreview', 'pattern', 'icon']; const field = fromJS(omit(plugin, fieldKeys)); const [value, setValue] = useState(fromJS(element?.data[dataKey] ?? { id: '' })); + const selection = useEditorSelection(); const path = editor.api.findPath(element); const isSelected = - editor.selection && - path && - Range.isRange(editor.selection) && - Range.includes(editor.selection, path); + selection && path && Range.isRange(selection) && Range.includes(selection, path); const insertBefore = path[0] === 0; + const insertAfter = + path[0] === editorState.children.length - 1 || editor.isVoid(editorState.children[path[0] + 1]); function handleChange(fieldName, value, metadata) { const newProperties = { @@ -65,32 +72,41 @@ function ShortcodeElement(props) { ); } + function handleInsertAfter(e) { + e.preventDefault(); + e.stopPropagation(); + + editor.tf.insertNodes( + { type: ParagraphPlugin.key, children: [{ text: '' }] }, + { select: true }, + ); + } + return ( - <> - - - {insertBefore && } - + {insertBefore && } + + {}} - isNewEditorComponent={element.data?.shortcodeNew} - isSelected={isSelected} - /> - {children} - - - + &:first-of-type { + margin-top: 0; + } + `} + value={value} + field={field} + onChange={handleChange} + isEditorComponent={true} + onValidateObject={() => {}} + isNewEditorComponent={element.data?.shortcodeNew} + isSelected={isSelected} + /> + {children} + + {insertAfter && } + ); } diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/EditorComponentsToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/EditorComponentsToolbarButton.js index 43655bca47d1..c5fb6aeabff5 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/EditorComponentsToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/EditorComponentsToolbarButton.js @@ -24,30 +24,22 @@ function EditorComponentsToolbarButton({ disabled, editorComponents, allowedEdit .filter(field => field.has('default')) .map(field => field.get('default')); - if (editor.api.isAt({ end: true }) && editor.api.isAt({ start: true })) { - // setBlockAboveNode(editor, { // find alternative for removed setBlockAboveNode - // children: [{ text: '' }], - // type: 'shortcode', - // id: plugin.id, - // data: { - // shortcode: plugin.id, - // shortcodeNew: true, - // shortcodeData: defaultValues.toJS(), - // }, - // }) - return; - } - - editor.tf.insertNodes({ - children: [{ text: '' }], - type: 'shortcode', - id: plugin.id, - data: { - shortcode: plugin.id, - shortcodeNew: true, - shortcodeData: defaultValues.toJS(), + editor.tf.insertNodes( + { + children: [{ text: '' }], + type: 'shortcode', + isElement: true, + isVoid: true, + data: { + shortcode: plugin.id, + shortcodeNew: true, + shortcodeData: defaultValues.toJS(), + }, + }, + { + removeEmpty: true, }, - }); + ); }, [editor], ); diff --git a/packages/decap-cms-widget-richtext/src/RichtextPreview.js b/packages/decap-cms-widget-richtext/src/RichtextPreview.js index 6d0402fb57b2..ad1f04e37dd2 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextPreview.js +++ b/packages/decap-cms-widget-richtext/src/RichtextPreview.js @@ -6,12 +6,22 @@ import DOMPurify from 'dompurify'; import { markdownToHtml } from './serializers'; -function RichtextPreview({ value, getAsset, resolveWidget, field, getRemarkPlugins }) { +function RichtextPreview({ + value, + getAsset, + resolveWidget, + field, + getRemarkPlugins, + getEditorComponents, +}) { if (value === null) { return null; } - - const html = markdownToHtml(value, { getAsset, resolveWidget }, getRemarkPlugins?.()); + const html = markdownToHtml( + value, + { getAsset, resolveWidget, editorComponents: getEditorComponents?.() }, + getRemarkPlugins?.(), + ); const toRender = field?.get('sanitize_preview', false) ? DOMPurify.sanitize(html) : html; return ; @@ -23,6 +33,7 @@ RichtextPreview.propTypes = { resolveWidget: PropTypes.func.isRequired, field: ImmutablePropTypes.map.isRequired, getRemarkPlugins: PropTypes.func, + getEditorComponents: PropTypes.func, }; export default RichtextPreview; diff --git a/packages/decap-cms-widget-richtext/src/serializers/index.js b/packages/decap-cms-widget-richtext/src/serializers/index.js index f187d0c5d9c0..85ae813dc79e 100644 --- a/packages/decap-cms-widget-richtext/src/serializers/index.js +++ b/packages/decap-cms-widget-richtext/src/serializers/index.js @@ -7,7 +7,6 @@ import remarkToRehype from 'remark-rehype'; import rehypeToHtml from 'rehype-stringify'; import htmlToRehype from 'rehype-parse'; import rehypeToRemark from 'rehype-remark'; -import { Map } from 'immutable'; import remarkToRehypeShortcodes from './remarkRehypeShortcodes'; import rehypePaperEmoji from './rehypePaperEmoji'; @@ -60,11 +59,11 @@ import slateToRemark from './slateRemark'; /** * Deserialize a Markdown string to an MDAST. */ -export function markdownToRemark(markdown, remarkPlugins) { +export function markdownToRemark(markdown, remarkPlugins, editorComponents) { const processor = unified() .use(markdownToRemarkPlugin, { fences: true, commonmark: true }) .use(markdownToRemarkRemoveTokenizers, { inlineTokenizers: ['url'] }) - .use(remarkParseShortcodes, { plugins: Map() }) + .use(remarkParseShortcodes, { plugins: editorComponents }) .use(remarkAllowHtmlEntities) .use(remarkSquashReferences) .use(remarkPlugins); @@ -155,11 +154,12 @@ export function remarkToMarkdown(obj, remarkPlugins, editorComponents) { /** * Convert Markdown to HTML. */ -export function markdownToHtml(markdown, { getAsset, resolveWidget, remarkPlugins = [] } = {}) { - const mdast = markdownToRemark(markdown, remarkPlugins); +export function markdownToHtml(markdown, { getAsset, resolveWidget, remarkPlugins = [], editorComponents } = {}) { + const mdast = markdownToRemark(markdown, remarkPlugins, editorComponents); + const hast = unified() - .use(remarkToRehypeShortcodes, { plugins: Map(), getAsset, resolveWidget }) + .use(remarkToRehypeShortcodes, { plugins: editorComponents, getAsset, resolveWidget }) .use(remarkToRehype, { allowDangerousHTML: true }) .runSync(mdast); @@ -200,8 +200,8 @@ export function htmlToSlate(html) { /** * Convert Markdown to Slate's Raw AST. */ -export function markdownToSlate(markdown, { voidCodeBlock, remarkPlugins = [] } = {}) { - const mdast = markdownToRemark(markdown, remarkPlugins); +export function markdownToSlate(markdown, { voidCodeBlock, remarkPlugins = [], editorComponents } = {}) { + const mdast = markdownToRemark(markdown, remarkPlugins, editorComponents); const slateRaw = unified() .use(remarkWrapHtml) From 87cb7714ad7dbc6cb5e2a6c30fe97de13bc078ed Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Wed, 15 Oct 2025 12:20:44 +0200 Subject: [PATCH 20/43] fix: lint --- .../src/MarkdownControl/components/VoidBlock.js | 1 - packages/decap-cms-widget-markdown/src/MarkdownPreview.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/decap-cms-widget-markdown/src/MarkdownControl/components/VoidBlock.js b/packages/decap-cms-widget-markdown/src/MarkdownControl/components/VoidBlock.js index 52aaaa01cab7..ad2b0a7b9ef0 100644 --- a/packages/decap-cms-widget-markdown/src/MarkdownControl/components/VoidBlock.js +++ b/packages/decap-cms-widget-markdown/src/MarkdownControl/components/VoidBlock.js @@ -42,7 +42,6 @@ function VoidBlock({ attributes, children, element }) { insertAtPath([...path.slice(0, -1), path[path.length - 1] + 1]); } - const insertBefore = path[0] === 0; const nextElement = editor.children[path[0] + 1]; const insertAfter = path[0] === editor.children.length - 1 || editor.isVoid(nextElement); diff --git a/packages/decap-cms-widget-markdown/src/MarkdownPreview.js b/packages/decap-cms-widget-markdown/src/MarkdownPreview.js index 3f779e956913..5378fc06ae0c 100644 --- a/packages/decap-cms-widget-markdown/src/MarkdownPreview.js +++ b/packages/decap-cms-widget-markdown/src/MarkdownPreview.js @@ -21,7 +21,7 @@ class MarkdownPreview extends React.Component { if (value === null) { return null; } - + const html = markdownToHtml(value, { getAsset, resolveWidget }, getRemarkPlugins?.()); const toRender = field?.get('sanitize_preview', false) ? DOMPurify.sanitize(html) : html; From d5f4b0c65f278d55a5a52302a8632cb9d102f9bd Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Wed, 15 Oct 2025 12:21:32 +0200 Subject: [PATCH 21/43] fix(widget-richtext): fix preview for using richtext as editor component --- dev-test/index.html | 33 +++++++++++++++++++ .../src/ObjectPreview.js | 4 +++ 2 files changed, 37 insertions(+) diff --git a/dev-test/index.html b/dev-test/index.html index 89601682e391..9ce69a276082 100644 --- a/dev-test/index.html +++ b/dev-test/index.html @@ -240,6 +240,39 @@ ); } }); + CMS.registerEditorComponent({ + id: 'container-markdown', + label: 'Container Markdown', + fields: [ + { name: 'inner', label: 'Body', widget: 'markdown', editor_components: [] }, + ], + pattern: /^{{< container-markdown >}}(.*?){{< \/container-markdown >}}/s, + fromBlock (match) { + return { + inner: match[1] || '', + } + }, + toBlock (obj) { + return `{{< container-markdown >}}${obj.inner}{{< /container-markdown >}}` + }, + }) + CMS.registerEditorComponent({ + id: 'container-richtext', + label: 'Container Richtext', + fields: [ + { name: 'inner', label: 'Body', widget: 'richtext', editor_components: ['container-richtext'] }, + ], + pattern: /^{{< container-richtext >}}(.*?){{< \/container-richtext >}}/s, + fromBlock (match) { + return { + inner: match[1] || '', + } + }, + toBlock (obj) { + return `{{< container-richtext >}}${obj.inner}{{< /container-richtext >}}` + }, + }) + diff --git a/packages/decap-cms-widget-object/src/ObjectPreview.js b/packages/decap-cms-widget-object/src/ObjectPreview.js index f9015fc0f8c3..2c74c7f5a194 100644 --- a/packages/decap-cms-widget-object/src/ObjectPreview.js +++ b/packages/decap-cms-widget-object/src/ObjectPreview.js @@ -1,8 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import { WidgetPreviewContainer } from 'decap-cms-ui-default'; +import { fromJS } from 'immutable'; function ObjectPreview({ field }) { + if (field && !field.get) { + field = fromJS(field) + } return ( {(field && field.get('fields')) || field.get('field') || null} From 6fa2dc158a3346f0989eb8af4d4f4ca66b7382a9 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Wed, 15 Oct 2025 12:29:03 +0200 Subject: [PATCH 22/43] style(widget-richtext): lint code --- .../decap-cms-widget-object/src/ObjectPreview.js | 2 +- .../src/RichtextControl.js | 13 ++++++++++--- .../src/RichtextControl/editorContext.js | 6 +----- .../src/serializers/index.js | 11 ++++++++--- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/decap-cms-widget-object/src/ObjectPreview.js b/packages/decap-cms-widget-object/src/ObjectPreview.js index 2c74c7f5a194..439db4a7196a 100644 --- a/packages/decap-cms-widget-object/src/ObjectPreview.js +++ b/packages/decap-cms-widget-object/src/ObjectPreview.js @@ -5,7 +5,7 @@ import { fromJS } from 'immutable'; function ObjectPreview({ field }) { if (field && !field.get) { - field = fromJS(field) + field = fromJS(field); } return ( diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl.js b/packages/decap-cms-widget-richtext/src/RichtextControl.js index 35a28cb0166f..0737b1c85d93 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl.js @@ -19,10 +19,17 @@ export default class RichtextControl extends React.Component { isDisabled: PropTypes.bool, }; - render() { - const { classNameWrapper, field, t, isDisabled, getEditorComponents, editorControl, onChange, value } = - this.props; + const { + classNameWrapper, + field, + t, + isDisabled, + getEditorComponents, + editorControl, + onChange, + value, + } = this.props; const visualEditor = ( diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/editorContext.js b/packages/decap-cms-widget-richtext/src/RichtextControl/editorContext.js index fd0c5c26a1eb..6ceb6806efbf 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/editorContext.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/editorContext.js @@ -8,9 +8,5 @@ export function useEditorContext() { export function EditorProvider({ children, editorControl, editorComponents }) { const value = { editorControl, editorComponents }; - return ( - - {children} - - ); + return {children}; } diff --git a/packages/decap-cms-widget-richtext/src/serializers/index.js b/packages/decap-cms-widget-richtext/src/serializers/index.js index 85ae813dc79e..a329b4f8ac82 100644 --- a/packages/decap-cms-widget-richtext/src/serializers/index.js +++ b/packages/decap-cms-widget-richtext/src/serializers/index.js @@ -154,10 +154,12 @@ export function remarkToMarkdown(obj, remarkPlugins, editorComponents) { /** * Convert Markdown to HTML. */ -export function markdownToHtml(markdown, { getAsset, resolveWidget, remarkPlugins = [], editorComponents } = {}) { +export function markdownToHtml( + markdown, + { getAsset, resolveWidget, remarkPlugins = [], editorComponents } = {}, +) { const mdast = markdownToRemark(markdown, remarkPlugins, editorComponents); - const hast = unified() .use(remarkToRehypeShortcodes, { plugins: editorComponents, getAsset, resolveWidget }) .use(remarkToRehype, { allowDangerousHTML: true }) @@ -200,7 +202,10 @@ export function htmlToSlate(html) { /** * Convert Markdown to Slate's Raw AST. */ -export function markdownToSlate(markdown, { voidCodeBlock, remarkPlugins = [], editorComponents } = {}) { +export function markdownToSlate( + markdown, + { voidCodeBlock, remarkPlugins = [], editorComponents } = {}, +) { const mdast = markdownToRemark(markdown, remarkPlugins, editorComponents); const slateRaw = unified() From 3a2c535f07c7451ca61183c7992648a473139535 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Wed, 15 Oct 2025 12:46:03 +0200 Subject: [PATCH 23/43] fix: temporarily remove serializer tests for richt ext widget --- .../__fixtures__/commonmarkExpected.json | 625 ------------------ .../duplicate_marks_github_issue_3280.md | 1 - .../serializers/__tests__/commonmark.spec.js | 110 --- .../src/serializers/__tests__/index.spec.js | 52 -- .../__tests__/remarkAllowHtmlEntities.spec.js | 25 - .../__tests__/remarkAssertParents.spec.js | 171 ----- .../remarkEscapeMarkdownEntities.spec.js | 84 --- .../__tests__/remarkPaddedLinks.spec.js | 43 -- .../__tests__/remarkPlugins.spec.js | 299 --------- .../__tests__/remarkShortcodes.spec.js | 106 --- .../serializers/__tests__/remarkSlate.spec.js | 67 -- .../remarkStripTrailingBreaks.spec.js | 23 - .../src/serializers/__tests__/slate.spec.js | 300 --------- 13 files changed, 1906 deletions(-) delete mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/commonmarkExpected.json delete mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/duplicate_marks_github_issue_3280.md delete mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/commonmark.spec.js delete mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/index.spec.js delete mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAllowHtmlEntities.spec.js delete mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAssertParents.spec.js delete mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkEscapeMarkdownEntities.spec.js delete mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPaddedLinks.spec.js delete mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPlugins.spec.js delete mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkShortcodes.spec.js delete mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkSlate.spec.js delete mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkStripTrailingBreaks.spec.js delete mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/slate.spec.js diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/commonmarkExpected.json b/packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/commonmarkExpected.json deleted file mode 100644 index 2e74df1471fe..000000000000 --- a/packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/commonmarkExpected.json +++ /dev/null @@ -1,625 +0,0 @@ -{ - "\tfoo\tbaz\t\tbim\n": "NOT_TO_EQUAL", - " \tfoo\tbaz\t\tbim\n": "NOT_TO_EQUAL", - " a\ta\n ὐ\ta\n": "NOT_TO_EQUAL", - " - foo\n\n\tbar\n": "NOT_TO_EQUAL", - "- foo\n\n\t\tbar\n": "NOT_TO_EQUAL", - ">\t\tfoo\n": "NOT_TO_EQUAL", - "-\t\tfoo\n": "NOT_TO_EQUAL", - " foo\n\tbar\n": "TO_EQUAL", - " - foo\n - bar\n\t - baz\n": "NOT_TO_EQUAL", - "#\tFoo\n": "TO_EQUAL", - "*\t*\t*\t\n": "TO_ERROR", - "- `one\n- two`\n": "TO_EQUAL", - "***\n---\n___\n": "NOT_TO_EQUAL", - "+++\n": "TO_EQUAL", - "===\n": "TO_EQUAL", - "--\n**\n__\n": "TO_EQUAL", - " ***\n ***\n ***\n": "NOT_TO_EQUAL", - " ***\n": "TO_EQUAL", - "Foo\n ***\n": "TO_EQUAL", - "_____________________________________\n": "NOT_TO_EQUAL", - " - - -\n": "NOT_TO_EQUAL", - " ** * ** * ** * **\n": "NOT_TO_EQUAL", - "- - - -\n": "NOT_TO_EQUAL", - "- - - - \n": "NOT_TO_EQUAL", - "_ _ _ _ a\n\na------\n\n---a---\n": "TO_EQUAL", - " *-*\n": "NOT_TO_EQUAL", - "- foo\n***\n- bar\n": "NOT_TO_EQUAL", - "Foo\n***\nbar\n": "NOT_TO_EQUAL", - "Foo\n---\nbar\n": "TO_EQUAL", - "* Foo\n* * *\n* Bar\n": "NOT_TO_EQUAL", - "- Foo\n- * * *\n": "NOT_TO_EQUAL", - "# foo\n## foo\n### foo\n#### foo\n##### foo\n###### foo\n": "TO_EQUAL", - "####### foo\n": "TO_EQUAL", - "#5 bolt\n\n#hashtag\n": "TO_EQUAL", - "\\## foo\n": "TO_EQUAL", - "# foo *bar* \\*baz\\*\n": "NOT_TO_EQUAL", - "# foo \n": "TO_EQUAL", - " ### foo\n ## foo\n # foo\n": "TO_EQUAL", - " # foo\n": "TO_EQUAL", - "foo\n # bar\n": "TO_EQUAL", - "## foo ##\n ### bar ###\n": "TO_EQUAL", - "# foo ##################################\n##### foo ##\n": "TO_EQUAL", - "### foo ### \n": "TO_EQUAL", - "### foo ### b\n": "TO_EQUAL", - "# foo#\n": "NOT_TO_EQUAL", - "### foo \\###\n## foo #\\##\n# foo \\#\n": "NOT_TO_EQUAL", - "****\n## foo\n****\n": "NOT_TO_EQUAL", - "Foo bar\n# baz\nBar foo\n": "TO_EQUAL", - "## \n#\n### ###\n": "TO_ERROR", - "Foo *bar*\n=========\n\nFoo *bar*\n---------\n": "TO_EQUAL", - "Foo *bar\nbaz*\n====\n": "NOT_TO_EQUAL", - "Foo\n-------------------------\n\nFoo\n=\n": "TO_EQUAL", - " Foo\n---\n\n Foo\n-----\n\n Foo\n ===\n": "NOT_TO_EQUAL", - " Foo\n ---\n\n Foo\n---\n": "NOT_TO_EQUAL", - "Foo\n ---- \n": "NOT_TO_EQUAL", - "Foo\n ---\n": "TO_EQUAL", - "Foo\n= =\n\nFoo\n--- -\n": "NOT_TO_EQUAL", - "Foo \n-----\n": "TO_EQUAL", - "Foo\\\n----\n": "TO_EQUAL", - "`Foo\n----\n`\n\n\n": "NOT_TO_EQUAL", - "> Foo\n---\n": "NOT_TO_EQUAL", - "> foo\nbar\n===\n": "NOT_TO_EQUAL", - "- Foo\n---\n": "NOT_TO_EQUAL", - "Foo\nBar\n---\n": "NOT_TO_EQUAL", - "---\nFoo\n---\nBar\n---\nBaz\n": "NOT_TO_EQUAL", - "\n====\n": "TO_EQUAL", - "---\n---\n": "TO_ERROR", - "- foo\n-----\n": "NOT_TO_EQUAL", - " foo\n---\n": "NOT_TO_EQUAL", - "> foo\n-----\n": "NOT_TO_EQUAL", - "\\> foo\n------\n": "NOT_TO_EQUAL", - "Foo\n\nbar\n---\nbaz\n": "TO_EQUAL", - "Foo\nbar\n\n---\n\nbaz\n": "NOT_TO_EQUAL", - "Foo\nbar\n* * *\nbaz\n": "NOT_TO_EQUAL", - "Foo\nbar\n\\---\nbaz\n": "NOT_TO_EQUAL", - " a simple\n indented code block\n": "TO_EQUAL", - " - foo\n\n bar\n": "NOT_TO_EQUAL", - "1. foo\n\n - bar\n": "TO_EQUAL", - " \n *hi*\n\n - one\n": "NOT_TO_EQUAL", - " chunk1\n\n chunk2\n \n \n \n chunk3\n": "TO_EQUAL", - " chunk1\n \n chunk2\n": "TO_EQUAL", - "Foo\n bar\n\n": "TO_EQUAL", - " foo\nbar\n": "TO_EQUAL", - "# Heading\n foo\nHeading\n------\n foo\n----\n": "NOT_TO_EQUAL", - " foo\n bar\n": "TO_EQUAL", - "\n \n foo\n \n\n": "TO_EQUAL", - " foo \n": "TO_EQUAL", - "```\n<\n >\n```\n": "NOT_TO_EQUAL", - "~~~\n<\n >\n~~~\n": "NOT_TO_EQUAL", - "``\nfoo\n``\n": "TO_EQUAL", - "```\naaa\n~~~\n```\n": "NOT_TO_EQUAL", - "~~~\naaa\n```\n~~~\n": "NOT_TO_EQUAL", - "````\naaa\n```\n``````\n": "NOT_TO_EQUAL", - "~~~~\naaa\n~~~\n~~~~\n": "NOT_TO_EQUAL", - "```\n": "TO_EQUAL", - "`````\n\n```\naaa\n": "NOT_TO_EQUAL", - "> ```\n> aaa\n\nbbb\n": "TO_EQUAL", - "```\n\n \n```\n": "NOT_TO_EQUAL", - "```\n```\n": "TO_EQUAL", - " ```\n aaa\naaa\n```\n": "TO_EQUAL", - " ```\naaa\n aaa\naaa\n ```\n": "TO_EQUAL", - " ```\n aaa\n aaa\n aaa\n ```\n": "TO_EQUAL", - " ```\n aaa\n ```\n": "NOT_TO_EQUAL", - "```\naaa\n ```\n": "TO_EQUAL", - " ```\naaa\n ```\n": "TO_EQUAL", - "```\naaa\n ```\n": "NOT_TO_EQUAL", - "``` ```\naaa\n": "TO_EQUAL", - "~~~~~~\naaa\n~~~ ~~\n": "NOT_TO_EQUAL", - "foo\n```\nbar\n```\nbaz\n": "TO_EQUAL", - "foo\n---\n~~~\nbar\n~~~\n# baz\n": "TO_EQUAL", - "```ruby\ndef foo(x)\n return 3\nend\n```\n": "TO_EQUAL", - "~~~~ ruby startline=3 $%@#$\ndef foo(x)\n return 3\nend\n~~~~~~~\n": "TO_EQUAL", - "````;\n````\n": "TO_EQUAL", - "``` aa ```\nfoo\n": "TO_EQUAL", - "```\n``` aaa\n```\n": "NOT_TO_EQUAL", - "
\n
\n**Hello**,\n\n_world_.\n
\n
\n": "NOT_TO_EQUAL", - "\n \n \n \n
\n hi\n
\n\nokay.\n": "TO_EQUAL", - "
\n*foo*\n": "NOT_TO_EQUAL", - "
\n\n*Markdown*\n\n
\n": "TO_EQUAL", - "
\n
\n": "TO_EQUAL", - "
\n
\n": "TO_EQUAL", - "
\n*foo*\n\n*bar*\n": "NOT_TO_EQUAL", - "\n": "NOT_TO_EQUAL", - "
\nfoo\n
\n": "TO_EQUAL", - "
\n``` c\nint x = 33;\n```\n": "NOT_TO_EQUAL", - "\n*bar*\n\n": "NOT_TO_EQUAL", - "\n*bar*\n\n": "NOT_TO_EQUAL", - "\n*bar*\n\n": "NOT_TO_EQUAL", - "\n*bar*\n": "NOT_TO_EQUAL", - "\n*foo*\n\n": "NOT_TO_EQUAL", - "\n\n*foo*\n\n\n": "TO_EQUAL", - "*foo*\n": "TO_EQUAL", - "
\nimport Text.HTML.TagSoup\n\nmain :: IO ()\nmain = print $ parseTags tags\n
\nokay\n": "TO_EQUAL", - "\nokay\n": "TO_EQUAL", - "\nh1 {color:red;}\n\np {color:blue;}\n\nokay\n": "TO_EQUAL", - "\n\nfoo\n": "TO_EQUAL", - ">
\n> foo\n\nbar\n": "TO_EQUAL", - "-
\n- foo\n": "TO_EQUAL", - "\n*foo*\n": "TO_EQUAL", - "*bar*\n*baz*\n": "NOT_TO_EQUAL", - "1. *bar*\n": "NOT_TO_EQUAL", - "\nokay\n": "TO_EQUAL", - "';\n\n?>\nokay\n": "TO_EQUAL", - "\n": "TO_EQUAL", - "\nokay\n": "NOT_TO_EQUAL", - " \n\n \n": "NOT_TO_EQUAL", - "
\n\n
\n": "NOT_TO_EQUAL", - "Foo\n
\nbar\n
\n": "TO_EQUAL", - "
\nbar\n
\n*foo*\n": "NOT_TO_EQUAL", - "Foo\n\nbaz\n": "TO_EQUAL", - "
\n\n*Emphasized* text.\n\n
\n": "TO_EQUAL", - "
\n*Emphasized* text.\n
\n": "NOT_TO_EQUAL", - "\n\n\n\n\n\n\n\n
\nHi\n
\n": "TO_EQUAL", - "\n\n \n\n \n\n \n\n
\n Hi\n
\n": "NOT_TO_EQUAL", - "[foo]: /url \"title\"\n\n[foo]\n": "TO_EQUAL", - " [foo]: \n /url \n 'the title' \n\n[foo]\n": "TO_EQUAL", - "[Foo*bar\\]]:my_(url) 'title (with parens)'\n\n[Foo*bar\\]]\n": "NOT_TO_EQUAL", - "[Foo bar]:\n\n'title'\n\n[Foo bar]\n": "TO_EQUAL", - "[foo]: /url '\ntitle\nline1\nline2\n'\n\n[foo]\n": "NOT_TO_EQUAL", - "[foo]: /url 'title\n\nwith blank line'\n\n[foo]\n": "TO_EQUAL", - "[foo]:\n/url\n\n[foo]\n": "TO_EQUAL", - "[foo]:\n\n[foo]\n": "TO_EQUAL", - "[foo]: /url\\bar\\*baz \"foo\\\"bar\\baz\"\n\n[foo]\n": "NOT_TO_EQUAL", - "[foo]\n\n[foo]: url\n": "TO_EQUAL", - "[foo]\n\n[foo]: first\n[foo]: second\n": "NOT_TO_EQUAL", - "[FOO]: /url\n\n[Foo]\n": "TO_EQUAL", - "[ΑΓΩ]: /φου\n\n[αγω]\n": "TO_EQUAL", - "[foo]: /url\n": "TO_ERROR", - "[\nfoo\n]: /url\nbar\n": "TO_EQUAL", - "[foo]: /url \"title\" ok\n": "NOT_TO_EQUAL", - "[foo]: /url\n\"title\" ok\n": "NOT_TO_EQUAL", - " [foo]: /url \"title\"\n\n[foo]\n": "NOT_TO_EQUAL", - "```\n[foo]: /url\n```\n\n[foo]\n": "TO_EQUAL", - "Foo\n[bar]: /baz\n\n[bar]\n": "TO_EQUAL", - "# [Foo]\n[foo]: /url\n> bar\n": "TO_EQUAL", - "[foo]: /foo-url \"foo\"\n[bar]: /bar-url\n \"bar\"\n[baz]: /baz-url\n\n[foo],\n[bar],\n[baz]\n": "TO_EQUAL", - "[foo]\n\n> [foo]: /url\n": "NOT_TO_EQUAL", - "aaa\n\nbbb\n": "TO_EQUAL", - "aaa\nbbb\n\nccc\nddd\n": "TO_EQUAL", - "aaa\n\n\nbbb\n": "TO_EQUAL", - " aaa\n bbb\n": "NOT_TO_EQUAL", - "aaa\n bbb\n ccc\n": "TO_EQUAL", - " aaa\nbbb\n": "NOT_TO_EQUAL", - " aaa\nbbb\n": "TO_EQUAL", - "aaa \nbbb \n": "NOT_TO_EQUAL", - " \n\naaa\n \n\n# aaa\n\n \n": "TO_EQUAL", - "> # Foo\n> bar\n> baz\n": "TO_EQUAL", - "># Foo\n>bar\n> baz\n": "TO_EQUAL", - " > # Foo\n > bar\n > baz\n": "TO_EQUAL", - " > # Foo\n > bar\n > baz\n": "NOT_TO_EQUAL", - "> # Foo\n> bar\nbaz\n": "TO_EQUAL", - "> bar\nbaz\n> foo\n": "TO_EQUAL", - "> foo\n---\n": "NOT_TO_EQUAL", - "> - foo\n- bar\n": "TO_EQUAL", - "> foo\n bar\n": "TO_EQUAL", - "> ```\nfoo\n```\n": "NOT_TO_EQUAL", - "> foo\n - bar\n": "NOT_TO_EQUAL", - ">\n": "TO_ERROR", - ">\n> \n> \n": "TO_ERROR", - ">\n> foo\n> \n": "TO_EQUAL", - "> foo\n\n> bar\n": "NOT_TO_EQUAL", - "> foo\n> bar\n": "TO_EQUAL", - "> foo\n>\n> bar\n": "TO_EQUAL", - "foo\n> bar\n": "TO_EQUAL", - "> aaa\n***\n> bbb\n": "NOT_TO_EQUAL", - "> bar\nbaz\n": "TO_EQUAL", - "> bar\n\nbaz\n": "TO_EQUAL", - "> bar\n>\nbaz\n": "NOT_TO_EQUAL", - "> > > foo\nbar\n": "TO_EQUAL", - ">>> foo\n> bar\n>>baz\n": "TO_EQUAL", - "> code\n\n> not code\n": "NOT_TO_EQUAL", - "A paragraph\nwith two lines.\n\n indented code\n\n> A block quote.\n": "TO_EQUAL", - "1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "TO_EQUAL", - "- one\n\n two\n": "NOT_TO_EQUAL", - "- one\n\n two\n": "NOT_TO_EQUAL", - " - one\n\n two\n": "NOT_TO_EQUAL", - " - one\n\n two\n": "NOT_TO_EQUAL", - " > > 1. one\n>>\n>> two\n": "NOT_TO_EQUAL", - ">>- one\n>>\n > > two\n": "TO_EQUAL", - "-one\n\n2.two\n": "TO_EQUAL", - "- foo\n\n\n bar\n": "TO_EQUAL", - "1. foo\n\n ```\n bar\n ```\n\n baz\n\n > bam\n": "TO_EQUAL", - "- Foo\n\n bar\n\n\n baz\n": "NOT_TO_EQUAL", - "123456789. ok\n": "TO_EQUAL", - "1234567890. not ok\n": "NOT_TO_EQUAL", - "0. ok\n": "NOT_TO_EQUAL", - "003. ok\n": "TO_EQUAL", - "-1. not ok\n": "TO_EQUAL", - "- foo\n\n bar\n": "TO_EQUAL", - " 10. foo\n\n bar\n": "TO_EQUAL", - " indented code\n\nparagraph\n\n more code\n": "TO_EQUAL", - "1. indented code\n\n paragraph\n\n more code\n": "TO_EQUAL", - "1. indented code\n\n paragraph\n\n more code\n": "TO_EQUAL", - " foo\n\nbar\n": "NOT_TO_EQUAL", - "- foo\n\n bar\n": "NOT_TO_EQUAL", - "- foo\n\n bar\n": "NOT_TO_EQUAL", - "-\n foo\n-\n ```\n bar\n ```\n-\n baz\n": "NOT_TO_EQUAL", - "- \n foo\n": "TO_ERROR", - "-\n\n foo\n": "NOT_TO_EQUAL", - "- foo\n-\n- bar\n": "TO_ERROR", - "- foo\n- \n- bar\n": "TO_ERROR", - "1. foo\n2.\n3. bar\n": "TO_ERROR", - "*\n": "NOT_TO_EQUAL", - "foo\n*\n\nfoo\n1.\n": "TO_EQUAL", - " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "TO_EQUAL", - " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "TO_EQUAL", - " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "TO_EQUAL", - " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "NOT_TO_EQUAL", - " 1. A paragraph\nwith two lines.\n\n indented code\n\n > A block quote.\n": "NOT_TO_EQUAL", - " 1. A paragraph\n with two lines.\n": "TO_EQUAL", - "> 1. > Blockquote\ncontinued here.\n": "TO_EQUAL", - "> 1. > Blockquote\n> continued here.\n": "TO_EQUAL", - "- foo\n - bar\n - baz\n - boo\n": "NOT_TO_EQUAL", - "- foo\n - bar\n - baz\n - boo\n": "TO_EQUAL", - "10) foo\n - bar\n": "NOT_TO_EQUAL", - "10) foo\n - bar\n": "TO_EQUAL", - "- - foo\n": "TO_EQUAL", - "1. - 2. foo\n": "TO_EQUAL", - "- # Foo\n- Bar\n ---\n baz\n": "NOT_TO_EQUAL", - "- foo\n- bar\n+ baz\n": "TO_EQUAL", - "1. foo\n2. bar\n3) baz\n": "TO_EQUAL", - "Foo\n- bar\n- baz\n": "TO_EQUAL", - "The number of windows in my house is\n14. The number of doors is 6.\n": "NOT_TO_EQUAL", - "The number of windows in my house is\n1. The number of doors is 6.\n": "TO_EQUAL", - "- foo\n\n- bar\n\n\n- baz\n": "NOT_TO_EQUAL", - "- foo\n - bar\n - baz\n\n\n bim\n": "NOT_TO_EQUAL", - "- foo\n- bar\n\n\n\n- baz\n- bim\n": "TO_EQUAL", - "- foo\n\n notcode\n\n- foo\n\n\n\n code\n": "NOT_TO_EQUAL", - "- a\n - b\n - c\n - d\n - e\n - f\n - g\n - h\n- i\n": "NOT_TO_EQUAL", - "1. a\n\n 2. b\n\n 3. c\n": "NOT_TO_EQUAL", - "- a\n- b\n\n- c\n": "NOT_TO_EQUAL", - "* a\n*\n\n* c\n": "TO_ERROR", - "- a\n- b\n\n c\n- d\n": "NOT_TO_EQUAL", - "- a\n- b\n\n [ref]: /url\n- d\n": "NOT_TO_EQUAL", - "- a\n- ```\n b\n\n\n ```\n- c\n": "NOT_TO_EQUAL", - "- a\n - b\n\n c\n- d\n": "NOT_TO_EQUAL", - "* a\n > b\n >\n* c\n": "NOT_TO_EQUAL", - "- a\n > b\n ```\n c\n ```\n- d\n": "NOT_TO_EQUAL", - "- a\n": "TO_EQUAL", - "- a\n - b\n": "NOT_TO_EQUAL", - "1. ```\n foo\n ```\n\n bar\n": "TO_EQUAL", - "* foo\n * bar\n\n baz\n": "NOT_TO_EQUAL", - "- a\n - b\n - c\n\n- d\n - e\n - f\n": "TO_EQUAL", - "`hi`lo`\n": "TO_EQUAL", - "\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~\n": "NOT_TO_EQUAL", - "\\\t\\A\\a\\ \\3\\φ\\«\n": "TO_EQUAL", - "\\*not emphasized*\n\\
not a tag\n\\[not a link](/foo)\n\\`not code`\n1\\. not a list\n\\* not a list\n\\# not a heading\n\\[foo]: /url \"not a reference\"\n": "NOT_TO_EQUAL", - "\\\\*emphasis*\n": "NOT_TO_EQUAL", - "foo\\\nbar\n": "NOT_TO_EQUAL", - "`` \\[\\` ``\n": "TO_EQUAL", - " \\[\\]\n": "TO_EQUAL", - "~~~\n\\[\\]\n~~~\n": "TO_EQUAL", - "\n": "NOT_TO_EQUAL", - "
\n": "TO_EQUAL", - "[foo](/bar\\* \"ti\\*tle\")\n": "TO_EQUAL", - "[foo]\n\n[foo]: /bar\\* \"ti\\*tle\"\n": "TO_EQUAL", - "``` foo\\+bar\nfoo\n```\n": "TO_EQUAL", - "  & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸\n": "NOT_TO_EQUAL", - "# Ӓ Ϡ � �\n": "NOT_TO_EQUAL", - "" ആ ಫ\n": "NOT_TO_EQUAL", - "  &x; &#; &#x;\n&ThisIsNotDefined; &hi?;\n": "NOT_TO_EQUAL", - "©\n": "NOT_TO_EQUAL", - "&MadeUpEntity;\n": "NOT_TO_EQUAL", - "\n": "TO_EQUAL", - "[foo](/föö \"föö\")\n": "TO_EQUAL", - "[foo]\n\n[foo]: /föö \"föö\"\n": "TO_EQUAL", - "``` föö\nfoo\n```\n": "TO_EQUAL", - "`föö`\n": "NOT_TO_EQUAL", - " föfö\n": "NOT_TO_EQUAL", - "`foo`\n": "TO_EQUAL", - "`` foo ` bar ``\n": "TO_EQUAL", - "` `` `\n": "TO_EQUAL", - "`foo bar\n baz`\n": "TO_EQUAL", - "`a b`\n": "NOT_TO_EQUAL", - "`foo `` bar`\n": "TO_EQUAL", - "`foo\\`bar`\n": "TO_EQUAL", - "*foo`*`\n": "NOT_TO_EQUAL", - "[not a `link](/foo`)\n": "NOT_TO_EQUAL", - "``\n": "NOT_TO_EQUAL", - "`\n": "TO_EQUAL", - "``\n": "NOT_TO_EQUAL", - "`\n": "TO_EQUAL", - "```foo``\n": "NOT_TO_EQUAL", - "`foo\n": "TO_EQUAL", - "`foo``bar``\n": "NOT_TO_EQUAL", - "*foo bar*\n": "TO_EQUAL", - "a * foo bar*\n": "NOT_TO_EQUAL", - "a*\"foo\"*\n": "NOT_TO_EQUAL", - "* a *\n": "NOT_TO_EQUAL", - "foo*bar*\n": "TO_EQUAL", - "5*6*78\n": "NOT_TO_EQUAL", - "_foo bar_\n": "TO_EQUAL", - "_ foo bar_\n": "NOT_TO_EQUAL", - "a_\"foo\"_\n": "NOT_TO_EQUAL", - "foo_bar_\n": "NOT_TO_EQUAL", - "5_6_78\n": "TO_EQUAL", - "пристаням_стремятся_\n": "NOT_TO_EQUAL", - "aa_\"bb\"_cc\n": "NOT_TO_EQUAL", - "foo-_(bar)_\n": "TO_EQUAL", - "_foo*\n": "TO_EQUAL", - "*foo bar *\n": "NOT_TO_EQUAL", - "*foo bar\n*\n": "NOT_TO_EQUAL", - "*(*foo)\n": "NOT_TO_EQUAL", - "*(*foo*)*\n": "NOT_TO_EQUAL", - "*foo*bar\n": "NOT_TO_EQUAL", - "_foo bar _\n": "NOT_TO_EQUAL", - "_(_foo)\n": "TO_EQUAL", - "_(_foo_)_\n": "NOT_TO_EQUAL", - "_foo_bar\n": "TO_EQUAL", - "_пристаням_стремятся\n": "NOT_TO_EQUAL", - "_foo_bar_baz_\n": "TO_EQUAL", - "_(bar)_.\n": "TO_EQUAL", - "**foo bar**\n": "TO_EQUAL", - "** foo bar**\n": "NOT_TO_EQUAL", - "a**\"foo\"**\n": "NOT_TO_EQUAL", - "foo**bar**\n": "TO_EQUAL", - "__foo bar__\n": "TO_EQUAL", - "__ foo bar__\n": "NOT_TO_EQUAL", - "__\nfoo bar__\n": "NOT_TO_EQUAL", - "a__\"foo\"__\n": "NOT_TO_EQUAL", - "foo__bar__\n": "NOT_TO_EQUAL", - "5__6__78\n": "NOT_TO_EQUAL", - "пристаням__стремятся__\n": "NOT_TO_EQUAL", - "__foo, __bar__, baz__\n": "NOT_TO_EQUAL", - "foo-__(bar)__\n": "TO_EQUAL", - "**foo bar **\n": "NOT_TO_EQUAL", - "**(**foo)\n": "NOT_TO_EQUAL", - "*(**foo**)*\n": "TO_EQUAL", - "**Gomphocarpus (*Gomphocarpus physocarpus*, syn.\n*Asclepias physocarpa*)**\n": "TO_EQUAL", - "**foo \"*bar*\" foo**\n": "NOT_TO_EQUAL", - "**foo**bar\n": "TO_EQUAL", - "__foo bar __\n": "NOT_TO_EQUAL", - "__(__foo)\n": "NOT_TO_EQUAL", - "_(__foo__)_\n": "TO_EQUAL", - "__foo__bar\n": "NOT_TO_EQUAL", - "__пристаням__стремятся\n": "NOT_TO_EQUAL", - "__foo__bar__baz__\n": "NOT_TO_EQUAL", - "__(bar)__.\n": "TO_EQUAL", - "*foo [bar](/url)*\n": "TO_EQUAL", - "*foo\nbar*\n": "TO_EQUAL", - "_foo __bar__ baz_\n": "TO_EQUAL", - "_foo _bar_ baz_\n": "NOT_TO_EQUAL", - "__foo_ bar_\n": "NOT_TO_EQUAL", - "*foo *bar**\n": "NOT_TO_EQUAL", - "*foo **bar** baz*\n": "TO_EQUAL", - "*foo**bar**baz*\n": "TO_EQUAL", - "***foo** bar*\n": "NOT_TO_EQUAL", - "*foo **bar***\n": "NOT_TO_EQUAL", - "*foo**bar***\n": "NOT_TO_EQUAL", - "*foo **bar *baz* bim** bop*\n": "NOT_TO_EQUAL", - "*foo [*bar*](/url)*\n": "NOT_TO_EQUAL", - "** is not an empty emphasis\n": "TO_EQUAL", - "**** is not an empty strong emphasis\n": "TO_EQUAL", - "**foo [bar](/url)**\n": "TO_EQUAL", - "**foo\nbar**\n": "TO_EQUAL", - "__foo _bar_ baz__\n": "TO_EQUAL", - "__foo __bar__ baz__\n": "NOT_TO_EQUAL", - "____foo__ bar__\n": "NOT_TO_EQUAL", - "**foo **bar****\n": "NOT_TO_EQUAL", - "**foo *bar* baz**\n": "TO_EQUAL", - "**foo*bar*baz**\n": "NOT_TO_EQUAL", - "***foo* bar**\n": "TO_EQUAL", - "**foo *bar***\n": "TO_EQUAL", - "**foo *bar **baz**\nbim* bop**\n": "NOT_TO_EQUAL", - "**foo [*bar*](/url)**\n": "TO_EQUAL", - "__ is not an empty emphasis\n": "TO_EQUAL", - "____ is not an empty strong emphasis\n": "TO_EQUAL", - "foo ***\n": "TO_EQUAL", - "foo *\\**\n": "NOT_TO_EQUAL", - "foo *_*\n": "NOT_TO_EQUAL", - "foo *****\n": "NOT_TO_EQUAL", - "foo **\\***\n": "TO_EQUAL", - "foo **_**\n": "TO_EQUAL", - "**foo*\n": "TO_EQUAL", - "*foo**\n": "NOT_TO_EQUAL", - "***foo**\n": "NOT_TO_EQUAL", - "****foo*\n": "NOT_TO_EQUAL", - "**foo***\n": "NOT_TO_EQUAL", - "*foo****\n": "NOT_TO_EQUAL", - "foo ___\n": "TO_EQUAL", - "foo _\\__\n": "NOT_TO_EQUAL", - "foo _*_\n": "TO_EQUAL", - "foo _____\n": "NOT_TO_EQUAL", - "foo __\\___\n": "TO_EQUAL", - "foo __*__\n": "TO_EQUAL", - "__foo_\n": "TO_EQUAL", - "_foo__\n": "NOT_TO_EQUAL", - "___foo__\n": "NOT_TO_EQUAL", - "____foo_\n": "NOT_TO_EQUAL", - "__foo___\n": "NOT_TO_EQUAL", - "_foo____\n": "NOT_TO_EQUAL", - "**foo**\n": "TO_EQUAL", - "*_foo_*\n": "NOT_TO_EQUAL", - "__foo__\n": "TO_EQUAL", - "_*foo*_\n": "NOT_TO_EQUAL", - "****foo****\n": "NOT_TO_EQUAL", - "____foo____\n": "NOT_TO_EQUAL", - "******foo******\n": "NOT_TO_EQUAL", - "***foo***\n": "TO_EQUAL", - "_____foo_____\n": "NOT_TO_EQUAL", - "*foo _bar* baz_\n": "TO_EQUAL", - "*foo __bar *baz bim__ bam*\n": "NOT_TO_EQUAL", - "**foo **bar baz**\n": "NOT_TO_EQUAL", - "*foo *bar baz*\n": "NOT_TO_EQUAL", - "*[bar*](/url)\n": "NOT_TO_EQUAL", - "_foo [bar_](/url)\n": "NOT_TO_EQUAL", - "*\n": "NOT_TO_EQUAL", - "**\n": "NOT_TO_EQUAL", - "__\n": "NOT_TO_EQUAL", - "*a `*`*\n": "NOT_TO_EQUAL", - "_a `_`_\n": "NOT_TO_EQUAL", - "**a\n": "NOT_TO_EQUAL", - "__a\n": "NOT_TO_EQUAL", - "[link](/uri \"title\")\n": "TO_EQUAL", - "[link](/uri)\n": "TO_EQUAL", - "[link]()\n": "TO_EQUAL", - "[link](<>)\n": "TO_EQUAL", - "[link](/my uri)\n": "TO_EQUAL", - "[link]()\n": "NOT_TO_EQUAL", - "[link](foo\nbar)\n": "TO_EQUAL", - "[link]()\n": "TO_EQUAL", - "[link](\\(foo\\))\n": "TO_EQUAL", - "[link](foo(and(bar)))\n": "TO_EQUAL", - "[link](foo\\(and\\(bar\\))\n": "TO_EQUAL", - "[link]()\n": "TO_EQUAL", - "[link](foo\\)\\:)\n": "TO_EQUAL", - "[link](#fragment)\n\n[link](http://example.com#fragment)\n\n[link](http://example.com?foo=3#frag)\n": "TO_EQUAL", - "[link](foo\\bar)\n": "TO_EQUAL", - "[link](foo%20bä)\n": "TO_EQUAL", - "[link](\"title\")\n": "TO_EQUAL", - "[link](/url \"title\")\n[link](/url 'title')\n[link](/url (title))\n": "TO_EQUAL", - "[link](/url \"title \\\""\")\n": "NOT_TO_EQUAL", - "[link](/url \"title\")\n": "NOT_TO_EQUAL", - "[link](/url \"title \"and\" title\")\n": "NOT_TO_EQUAL", - "[link](/url 'title \"and\" title')\n": "NOT_TO_EQUAL", - "[link]( /uri\n \"title\" )\n": "TO_EQUAL", - "[link] (/uri)\n": "NOT_TO_EQUAL", - "[link [foo [bar]]](/uri)\n": "NOT_TO_EQUAL", - "[link] bar](/uri)\n": "TO_EQUAL", - "[link [bar](/uri)\n": "TO_EQUAL", - "[link \\[bar](/uri)\n": "NOT_TO_EQUAL", - "[link *foo **bar** `#`*](/uri)\n": "TO_EQUAL", - "[![moon](moon.jpg)](/uri)\n": "NOT_TO_EQUAL", - "[foo [bar](/uri)](/uri)\n": "NOT_TO_EQUAL", - "[foo *[bar [baz](/uri)](/uri)*](/uri)\n": "NOT_TO_EQUAL", - "![[[foo](uri1)](uri2)](uri3)\n": "NOT_TO_EQUAL", - "*[foo*](/uri)\n": "NOT_TO_EQUAL", - "[foo *bar](baz*)\n": "TO_EQUAL", - "*foo [bar* baz]\n": "TO_EQUAL", - "[foo \n": "NOT_TO_EQUAL", - "[foo`](/uri)`\n": "NOT_TO_EQUAL", - "[foo\n": "NOT_TO_EQUAL", - "[foo][bar]\n\n[bar]: /url \"title\"\n": "TO_EQUAL", - "[link [foo [bar]]][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL", - "[link \\[bar][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL", - "[link *foo **bar** `#`*][ref]\n\n[ref]: /uri\n": "TO_EQUAL", - "[![moon](moon.jpg)][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL", - "[foo [bar](/uri)][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL", - "[foo *bar [baz][ref]*][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL", - "*[foo*][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL", - "[foo *bar][ref]\n\n[ref]: /uri\n": "TO_EQUAL", - "[foo \n\n[ref]: /uri\n": "NOT_TO_EQUAL", - "[foo`][ref]`\n\n[ref]: /uri\n": "NOT_TO_EQUAL", - "[foo\n\n[ref]: /uri\n": "NOT_TO_EQUAL", - "[foo][BaR]\n\n[bar]: /url \"title\"\n": "TO_EQUAL", - "[Толпой][Толпой] is a Russian word.\n\n[ТОЛПОЙ]: /url\n": "TO_EQUAL", - "[Foo\n bar]: /url\n\n[Baz][Foo bar]\n": "TO_EQUAL", - "[foo] [bar]\n\n[bar]: /url \"title\"\n": "NOT_TO_EQUAL", - "[foo]\n[bar]\n\n[bar]: /url \"title\"\n": "NOT_TO_EQUAL", - "[foo]: /url1\n\n[foo]: /url2\n\n[bar][foo]\n": "NOT_TO_EQUAL", - "[bar][foo\\!]\n\n[foo!]: /url\n": "NOT_TO_EQUAL", - "[foo][ref[]\n\n[ref[]: /uri\n": "NOT_TO_EQUAL", - "[foo][ref[bar]]\n\n[ref[bar]]: /uri\n": "NOT_TO_EQUAL", - "[[[foo]]]\n\n[[[foo]]]: /url\n": "TO_EQUAL", - "[foo][ref\\[]\n\n[ref\\[]: /uri\n": "TO_EQUAL", - "[bar\\\\]: /uri\n\n[bar\\\\]\n": "NOT_TO_EQUAL", - "[]\n\n[]: /uri\n": "TO_EQUAL", - "[\n ]\n\n[\n ]: /uri\n": "NOT_TO_EQUAL", - "[foo][]\n\n[foo]: /url \"title\"\n": "TO_EQUAL", - "[*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n": "TO_EQUAL", - "[Foo][]\n\n[foo]: /url \"title\"\n": "TO_EQUAL", - "[foo] \n[]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", - "[foo]\n\n[foo]: /url \"title\"\n": "TO_EQUAL", - "[*foo* bar]\n\n[*foo* bar]: /url \"title\"\n": "TO_EQUAL", - "[[*foo* bar]]\n\n[*foo* bar]: /url \"title\"\n": "TO_EQUAL", - "[[bar [foo]\n\n[foo]: /url\n": "TO_EQUAL", - "[Foo]\n\n[foo]: /url \"title\"\n": "TO_EQUAL", - "[foo] bar\n\n[foo]: /url\n": "TO_EQUAL", - "\\[foo]\n\n[foo]: /url \"title\"\n": "TO_EQUAL", - "[foo*]: /url\n\n*[foo*]\n": "NOT_TO_EQUAL", - "[foo][bar]\n\n[foo]: /url1\n[bar]: /url2\n": "TO_EQUAL", - "[foo][]\n\n[foo]: /url1\n": "TO_EQUAL", - "[foo]()\n\n[foo]: /url1\n": "TO_EQUAL", - "[foo](not a link)\n\n[foo]: /url1\n": "TO_EQUAL", - "[foo][bar][baz]\n\n[baz]: /url\n": "NOT_TO_EQUAL", - "[foo][bar][baz]\n\n[baz]: /url1\n[bar]: /url2\n": "TO_EQUAL", - "[foo][bar][baz]\n\n[baz]: /url1\n[foo]: /url2\n": "NOT_TO_EQUAL", - "![foo](/url \"title\")\n": "NOT_TO_EQUAL", - "![foo *bar*]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n": "NOT_TO_EQUAL", - "![foo ![bar](/url)](/url2)\n": "NOT_TO_EQUAL", - "![foo [bar](/url)](/url2)\n": "NOT_TO_EQUAL", - "![foo *bar*][]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n": "NOT_TO_EQUAL", - "![foo *bar*][foobar]\n\n[FOOBAR]: train.jpg \"train & tracks\"\n": "NOT_TO_EQUAL", - "![foo](train.jpg)\n": "NOT_TO_EQUAL", - "My ![foo bar](/path/to/train.jpg \"title\" )\n": "NOT_TO_EQUAL", - "![foo]()\n": "NOT_TO_EQUAL", - "![](/url)\n": "NOT_TO_EQUAL", - "![foo][bar]\n\n[bar]: /url\n": "NOT_TO_EQUAL", - "![foo][bar]\n\n[BAR]: /url\n": "NOT_TO_EQUAL", - "![foo][]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", - "![*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n": "NOT_TO_EQUAL", - "![Foo][]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", - "![foo] \n[]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", - "![foo]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", - "![*foo* bar]\n\n[*foo* bar]: /url \"title\"\n": "NOT_TO_EQUAL", - "![[foo]]\n\n[[foo]]: /url \"title\"\n": "NOT_TO_EQUAL", - "![Foo]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", - "!\\[foo]\n\n[foo]: /url \"title\"\n": "TO_EQUAL", - "\\![foo]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", - "\n": "TO_EQUAL", - "\n": "NOT_TO_EQUAL", - "\n": "TO_EQUAL", - "\n": "NOT_TO_EQUAL", - "\n": "NOT_TO_EQUAL", - "\n": "TO_EQUAL", - "\n": "TO_EQUAL", - "\n": "NOT_TO_EQUAL", - "\n": "NOT_TO_EQUAL", - "\n": "NOT_TO_EQUAL", - "\n": "TO_EQUAL", - "\n": "TO_EQUAL", - "\n": "NOT_TO_EQUAL", - "<>\n": "NOT_TO_EQUAL", - "< http://foo.bar >\n": "NOT_TO_EQUAL", - "\n": "NOT_TO_EQUAL", - "\n": "NOT_TO_EQUAL", - "http://example.com\n": "TO_EQUAL", - "foo@bar.example.com\n": "TO_EQUAL", - "\n": "TO_EQUAL", - "\n": "TO_EQUAL", - "\n": "TO_EQUAL", - "\n": "TO_EQUAL", - "Foo \n": "TO_EQUAL", - "<33> <__>\n": "NOT_TO_EQUAL", - "\n": "NOT_TO_EQUAL", - " \n": "NOT_TO_EQUAL", - "< a><\nfoo>\n": "NOT_TO_EQUAL", - "\n": "NOT_TO_EQUAL", - "\n": "TO_EQUAL", - "\n": "NOT_TO_EQUAL", - "foo \n": "TO_EQUAL", - "foo \n": "NOT_TO_EQUAL", - "foo foo -->\n\nfoo \n": "NOT_TO_EQUAL", - "foo \n": "TO_EQUAL", - "foo \n": "TO_EQUAL", - "foo &<]]>\n": "NOT_TO_EQUAL", - "foo \n": "TO_EQUAL", - "foo \n": "TO_EQUAL", - "\n": "NOT_TO_EQUAL", - "foo \nbaz\n": "NOT_TO_EQUAL", - "foo\\\nbaz\n": "NOT_TO_EQUAL", - "foo \nbaz\n": "NOT_TO_EQUAL", - "foo \n bar\n": "NOT_TO_EQUAL", - "foo\\\n bar\n": "NOT_TO_EQUAL", - "*foo \nbar*\n": "NOT_TO_EQUAL", - "*foo\\\nbar*\n": "NOT_TO_EQUAL", - "`code \nspan`\n": "TO_EQUAL", - "`code\\\nspan`\n": "TO_EQUAL", - "\n": "TO_EQUAL", - "\n": "TO_EQUAL", - "foo\\\n": "TO_EQUAL", - "foo \n": "TO_EQUAL", - "### foo\\\n": "TO_EQUAL", - "### foo \n": "TO_EQUAL", - "foo\nbaz\n": "TO_EQUAL", - "foo \n baz\n": "TO_EQUAL", - "hello $.;'there\n": "TO_EQUAL", - "Foo χρῆν\n": "TO_EQUAL", - "Multiple spaces\n": "TO_EQUAL" -} \ No newline at end of file diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/duplicate_marks_github_issue_3280.md b/packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/duplicate_marks_github_issue_3280.md deleted file mode 100644 index 2d6cad7de1d4..000000000000 --- a/packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/duplicate_marks_github_issue_3280.md +++ /dev/null @@ -1 +0,0 @@ -Fill to_*this*_mark, and your charge is but a penny; to_*this*_a penny more; and so on to the full glass—the Cape Horn measure, which you may gulp down for a shilling.\n\nUpon entering the place I found a number of young seamen gathered about a table, examining by a dim light divers specimens of_*skrimshander*. \ No newline at end of file diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/commonmark.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/commonmark.spec.js deleted file mode 100644 index 5998821084f9..000000000000 --- a/packages/decap-cms-widget-richtext/src/serializers/__tests__/commonmark.spec.js +++ /dev/null @@ -1,110 +0,0 @@ -import { flow } from 'lodash'; -import { tests as commonmarkSpec } from 'commonmark-spec'; -import * as commonmark from 'commonmark'; - -import { markdownToSlate, slateToMarkdown } from '../index.js'; - -const skips = [ - { - number: [456], - reason: 'Remark ¯\\_(ツ)_/¯', - }, - { - number: [416, 417, 424, 425, 426, 431, 457, 460, 462, 464, 467], - reason: 'Remark does not support infinite (redundant) nested marks', - }, - { - number: [455, 469, 470, 471], - reason: 'Remark parses the initial set of identical nested delimiters first', - }, - { - number: [473, 476, 478, 480], - reason: 'we convert underscores to asterisks for strong/emphasis', - }, - { number: 490, reason: 'Remark strips pointy enclosing pointy brackets from link url' }, - { number: 503, reason: 'Remark allows non-breaking space between link url and title' }, - { number: 507, reason: 'Remark allows a space between link alt and url' }, - { - number: [ - 511, 516, 525, 528, 529, 530, 532, 533, 534, 540, 541, 542, 543, 546, 548, 560, 565, 567, - ], - reason: 'we convert link references to standard links, but Remark also fails these', - }, - { - number: [569, 570, 571, 572, 573, 581, 585], - reason: 'Remark does not recognize or remove marks in image alt text', - }, - { number: 589, reason: 'Remark does not honor backslash escape of image exclamation point' }, - { number: 593, reason: 'Remark removes "mailto:" from autolink text' }, - { number: 599, reason: 'Remark does not escape all expected entities' }, - { number: 602, reason: 'Remark allows autolink emails to contain backslashes' }, -]; - -const onlys = [ - // just add the spec number, eg: - // 431, -]; - -/** - * Each test receives input markdown and output html as expected for Commonmark - * compliance. To test all of our handling in one go, we serialize the markdown - * into our Slate AST, then back to raw markdown, and finally to HTML. - */ -const reader = new commonmark.Parser(); -const writer = new commonmark.HtmlRenderer(); - -function parseWithCommonmark(markdown) { - const parsed = reader.parse(markdown); - return writer.render(parsed); -} - -const parse = flow([markdownToSlate, slateToMarkdown]); - -/** - * Passing this test suite requires 100% Commonmark compliance. There are 624 - * tests, of which we're passing about 300 as of introduction of this suite. To - * work on improving Commonmark support, update __fixtures__/commonmarkExpected.json - */ -describe.skip('Commonmark support', function () { - const specs = - onlys.length > 0 - ? commonmarkSpec.filter(({ number }) => onlys.includes(number)) - : commonmarkSpec; - specs.forEach(spec => { - const skip = skips.find(({ number }) => { - return Array.isArray(number) ? number.includes(spec.number) : number === spec.number; - }); - const specUrl = `https://spec.commonmark.org/0.29/#example-${spec.number}`; - const parsed = parse(spec.markdown); - const commonmarkParsedHtml = parseWithCommonmark(parsed); - const description = ` -${spec.section} -${specUrl} - -Spec: -${JSON.stringify(spec, null, 2)} - -Markdown input: -${spec.markdown} - -Markdown parsed through Slate/Remark and back to Markdown: -${parsed} - -HTML output: -${commonmarkParsedHtml} - -Expected HTML output: -${spec.html} - `; - if (skip) { - const showMessage = Array.isArray(skip.number) ? skip.number[0] === spec.number : true; - if (showMessage) { - //console.log(`skipping spec ${skip.number}\n${skip.reason}\n${specUrl}`); - } - } - const testFn = skip ? test.skip : test; - testFn(description, () => { - expect(commonmarkParsedHtml).toEqual(spec.html); - }); - }); -}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/index.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/index.spec.js deleted file mode 100644 index 33a4fac515ac..000000000000 --- a/packages/decap-cms-widget-richtext/src/serializers/__tests__/index.spec.js +++ /dev/null @@ -1,52 +0,0 @@ -import path from 'path'; -import fs from 'fs'; - -import { markdownToSlate, htmlToSlate } from '../'; - -describe('markdownToSlate', () => { - it('should not add duplicate identical marks under the same node (GitHub Issue 3280)', () => { - const mdast = fs.readFileSync( - path.join(__dirname, '__fixtures__', 'duplicate_marks_github_issue_3280.md'), - ); - const slate = markdownToSlate(mdast); - - expect(slate).toEqual([ - { - type: 'p', - children: [ - { - text: 'Fill to', - }, - { - italic: true, - marks: [{ type: 'italic' }], - text: 'this_mark, and your charge is but a penny; tothisa penny more; and so on to the full glass—the Cape Horn measure, which you may gulp down for a shilling.\\n\\nUpon entering the place I found a number of young seamen gathered about a table, examining by a dim light divers specimens ofskrimshander', - }, - { - text: '.', - }, - ], - }, - ]); - }); -}); - -describe('htmlToSlate', () => { - it('should preserve spaces in rich html (GitHub Issue 3727)', () => { - const html = `Bold Text regular text `; - - const actual = htmlToSlate(html); - expect(actual).toEqual({ - type: 'root', - children: [ - { - type: 'p', - children: [ - { text: 'Bold Text', bold: true, marks: [{ type: 'bold' }] }, - { text: ' regular text' }, - ], - }, - ], - }); - }); -}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAllowHtmlEntities.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAllowHtmlEntities.spec.js deleted file mode 100644 index 844137f0a440..000000000000 --- a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAllowHtmlEntities.spec.js +++ /dev/null @@ -1,25 +0,0 @@ -import unified from 'unified'; -import markdownToRemark from 'remark-parse'; - -import remarkAllowHtmlEntities from '../remarkAllowHtmlEntities'; - -function process(markdown) { - const mdast = unified().use(markdownToRemark).use(remarkAllowHtmlEntities).parse(markdown); - - /** - * The MDAST will look like: - * - * { type: 'root', children: [ - * { type: 'paragraph', children: [ - * // results here - * ]} - * ]} - */ - return mdast.children[0].children[0].value; -} - -describe('remarkAllowHtmlEntities', () => { - it('should not decode HTML entities', () => { - expect(process('<div>')).toEqual('<div>'); - }); -}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAssertParents.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAssertParents.spec.js deleted file mode 100644 index 670d5c7900ef..000000000000 --- a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAssertParents.spec.js +++ /dev/null @@ -1,171 +0,0 @@ -import u from 'unist-builder'; - -import remarkAssertParents from '../remarkAssertParents'; - -const transform = remarkAssertParents(); - -describe('remarkAssertParents', () => { - it('should unnest invalidly nested blocks', () => { - const input = u('root', [ - u('paragraph', [ - u('paragraph', [u('text', 'Paragraph text.')]), - u('heading', { depth: 1 }, [u('text', 'Heading text.')]), - u('code', 'someCode()'), - u('blockquote', [u('text', 'Quote text.')]), - u('list', [u('listItem', [u('text', 'A list item.')])]), - u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]), - u('thematicBreak'), - ]), - ]); - - const output = u('root', [ - u('paragraph', [u('text', 'Paragraph text.')]), - u('heading', { depth: 1 }, [u('text', 'Heading text.')]), - u('code', 'someCode()'), - u('blockquote', [u('text', 'Quote text.')]), - u('list', [u('listItem', [u('text', 'A list item.')])]), - u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]), - u('thematicBreak'), - ]); - - expect(transform(input)).toEqual(output); - }); - - it('should unnest deeply nested blocks', () => { - const input = u('root', [ - u('paragraph', [ - u('paragraph', [ - u('paragraph', [ - u('paragraph', [u('text', 'Paragraph text.')]), - u('heading', { depth: 1 }, [u('text', 'Heading text.')]), - u('code', 'someCode()'), - u('blockquote', [ - u('paragraph', [u('strong', [u('heading', [u('text', 'Quote text.')])])]), - ]), - u('list', [u('listItem', [u('text', 'A list item.')])]), - u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]), - u('thematicBreak'), - ]), - ]), - ]), - ]); - - const output = u('root', [ - u('paragraph', [u('text', 'Paragraph text.')]), - u('heading', { depth: 1 }, [u('text', 'Heading text.')]), - u('code', 'someCode()'), - u('blockquote', [u('heading', [u('text', 'Quote text.')])]), - u('list', [u('listItem', [u('text', 'A list item.')])]), - u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]), - u('thematicBreak'), - ]); - - expect(transform(input)).toEqual(output); - }); - - it('should remove blocks that are emptied as a result of denesting', () => { - const input = u('root', [ - u('paragraph', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]), - ]); - - const output = u('root', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]); - - expect(transform(input)).toEqual(output); - }); - - it('should remove blocks that are emptied as a result of denesting', () => { - const input = u('root', [ - u('paragraph', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]), - ]); - - const output = u('root', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]); - - expect(transform(input)).toEqual(output); - }); - - it('should handle asymmetrical splits', () => { - const input = u('root', [ - u('paragraph', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]), - ]); - - const output = u('root', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]); - - expect(transform(input)).toEqual(output); - }); - - it('should nest invalidly nested blocks in the nearest valid ancestor', () => { - const input = u('root', [ - u('paragraph', [ - u('blockquote', [u('strong', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])])]), - ]), - ]); - - const output = u('root', [ - u('blockquote', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]), - ]); - - expect(transform(input)).toEqual(output); - }); - - it('should preserve validly nested siblings of invalidly nested blocks', () => { - const input = u('root', [ - u('paragraph', [ - u('blockquote', [ - u('strong', [ - u('text', 'Deep validly nested text a.'), - u('heading', { depth: 1 }, [u('text', 'Heading text.')]), - u('text', 'Deep validly nested text b.'), - ]), - ]), - u('text', 'Validly nested text.'), - ]), - ]); - - const output = u('root', [ - u('blockquote', [ - u('strong', [u('text', 'Deep validly nested text a.')]), - u('heading', { depth: 1 }, [u('text', 'Heading text.')]), - u('strong', [u('text', 'Deep validly nested text b.')]), - ]), - u('paragraph', [u('text', 'Validly nested text.')]), - ]); - - expect(transform(input)).toEqual(output); - }); - - it('should allow intermediate parents like list and table to contain required block children', () => { - const input = u('root', [ - u('blockquote', [ - u('list', [ - u('listItem', [ - u('table', [ - u('tableRow', [ - u('tableCell', [ - u('heading', { depth: 1 }, [u('text', 'Validly nested heading text.')]), - ]), - ]), - ]), - ]), - ]), - ]), - ]); - - const output = u('root', [ - u('blockquote', [ - u('list', [ - u('listItem', [ - u('table', [ - u('tableRow', [ - u('tableCell', [ - u('heading', { depth: 1 }, [u('text', 'Validly nested heading text.')]), - ]), - ]), - ]), - ]), - ]), - ]), - ]); - - expect(transform(input)).toEqual(output); - }); -}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkEscapeMarkdownEntities.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkEscapeMarkdownEntities.spec.js deleted file mode 100644 index 37ff0a85d36f..000000000000 --- a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkEscapeMarkdownEntities.spec.js +++ /dev/null @@ -1,84 +0,0 @@ -import unified from 'unified'; -import u from 'unist-builder'; - -import remarkEscapeMarkdownEntities from '../remarkEscapeMarkdownEntities'; - -function process(text) { - const tree = u('root', [u('text', text)]); - const escapedMdast = unified().use(remarkEscapeMarkdownEntities).runSync(tree); - - return escapedMdast.children[0].value; -} - -describe('remarkEscapeMarkdownEntities', () => { - it('should escape common markdown entities', () => { - expect(process('*a*')).toEqual('\\*a\\*'); - expect(process('**a**')).toEqual('\\*\\*a\\*\\*'); - expect(process('***a***')).toEqual('\\*\\*\\*a\\*\\*\\*'); - expect(process('_a_')).toEqual('\\_a\\_'); - expect(process('__a__')).toEqual('\\_\\_a\\_\\_'); - expect(process('~~a~~')).toEqual('\\~\\~a\\~\\~'); - expect(process('[]')).toEqual('\\[]'); - expect(process('[]()')).toEqual('\\[]()'); - expect(process('[a](b)')).toEqual('\\[a](b)'); - expect(process('[Test sentence.](https://www.example.com)')).toEqual( - '\\[Test sentence.](https://www.example.com)', - ); - expect(process('![a](b)')).toEqual('!\\[a](b)'); - }); - - it('should not escape inactive, single markdown entities', () => { - expect(process('a*b')).toEqual('a*b'); - expect(process('_')).toEqual('_'); - expect(process('~')).toEqual('~'); - expect(process('[')).toEqual('['); - }); - - it('should escape leading markdown entities', () => { - expect(process('#')).toEqual('\\#'); - expect(process('-')).toEqual('\\-'); - expect(process('*')).toEqual('\\*'); - expect(process('>')).toEqual('\\>'); - expect(process('=')).toEqual('\\='); - expect(process('|')).toEqual('\\|'); - expect(process('```')).toEqual('\\`\\``'); - expect(process(' ')).toEqual('\\ '); - }); - - it('should escape leading markdown entities preceded by whitespace', () => { - expect(process('\n #')).toEqual('\\#'); - expect(process(' \n-')).toEqual('\\-'); - }); - - it('should not escape leading markdown entities preceded by non-whitespace characters', () => { - expect(process('a# # b #')).toEqual('a# # b #'); - expect(process('a- - b -')).toEqual('a- - b -'); - }); - - it('should not escape html tags', () => { - expect(process('')).toEqual(''); - expect(process('a b e')).toEqual('a b e'); - }); - - it('should escape the contents of html blocks', () => { - expect(process('
*a*
')).toEqual('
\\*a\\*
'); - }); - - it('should not escape the contents of preformatted html blocks', () => { - expect(process('
*a*
')).toEqual('
*a*
'); - expect(process('')).toEqual(''); - expect(process('')).toEqual(''); - expect(process('
\n*a*\n
')).toEqual('
\n*a*\n
'); - expect(process('a b
*c*
d e')).toEqual('a b
*c*
d e'); - }); - - it('should not escape footnote references', () => { - expect(process('[^a]')).toEqual('[^a]'); - expect(process('[^1]')).toEqual('[^1]'); - }); - - it('should not escape footnotes', () => { - expect(process('[^a]:')).toEqual('[^a]:'); - expect(process('[^1]:')).toEqual('[^1]:'); - }); -}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPaddedLinks.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPaddedLinks.spec.js deleted file mode 100644 index 0d5cf4abdf22..000000000000 --- a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPaddedLinks.spec.js +++ /dev/null @@ -1,43 +0,0 @@ -import unified from 'unified'; -import markdownToRemark from 'remark-parse'; -import remarkToMarkdown from 'remark-stringify'; - -import remarkPaddedLinks from '../remarkPaddedLinks'; - -function input(markdown) { - return unified() - .use(markdownToRemark) - .use(remarkPaddedLinks) - .use(remarkToMarkdown) - .processSync(markdown).contents; -} - -function output(markdown) { - return unified().use(markdownToRemark).use(remarkToMarkdown).processSync(markdown).contents; -} - -describe('remarkPaddedLinks', () => { - it('should move leading and trailing spaces outside of a link', () => { - expect(input('[ a ](b)')).toEqual(output(' [a](b) ')); - }); - - it('should convert multiple leading or trailing spaces to a single space', () => { - expect(input('[ a ](b)')).toEqual(output(' [a](b) ')); - }); - - it('should work with only a leading space or only a trailing space', () => { - expect(input('[ a](b)[c ](d)')).toEqual(output(' [a](b)[c](d) ')); - }); - - it('should work for nested links', () => { - expect(input('* # a[ b ](c)d')).toEqual(output('* # a [b](c) d')); - }); - - it('should work for parents with multiple links that are not siblings', () => { - expect(input('# a[ b ](c)d **[ e ](f)**')).toEqual(output('# a [b](c) d ** [e](f) **')); - }); - - it('should work for links with arbitrarily nested children', () => { - expect(input('[ a __*b*__ _c_ ](d)')).toEqual(output(' [a __*b*__ _c_](d) ')); - }); -}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPlugins.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPlugins.spec.js deleted file mode 100644 index 83ccf907e8d9..000000000000 --- a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPlugins.spec.js +++ /dev/null @@ -1,299 +0,0 @@ -import visit from 'unist-util-visit'; - -import { markdownToRemark, remarkToMarkdown } from '..'; - -describe('registered remark plugins', () => { - function withNetlifyLinks() { - return function transformer(tree) { - visit(tree, 'link', function onLink(node) { - node.url = 'https://netlify.com'; - }); - }; - } - - it('should use remark transformer plugins when converting mdast to markdown', () => { - const plugins = [withNetlifyLinks]; - const result = remarkToMarkdown( - { - type: 'root', - children: [ - { - type: 'paragraph', - children: [ - { - type: 'text', - value: 'Some ', - }, - { - type: 'emphasis', - children: [ - { - type: 'text', - value: 'important', - }, - ], - }, - { - type: 'text', - value: ' text with ', - }, - { - type: 'link', - title: null, - url: 'https://this-value-should-be-replaced.com', - children: [ - { - type: 'text', - value: 'a link', - }, - ], - }, - { - type: 'text', - value: ' in it.', - }, - ], - }, - ], - }, - plugins, - ); - expect(result).toMatchInlineSnapshot( - `"Some *important* text with [a link](https://netlify.com) in it."`, - ); - }); - - it('should use remark transformer plugins when converting markdown to mdast', () => { - const plugins = [withNetlifyLinks]; - const result = markdownToRemark( - 'Some text with [a link](https://this-value-should-be-replaced.com) in it.', - plugins, - ); - expect(result).toMatchInlineSnapshot(` -Object { - "children": Array [ - Object { - "children": Array [ - Object { - "children": Array [], - "position": Position { - "end": Object { - "column": 16, - "line": 1, - "offset": 15, - }, - "indent": Array [], - "start": Object { - "column": 1, - "line": 1, - "offset": 0, - }, - }, - "type": "text", - "value": "Some text with ", - }, - Object { - "children": Array [ - Object { - "children": Array [], - "position": Position { - "end": Object { - "column": 23, - "line": 1, - "offset": 22, - }, - "indent": Array [], - "start": Object { - "column": 17, - "line": 1, - "offset": 16, - }, - }, - "type": "text", - "value": "a link", - }, - ], - "position": Position { - "end": Object { - "column": 67, - "line": 1, - "offset": 66, - }, - "indent": Array [], - "start": Object { - "column": 16, - "line": 1, - "offset": 15, - }, - }, - "title": null, - "type": "link", - "url": "https://netlify.com", - }, - Object { - "children": Array [], - "position": Position { - "end": Object { - "column": 74, - "line": 1, - "offset": 73, - }, - "indent": Array [], - "start": Object { - "column": 67, - "line": 1, - "offset": 66, - }, - }, - "type": "text", - "value": " in it.", - }, - ], - "position": Position { - "end": Object { - "column": 74, - "line": 1, - "offset": 73, - }, - "indent": Array [], - "start": Object { - "column": 1, - "line": 1, - "offset": 0, - }, - }, - "type": "paragraph", - }, - ], - "position": Object { - "end": Object { - "column": 74, - "line": 1, - "offset": 73, - }, - "start": Object { - "column": 1, - "line": 1, - "offset": 0, - }, - }, - "type": "root", -} -`); - }); - - it('should use remark serializer plugins when converting mdast to markdown', () => { - function withEscapedLessThanChar() { - if (this.Compiler) { - this.Compiler.prototype.visitors.text = node => { - return node.value.replace(/ { - const settings = { - emphasis: '_', - bullet: '-', - }; - - const plugins = [{ settings }]; - const result = remarkToMarkdown( - { - type: 'root', - children: [ - { - type: 'paragraph', - children: [ - { - type: 'text', - value: 'Some ', - }, - { - type: 'emphasis', - children: [ - { - type: 'text', - value: 'important', - }, - ], - }, - { - type: 'text', - value: ' points:', - }, - ], - }, - { - type: 'list', - ordered: false, - start: null, - spread: false, - children: [ - { - type: 'listItem', - spread: false, - checked: null, - children: [ - { - type: 'paragraph', - children: [ - { - type: 'text', - value: 'One', - }, - ], - }, - ], - }, - { - type: 'listItem', - spread: false, - checked: null, - children: [ - { - type: 'paragraph', - children: [ - { - type: 'text', - value: 'Two', - }, - ], - }, - ], - }, - ], - }, - ], - }, - plugins, - ); - expect(result).toMatchInlineSnapshot(` -"Some _important_ points: - -- One -- Two" -`); - }); -}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkShortcodes.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkShortcodes.spec.js deleted file mode 100644 index 53d4ea042ab9..000000000000 --- a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkShortcodes.spec.js +++ /dev/null @@ -1,106 +0,0 @@ -import { Map, OrderedMap } from 'immutable'; - -import { remarkParseShortcodes, getLinesWithOffsets } from '../remarkShortcodes'; - -// Stub of Remark Parser -function process(value, plugins, processEat = () => {}) { - function eat() { - return processEat; - } - - function Parser() {} - Parser.prototype.blockTokenizers = {}; - Parser.prototype.blockMethods = []; - remarkParseShortcodes.call({ Parser }, { plugins }); - Parser.prototype.blockTokenizers.shortcode(eat, value); -} - -function EditorComponent({ id = 'foo', fromBlock = jest.fn(), pattern }) { - return { - id, - fromBlock, - pattern, - }; -} - -describe('remarkParseShortcodes', () => { - describe('pattern matching', () => { - it('should work', () => { - const editorComponent = EditorComponent({ pattern: /bar/ }); - process('foo bar', Map({ [editorComponent.id]: editorComponent })); - expect(editorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['bar'])); - }); - it('should match value surrounded in newlines', () => { - const editorComponent = EditorComponent({ pattern: /^bar$/ }); - process('foo\n\nbar\n', Map({ [editorComponent.id]: editorComponent })); - expect(editorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['bar'])); - }); - it('should match multiline shortcodes', () => { - const editorComponent = EditorComponent({ pattern: /^foo\nbar$/ }); - process('foo\nbar', Map({ [editorComponent.id]: editorComponent })); - expect(editorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['foo\nbar'])); - }); - it('should match multiline shortcodes with empty lines', () => { - const editorComponent = EditorComponent({ pattern: /^foo\n\nbar$/ }); - process('foo\n\nbar', Map({ [editorComponent.id]: editorComponent })); - expect(editorComponent.fromBlock).toHaveBeenCalledWith( - expect.arrayContaining(['foo\n\nbar']), - ); - }); - it('should match shortcodes based on order of occurrence in value', () => { - const fooEditorComponent = EditorComponent({ id: 'foo', pattern: /foo/ }); - const barEditorComponent = EditorComponent({ id: 'bar', pattern: /bar/ }); - process( - 'foo\n\nbar', - OrderedMap([ - [barEditorComponent.id, barEditorComponent], - [fooEditorComponent.id, fooEditorComponent], - ]), - ); - expect(fooEditorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['foo'])); - }); - it('should match shortcodes based on order of occurrence in value even when some use line anchors', () => { - const barEditorComponent = EditorComponent({ id: 'bar', pattern: /bar/ }); - const bazEditorComponent = EditorComponent({ id: 'baz', pattern: /^baz$/ }); - process( - 'foo\n\nbar\n\nbaz', - OrderedMap([ - [bazEditorComponent.id, bazEditorComponent], - [barEditorComponent.id, barEditorComponent], - ]), - ); - expect(barEditorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['bar'])); - }); - }); - describe('output', () => { - it('should be a remark shortcode node', () => { - const processEat = jest.fn(); - const shortcodeData = { bar: 'baz' }; - const expectedNode = { type: 'shortcode', data: { shortcode: 'foo', shortcodeData } }; - const editorComponent = EditorComponent({ pattern: /bar/, fromBlock: () => shortcodeData }); - process('foo bar', Map({ [editorComponent.id]: editorComponent }), processEat); - expect(processEat).toHaveBeenCalledWith(expectedNode); - }); - }); -}); - -describe('getLinesWithOffsets', () => { - test('should split into lines', () => { - const value = ' line1\n\nline2 \n\n line3 \n\n'; - - const lines = getLinesWithOffsets(value); - expect(lines).toEqual([ - { line: ' line1', start: 0 }, - { line: 'line2', start: 8 }, - { line: ' line3', start: 16 }, - { line: '', start: 30 }, - ]); - }); - - test('should return single item on no match', () => { - const value = ' line1 '; - - const lines = getLinesWithOffsets(value); - expect(lines).toEqual([{ line: ' line1', start: 0 }]); - }); -}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkSlate.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkSlate.spec.js deleted file mode 100644 index d55f4416c7df..000000000000 --- a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkSlate.spec.js +++ /dev/null @@ -1,67 +0,0 @@ -import { mergeAdjacentTexts } from '../remarkSlate'; -describe('remarkSlate', () => { - describe('mergeAdjacentTexts', () => { - it('should handle empty array', () => { - const children = []; - expect(mergeAdjacentTexts(children)).toBe(children); - }); - - it('should merge adjacent texts with same marks', () => { - const children = [ - { text: '
', marks: [] }, - { text: 'Netlify', marks: [] }, - { text: '', marks: [] }, - ]; - - expect(mergeAdjacentTexts(children)).toEqual([ - { - text: 'Netlify', - marks: [], - }, - ]); - }); - - it('should not merge adjacent texts with different marks', () => { - const children = [ - { text: '', marks: [] }, - { text: 'Netlify', marks: ['b'] }, - { text: '', marks: [] }, - ]; - - expect(mergeAdjacentTexts(children)).toEqual(children); - }); - - it('should handle mixed children array', () => { - const children = [ - { object: 'inline' }, - { text: '', marks: [] }, - { text: 'Netlify', marks: [] }, - { text: '', marks: [] }, - { object: 'inline' }, - { text: '', marks: [] }, - { text: 'Netlify', marks: ['b'] }, - { text: '', marks: [] }, - { text: '', marks: [] }, - { object: 'inline' }, - { text: '', marks: [] }, - ]; - - expect(mergeAdjacentTexts(children)).toEqual([ - { object: 'inline' }, - { - text: 'Netlify', - marks: [], - }, - { object: 'inline' }, - { text: '', marks: [] }, - { text: 'Netlify', marks: ['b'] }, - { - text: '', - marks: [], - }, - { object: 'inline' }, - { text: '', marks: [] }, - ]); - }); - }); -}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkStripTrailingBreaks.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkStripTrailingBreaks.spec.js deleted file mode 100644 index 67ff12b9689a..000000000000 --- a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkStripTrailingBreaks.spec.js +++ /dev/null @@ -1,23 +0,0 @@ -import unified from 'unified'; -import u from 'unist-builder'; - -import remarkStripTrailingBreaks from '../remarkStripTrailingBreaks'; - -function process(children) { - const tree = u('root', children); - const strippedMdast = unified().use(remarkStripTrailingBreaks).runSync(tree); - - return strippedMdast.children; -} - -describe('remarkStripTrailingBreaks', () => { - it('should remove trailing breaks at the end of a block', () => { - expect(process([u('break')])).toEqual([]); - expect(process([u('break'), u('text', '\n \n')])).toEqual([u('text', '\n \n')]); - expect(process([u('text', 'a'), u('break')])).toEqual([u('text', 'a')]); - }); - - it('should not remove trailing breaks that are not at the end of a block', () => { - expect(process([u('break'), u('text', 'a')])).toEqual([u('break'), u('text', 'a')]); - }); -}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/slate.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/slate.spec.js deleted file mode 100644 index e05cd76ba962..000000000000 --- a/packages/decap-cms-widget-richtext/src/serializers/__tests__/slate.spec.js +++ /dev/null @@ -1,300 +0,0 @@ -/** @jsx h */ - -import { flow } from 'lodash'; - -import h from '../../../test-helpers/h'; -import { markdownToSlate, slateToMarkdown } from '../index'; - -const process = flow([markdownToSlate, slateToMarkdown]); - -describe('slate', () => { - it('should not decode encoded html entities in inline code', () => { - expect(process('<div>')).toEqual( - '<div>', - ); - }); - - it('should parse non-text children of mark nodes', () => { - expect(process('**a[b](c)d**')).toEqual('**a[b](c)d**'); - expect(process('**[a](b)**')).toEqual('**[a](b)**'); - expect(process('**![a](b)**')).toEqual('**![a](b)**'); - expect(process('_`a`_')).toEqual('*`a`*'); - }); - - it('should handle unstyled code nodes adjacent to styled code nodes', () => { - expect(process('`foo`***`bar`***')).toEqual('`foo`***`bar`***'); - }); - - it('should handle styled code nodes adjacent to non-code text', () => { - expect(process('_`a`b_')).toEqual('*`a`b*'); - expect(process('_`a`**b**_')).toEqual('*`a`**b***'); - }); - - it('should condense adjacent, identically styled text and inline nodes', () => { - expect(process('**a ~~b~~~~c~~**')).toEqual('**a ~~bc~~**'); - expect(process('**a ~~b~~~~[c](d)~~**')).toEqual('**a ~~b[c](d)~~**'); - }); - - it('should handle nested markdown entities', () => { - expect(process('**a**b**c**')).toEqual('**a**b**c**'); - expect(process('**a _b_ c**')).toEqual('**a *b* c**'); - expect(process('*`a`*')).toEqual('*`a`*'); - }); - - it('should parse inline images as images', () => { - expect(process('a ![b](c)')).toEqual('a ![b](c)'); - }); - - it('should not escape markdown entities in html', () => { - expect(process('*')).toEqual('*'); - }); - - it('should wrap break tags in surrounding marks', () => { - expect(process('*a \nb*')).toEqual('*a\\\nb*'); - }); - - // slateAst no longer valid - - it('should not output empty headers in markdown', () => { - // prettier-ignore - const slateAst = ( - - - foo - - - - ); - expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"foo"`); - }); - - it('should not output empty marks in markdown', () => { - // prettier-ignore - const slateAst = ( - - - - foobar - baz - - - ); - expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"foobarbaz"`); - }); - - it('should not produce invalid markdown when a styled block has trailing whitespace', () => { - // prettier-ignore - const slateAst = ( - - - foo bar bim bam - - - ); - expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"**foo** bar **bim *bam***"`); - }); - - it('should not produce invalid markdown when a styled block has leading whitespace', () => { - // prettier-ignore - const slateAst = ( - - - foo bar - - - ); - expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"foo **bar**"`); - }); - - it('should group adjacent marks into a single mark when possible', () => { - // prettier-ignore - const slateAst = ( - - - shared mark - - link - - {' '} - not shared mark - - another - link - - - - ); - expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot( - `"**shared mark*[link](link)*** **not shared mark***[another **link**](link)*"`, - ); - }); - - describe('links', () => { - it('should handle inline code in link content', () => { - // prettier-ignore - const slateAst = ( - - - - foo - - - - ); - expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"[\`foo\`](link)"`); - }); - }); - - describe('code marks', () => { - it('can contain other marks', () => { - // prettier-ignore - const slateAst = ( - - - foo - - - ); - expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"***\`foo\`***"`); - }); - - it('can be condensed when no other marks are present', () => { - // prettier-ignore - const slateAst = ( - - - foo - bar - - - ); - expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"\`foo\`"`); - }); - }); - - describe('with nested styles within a single word', () => { - it('should not produce invalid markdown when a bold word has italics applied to a smaller part', () => { - // prettier-ignore - const slateAst = ( - - - h - e - y - - - ); - expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"**h*e*y**"`); - }); - - it('should not produce invalid markdown when an italic word has bold applied to a smaller part', () => { - // prettier-ignore - const slateAst = ( - - - h - e - y - - - ); - expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"*h**e**y*"`); - }); - - it('should handle italics inside bold inside strikethrough', () => { - // prettier-ignore - const slateAst = ( - - - h - e - l - l - o - - - ); - expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"~~h**e*l*l**o~~"`); - }); - - it('should handle bold inside italics inside strikethrough', () => { - // prettier-ignore - const slateAst = ( - - - h - e - l - l - o - - - ); - expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"~~h*e**l**l*o~~"`); - }); - - it('should handle strikethrough inside italics inside bold', () => { - // prettier-ignore - const slateAst = ( - - - h - e - l - l - o - - - ); - expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"**h*e~~l~~l*o**"`); - }); - - it('should handle italics inside strikethrough inside bold', () => { - // prettier-ignore - const slateAst = ( - - - h - e - l - l - o - - - ); - expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"**h~~e*l*l~~o**"`); - }); - - it('should handle strikethrough inside bold inside italics', () => { - // prettier-ignore - const slateAst = ( - - - h - e - l - l - o - - - ); - expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"*h**e~~l~~l**o*"`); - }); - - it('should handle bold inside strikethrough inside italics', () => { - // prettier-ignore - const slateAst = ( - - - h - e - l - l - o - - - ); - expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"*h~~e**l**l~~o*"`); - }); - }); -}); From 6554142d64768a5bd20cf2292495ee4a7b922647 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Fri, 17 Oct 2025 10:05:16 +0200 Subject: [PATCH 24/43] fix: add initialValue to markdown-widget slate editor --- .../decap-cms-widget-markdown/src/MarkdownControl/RawEditor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/decap-cms-widget-markdown/src/MarkdownControl/RawEditor.js b/packages/decap-cms-widget-markdown/src/MarkdownControl/RawEditor.js index 6a99b9c8481d..be007246a58e 100644 --- a/packages/decap-cms-widget-markdown/src/MarkdownControl/RawEditor.js +++ b/packages/decap-cms-widget-markdown/src/MarkdownControl/RawEditor.js @@ -57,7 +57,7 @@ function RawEditor(props) { } return ( - + Date: Fri, 17 Oct 2025 10:07:11 +0200 Subject: [PATCH 25/43] feat(widget-richtext): add raw markdown editor and sticky header for both + some styling --- .../src/RichtextControl.js | 63 ++++++++++- .../src/RichtextControl/RawEditor.js | 100 ++++++++++++++++++ .../src/RichtextControl/VisualEditor.js | 93 ++++++++-------- .../src/RichtextControl/components/Editor.js | 2 +- .../components/Toolbar/Toolbar.js | 54 +++++++++- .../src/RichtextControl/defaultEmptyBlock.js | 10 ++ .../decap-cms-widget-richtext/src/styles.js | 13 ++- 7 files changed, 283 insertions(+), 52 deletions(-) create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/RawEditor.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/defaultEmptyBlock.js diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl.js b/packages/decap-cms-widget-richtext/src/RichtextControl.js index 0737b1c85d93..750db9846123 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl.js @@ -1,9 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { List } from 'immutable'; import VisualEditor from './RichtextControl/VisualEditor'; import { EditorProvider } from './RichtextControl/editorContext'; +import RawEditor from './RichtextControl/RawEditor'; + +const MODE_STORAGE_KEY = 'cms.md-mode'; export default class RichtextControl extends React.Component { static propTypes = { @@ -18,6 +22,40 @@ export default class RichtextControl extends React.Component { t: PropTypes.func.isRequired, isDisabled: PropTypes.bool, }; + + + constructor(props) { + super(props); + const preferredMode = localStorage.getItem(MODE_STORAGE_KEY) ?? 'rich_text'; + + this.state = { + mode: + this.getAllowedModes().indexOf(preferredMode) !== -1 + ? preferredMode + : this.getAllowedModes()[0], + pendingFocus: false, + }; + } + + componentDidMount() { + // Manually validate PropTypes - React 19 breaking change + PropTypes.checkPropTypes(RichtextControl.propTypes, this.props, 'prop', 'RichtextControl'); + } + + handleMode = mode => { + this.setState({ mode, pendingFocus: true }); + localStorage.setItem(MODE_STORAGE_KEY, mode); + }; + + setFocusReceived = () => { + this.setState({ pendingFocus: false }); + }; + + getAllowedModes = () => this.props.field.get('modes', List(['rich_text', 'raw'])).toArray(); + + focus() { + this.setState({ pendingFocus: true }); + } render() { const { @@ -31,6 +69,8 @@ export default class RichtextControl extends React.Component { value, } = this.props; + const isShowModeToggle = this.getAllowedModes().length > 1; + const visualEditor = (
@@ -40,12 +80,33 @@ export default class RichtextControl extends React.Component { className={classNameWrapper} getEditorComponents={getEditorComponents} isDisabled={isDisabled} + onMode={this.handleMode} + isShowModeToggle={isShowModeToggle} onChange={onChange} value={value} />
); - return visualEditor; + + // const rawEditor =
raw editor
; + + const rawEditor = ( +
+ +
) + + return this.state.mode === 'rich_text' ? visualEditor : rawEditor; } } diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/RawEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/RawEditor.js new file mode 100644 index 000000000000..819cf1184cf4 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/RawEditor.js @@ -0,0 +1,100 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { css, ClassNames } from '@emotion/react'; +import { lengths, fonts } from 'decap-cms-ui-default'; +import { ReactEditor } from 'slate-react'; +import { ParagraphPlugin, Plate, usePlateEditor } from 'platejs/react'; +import { SingleBlockPlugin } from 'platejs'; + +import { editorContainerStyles, EditorControlBar } from '../styles'; +import defaultEmptyBlock from './defaultEmptyBlock'; +import Toolbar from './components/Toolbar'; +import Editor from './components/Editor'; +import ParagraphElement from './components/Element/ParagraphElement'; + +function editorStyles({ minimal }) { + return css` + position: relative; + overflow: hidden; + overflow-x: auto; + min-height: ${minimal ? 'auto' : lengths.richTextEditorMinHeight}; + font-family: ${fonts.mono}; + display: flex; + flex-direction: column; + `; +} + +function RawEditor(props) { + const { className, field, isShowModeToggle, t, onChange, value } = props; + + const initialValue = [defaultEmptyBlock(value || '')]; + + const editor = usePlateEditor({ + plugins: [SingleBlockPlugin], + override: { + components: { + [ParagraphPlugin.key]: ParagraphElement, + }, + }, + value: initialValue, + }); + + useEffect(() => { + if (props.pendingFocus) { + ReactEditor.focus(editor); + props.pendingFocus(); + } + }, [props.pendingFocus]); + + function handleToggleMode() { + props.onMode('rich_text'); + } + + function handleChange({ value }) { + onChange(value.map(line => line.children[0].text).join('\n')); + } + + return ( + + + {({ cx, css }) => ( +
+ + + +
+ +
+
+ )} +
+
+ ); +} + +RawEditor.propTypes = { + onChange: PropTypes.func.isRequired, + onMode: PropTypes.func.isRequired, + className: PropTypes.string.isRequired, + value: PropTypes.string, + field: ImmutablePropTypes.map.isRequired, + isShowModeToggle: PropTypes.bool.isRequired, + t: PropTypes.func.isRequired, +}; + +export default RawEditor; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index 7cf492b17fb3..f41e647c16a4 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -4,11 +4,11 @@ import { usePlateEditor, Plate, ParagraphPlugin, PlateLeaf } from 'platejs/react import { BoldPlugin, ItalicPlugin, CodePlugin, HeadingPlugin } from '@platejs/basic-nodes/react'; import { ListPlugin } from '@platejs/list-classic/react'; import { LinkPlugin } from '@platejs/link/react'; -import { ClassNames } from '@emotion/react'; +import { ClassNames, css } from '@emotion/react'; import { fonts, lengths, zIndex } from 'decap-cms-ui-default'; import { fromJS } from 'immutable'; -import { editorStyleVars } from '../styles'; +import { editorContainerStyles, EditorControlBar, editorStyleVars } from '../styles'; import { markdownToSlate, slateToMarkdown } from '../serializers'; import Editor from './components/Editor'; import Toolbar from './components/Toolbar'; @@ -21,21 +21,20 @@ import BlockquoteElement from './components/Element/BlockquoteElement'; import LinkElement from './components/Element/LinkElement'; import ExtendedBlockquotePlugin from './plugins/ExtendedBlockquotePlugin'; import ShortcodePlugin from './plugins/ShortcodePlugin'; -// import ShortcodeElement from './components/Element/ShortcodeElement'; - -function visualEditorStyles({ minimal }) { - return ` - position: relative; - overflow: auto; - font-family: ${fonts.primary}; - min-height: ${minimal ? 'auto' : lengths.richTextEditorMinHeight}; - margin-top: -${editorStyleVars.stickyDistanceBottom}; - padding: 0; - display: flex; - flex-direction: column; - z-index: ${zIndex.zIndex100}; - white-space: pre-wrap; -`; +import defaultEmptyBlock from './defaultEmptyBlock'; + +function editorStyles({ minimal }) { + return css` + position: relative; + font-family: ${fonts.primary}; + min-height: ${minimal ? 'auto' : lengths.richTextEditorMinHeight}; + margin-top: -${editorStyleVars.stickyDistanceBottom}; + padding: 0; + display: flex; + flex-direction: column; + z-index: ${zIndex.zIndex100}; + white-space: pre-wrap; + `; } function mergeMediaConfig(editorComponents, field) { @@ -70,16 +69,19 @@ function mergeMediaConfig(editorComponents, field) { } } -const emptyValue = [ - { - id: '1', - type: ParagraphPlugin.key, - children: [{ text: '' }], - }, -]; +const emptyValue = [defaultEmptyBlock()]; export default function VisualEditor(props) { - const { t, field, className, isDisabled, onChange, getEditorComponents } = props; + const { + t, + field, + className, + isDisabled, + onMode, + isShowModeToggle, + onChange, + getEditorComponents, + } = props; let editorComponents = getEditorComponents(); const codeBlockComponent = fromJS(editorComponents.find(({ type }) => type === 'code-block')); @@ -100,7 +102,7 @@ export default function VisualEditor(props) { } function handleToggleMode() { - console.log('handleToggleMode'); + onMode('raw'); } function handleChange({ value }) { @@ -156,30 +158,29 @@ export default function VisualEditor(props) { className={cx( className, css` - ${visualEditorStyles({ minimal: field.get('minimal') })} + ${editorContainerStyles} `, )} > - false} - getAsset={() => false} - hasInline={() => false} - hasBlock={() => false} - hasQuote={() => false} - hasListItems={() => false} - isShowModeToggle={() => false} - onChange={() => false} - t={t} - disabled={isDisabled} - /> - + + false} // investinagte + getAsset={() => false} // investigate + isShowModeToggle={isShowModeToggle} + t={t} + disabled={isDisabled} + /> + +
+ +
)} diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Editor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Editor.js index 2d4f912f55f5..039b954e7dad 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Editor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Editor.js @@ -11,7 +11,7 @@ function Editor(props) { (props.offPosition ? '62px' : '70px')}; + + ${props => + props.isActive && + css` + font-weight: 600; + color: ${colors.active}; + `}; +`; + function Toolbar(props) { - const { disabled, t, editorComponents, allowedEditorComponents } = props; + const { + disabled, + t, + rawMode, + onToggleMode, + isShowModeToggle, + editorComponents, + allowedEditorComponents, + } = props; function isVisible(button) { const { buttons } = props; @@ -34,7 +68,7 @@ function Toolbar(props) { return ( -
+
{isVisible('bold') && (
+ {isShowModeToggle && ( + + + {t('editor.editorWidgets.markdown.richText')} + + + + {t('editor.editorWidgets.markdown.markdown')} + + + )} ); } @@ -105,6 +150,9 @@ function Toolbar(props) { Toolbar.propTypes = { buttons: PropTypes.array, disabled: PropTypes.bool, + onToggleMode: PropTypes.func.isRequired, + rawMode: PropTypes.bool, + isShowModeToggle: PropTypes.bool, editorComponents: ImmutablePropTypes.map, allowedEditorComponents: ImmutablePropTypes.list, t: PropTypes.func.isRequired, diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/defaultEmptyBlock.js b/packages/decap-cms-widget-richtext/src/RichtextControl/defaultEmptyBlock.js new file mode 100644 index 000000000000..9bb4365c5a0e --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/defaultEmptyBlock.js @@ -0,0 +1,10 @@ +import { ParagraphPlugin } from "platejs/react"; + +function defaultEmptyBlock(text = '') { + return { + type: ParagraphPlugin.key, + children: [{ text }], + }; +} + +export default defaultEmptyBlock; \ No newline at end of file diff --git a/packages/decap-cms-widget-richtext/src/styles.js b/packages/decap-cms-widget-richtext/src/styles.js index 79698f537ecf..441d42783aa4 100644 --- a/packages/decap-cms-widget-richtext/src/styles.js +++ b/packages/decap-cms-widget-richtext/src/styles.js @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { zIndex } from 'decap-cms-ui-default'; +import { fonts, zIndex } from 'decap-cms-ui-default'; export const editorStyleVars = { stickyDistanceBottom: '0', @@ -11,3 +11,14 @@ export const EditorControlBar = styled.div` top: 0; margin-bottom: ${editorStyleVars.stickyDistanceBottom}; `; + +export const editorContainerStyles = ` + position: relative; + font-family: ${fonts.primary}; + margin-top: -${editorStyleVars.stickyDistanceBottom}; + padding: 0; + display: flex; + flex-direction: column; + z-index: ${zIndex.zIndex100}; + white-space: pre-wrap; +`; From d2cc76450e13002f2b44a5afc91892e2a8da9905 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Tue, 21 Oct 2025 13:21:26 +0200 Subject: [PATCH 26/43] feat(widget-richtext): fix pendingFocus --- .../decap-cms-widget-richtext/src/RichtextControl.js | 11 ++++++----- .../src/RichtextControl/RawEditor.js | 3 +-- .../src/RichtextControl/VisualEditor.js | 9 ++++++++- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl.js b/packages/decap-cms-widget-richtext/src/RichtextControl.js index 750db9846123..fbe07f9a5e80 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl.js @@ -22,8 +22,6 @@ export default class RichtextControl extends React.Component { t: PropTypes.func.isRequired, isDisabled: PropTypes.bool, }; - - constructor(props) { super(props); const preferredMode = localStorage.getItem(MODE_STORAGE_KEY) ?? 'rich_text'; @@ -70,6 +68,7 @@ export default class RichtextControl extends React.Component { } = this.props; const isShowModeToggle = this.getAllowedModes().length > 1; + const { mode, pendingFocus } = this.state; const visualEditor = ( @@ -83,6 +82,7 @@ export default class RichtextControl extends React.Component { onMode={this.handleMode} isShowModeToggle={isShowModeToggle} onChange={onChange} + pendingFocus={pendingFocus && this.setFocusReceived} value={value} />
@@ -102,11 +102,12 @@ export default class RichtextControl extends React.Component { className={classNameWrapper} value={value} field={field} - // pendingFocus={pendingFocus && this.setFocusReceived} + pendingFocus={pendingFocus && this.setFocusReceived} t={t} /> -
) +
+ ); - return this.state.mode === 'rich_text' ? visualEditor : rawEditor; + return mode === 'rich_text' ? visualEditor : rawEditor; } } diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/RawEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/RawEditor.js index 819cf1184cf4..336c75403cc4 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/RawEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/RawEditor.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { css, ClassNames } from '@emotion/react'; import { lengths, fonts } from 'decap-cms-ui-default'; -import { ReactEditor } from 'slate-react'; import { ParagraphPlugin, Plate, usePlateEditor } from 'platejs/react'; import { SingleBlockPlugin } from 'platejs'; @@ -42,7 +41,7 @@ function RawEditor(props) { useEffect(() => { if (props.pendingFocus) { - ReactEditor.focus(editor); + editor.tf.focus({ edge: 'endEditor' }); props.pendingFocus(); } }, [props.pendingFocus]); diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index f41e647c16a4..3425d99ef4f8 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { KEYS } from 'platejs'; import { usePlateEditor, Plate, ParagraphPlugin, PlateLeaf } from 'platejs/react'; import { BoldPlugin, ItalicPlugin, CodePlugin, HeadingPlugin } from '@platejs/basic-nodes/react'; @@ -151,6 +151,13 @@ export default function VisualEditor(props) { value: initialValue, }); + useEffect(() => { + if (props.pendingFocus) { + editor.tf.focus({ edge: 'endEditor' }); + props.pendingFocus(); + } + }, [props.pendingFocus]); + return ( {({ css, cx }) => ( From e05868a78f94653c7b79ecdc9a916d3949be8434 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Tue, 21 Oct 2025 13:35:46 +0200 Subject: [PATCH 27/43] feat(widget-richtext): remove prop drilled unused getAsset --- packages/decap-cms-widget-richtext/src/RichtextControl.js | 6 ------ .../src/RichtextControl/VisualEditor.js | 2 -- 2 files changed, 8 deletions(-) diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl.js b/packages/decap-cms-widget-richtext/src/RichtextControl.js index fbe07f9a5e80..dbb9866979ab 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl.js @@ -12,8 +12,6 @@ const MODE_STORAGE_KEY = 'cms.md-mode'; export default class RichtextControl extends React.Component { static propTypes = { onChange: PropTypes.func.isRequired, - onAddAsset: PropTypes.func.isRequired, - getAsset: PropTypes.func.isRequired, classNameWrapper: PropTypes.string.isRequired, editorControl: PropTypes.elementType.isRequired, value: PropTypes.string, @@ -89,16 +87,12 @@ export default class RichtextControl extends React.Component { ); - // const rawEditor =
raw editor
; - const rawEditor = (
false} // investinagte - getAsset={() => false} // investigate isShowModeToggle={isShowModeToggle} t={t} disabled={isDisabled} From 4d7bb88c7f34dbe0ba0c9ae2dd5005c6bf7c08d1 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Tue, 21 Oct 2025 14:27:25 +0200 Subject: [PATCH 28/43] feat(widget-richtext): add webpack config to fix build process --- packages/decap-cms-widget-richtext/webpack.config.js | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 packages/decap-cms-widget-richtext/webpack.config.js diff --git a/packages/decap-cms-widget-richtext/webpack.config.js b/packages/decap-cms-widget-richtext/webpack.config.js new file mode 100644 index 000000000000..42edd361d4a7 --- /dev/null +++ b/packages/decap-cms-widget-richtext/webpack.config.js @@ -0,0 +1,3 @@ +const { getConfig } = require('../../scripts/webpack.js'); + +module.exports = getConfig(); From 7f2fa4b111ad7510e86b6e8bbc991dde2b8dbef7 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Tue, 21 Oct 2025 14:28:02 +0200 Subject: [PATCH 29/43] feat(richtext-widget): lint --- .../src/RichtextControl/defaultEmptyBlock.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/defaultEmptyBlock.js b/packages/decap-cms-widget-richtext/src/RichtextControl/defaultEmptyBlock.js index 9bb4365c5a0e..385e10207644 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/defaultEmptyBlock.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/defaultEmptyBlock.js @@ -1,4 +1,4 @@ -import { ParagraphPlugin } from "platejs/react"; +import { ParagraphPlugin } from 'platejs/react'; function defaultEmptyBlock(text = '') { return { @@ -7,4 +7,4 @@ function defaultEmptyBlock(text = '') { }; } -export default defaultEmptyBlock; \ No newline at end of file +export default defaultEmptyBlock; From c3316a8e1aaa1a6d2d92649512721692dffff468 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Tue, 18 Nov 2025 11:37:23 +0100 Subject: [PATCH 30/43] feat(widget-richtext): add unit tests from the markdown and adapt mergeMediaConfig --- .../src/RichtextControl/VisualEditor.js | 33 +-- .../__tests__/VisualEditor.spec.js | 56 +++++ .../src/RichtextControl/mergeMediaConfig.js | 31 +++ .../src/__tests__/renderer.spec.js | 228 ++++++++++++++++++ .../src/serializers/index.js | 3 +- 5 files changed, 318 insertions(+), 33 deletions(-) create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/__tests__/VisualEditor.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/mergeMediaConfig.js create mode 100644 packages/decap-cms-widget-richtext/src/__tests__/renderer.spec.js diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index bc7a5e4f990e..0267a04a55ba 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -22,6 +22,7 @@ import LinkElement from './components/Element/LinkElement'; import ExtendedBlockquotePlugin from './plugins/ExtendedBlockquotePlugin'; import ShortcodePlugin from './plugins/ShortcodePlugin'; import defaultEmptyBlock from './defaultEmptyBlock'; +import { mergeMediaConfig } from './mergeMediaConfig'; function editorStyles({ minimal }) { return css` @@ -37,38 +38,6 @@ function editorStyles({ minimal }) { `; } -function mergeMediaConfig(editorComponents, field) { - // merge editor media library config to image components - if (editorComponents.has('image')) { - const imageComponent = editorComponents.get('image'); - const fields = imageComponent?.fields; - - if (fields) { - imageComponent.fields = fields.update( - fields.findIndex(f => f.get('widget') === 'image'), - f => { - // merge `media_library` config - if (field.has('media_library')) { - f = f.set( - 'media_library', - field.get('media_library').mergeDeep(f.get('media_library')), - ); - } - // merge 'media_folder' - if (field.has('media_folder') && !f.has('media_folder')) { - f = f.set('media_folder', field.get('media_folder')); - } - // merge 'public_folder' - if (field.has('public_folder') && !f.has('public_folder')) { - f = f.set('public_folder', field.get('public_folder')); - } - return f; - }, - ); - } - } -} - const emptyValue = [defaultEmptyBlock()]; export default function VisualEditor(props) { diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/__tests__/VisualEditor.spec.js b/packages/decap-cms-widget-richtext/src/RichtextControl/__tests__/VisualEditor.spec.js new file mode 100644 index 000000000000..5f87329347aa --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/__tests__/VisualEditor.spec.js @@ -0,0 +1,56 @@ +import { Map, fromJS } from 'immutable'; + +import { mergeMediaConfig } from '../mergeMediaConfig'; + +describe('VisualEditor', () => { + describe('mergeMediaConfig', () => { + it('should copy editor media settings to image component', () => { + const editorComponents = Map({ + image: { + id: 'image', + label: 'Image', + type: 'shortcode', + icon: 'exclamation-triangle', + widget: 'object', + pattern: {}, + fields: fromJS([ + { + label: 'Image', + name: 'image', + widget: 'image', + media_library: { allow_multiple: false }, + }, + { label: 'Alt Text', name: 'alt' }, + { label: 'Title', name: 'title' }, + ]), + }, + }); + + const field = fromJS({ + label: 'Body', + name: 'body', + widget: 'markdown', + media_folder: '/{{media_folder}}/posts/images/widget/body', + public_folder: '{{public_folder}}/posts/images/widget/body', + media_library: { config: { max_file_size: 1234 } }, + }); + + mergeMediaConfig(editorComponents, field); + + expect(editorComponents.get('image').fields).toEqual( + fromJS([ + { + label: 'Image', + name: 'image', + widget: 'image', + media_library: { allow_multiple: false, config: { max_file_size: 1234 } }, + media_folder: '/{{media_folder}}/posts/images/widget/body', + public_folder: '{{public_folder}}/posts/images/widget/body', + }, + { label: 'Alt Text', name: 'alt' }, + { label: 'Title', name: 'title' }, + ]), + ); + }); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/mergeMediaConfig.js b/packages/decap-cms-widget-richtext/src/RichtextControl/mergeMediaConfig.js new file mode 100644 index 000000000000..19e8a81fbc56 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/mergeMediaConfig.js @@ -0,0 +1,31 @@ +export function mergeMediaConfig(editorComponents, field) { + // merge editor media library config to image components + if (editorComponents.has('image')) { + const imageComponent = editorComponents.get('image'); + const fields = imageComponent?.fields; + + if (fields) { + imageComponent.fields = fields.update( + fields.findIndex(f => f.get('widget') === 'image'), + f => { + // merge `media_library` config + if (field.has('media_library')) { + f = f.set( + 'media_library', + field.get('media_library').mergeDeep(f.get('media_library')), + ); + } + // merge 'media_folder' + if (field.has('media_folder') && !f.has('media_folder')) { + f = f.set('media_folder', field.get('media_folder')); + } + // merge 'public_folder' + if (field.has('public_folder') && !f.has('public_folder')) { + f = f.set('public_folder', field.get('public_folder')); + } + return f; + }, + ); + } + } +} diff --git a/packages/decap-cms-widget-richtext/src/__tests__/renderer.spec.js b/packages/decap-cms-widget-richtext/src/__tests__/renderer.spec.js new file mode 100644 index 000000000000..074264c60ea6 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/__tests__/renderer.spec.js @@ -0,0 +1,228 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import padStart from 'lodash/padStart'; +import { Map } from 'immutable'; + +import RichtextPreview from '../RichtextPreview'; +import { markdownToHtml } from '../serializers'; + +describe('RichtextPreview Preview renderer', () => { + describe('RichtextPreview rendering', () => { + describe('General', () => { + it('should render markdown', async () => { + const value = ` +# H1 + +Text with **bold** & _em_ elements + +## H2 + +* ul item 1 +* ul item 2 + +### H3 + +1. ol item 1 +1. ol item 2 +1. ol item 3 + +#### H4 + +[link title](http://google.com) + +##### H5 + +![alt text](https://pbs.twimg.com/profile_images/678903331176214528/TQTdqGwD.jpg) + +###### H6 + +![](https://pbs.twimg.com/profile_images/678903331176214528/TQTdqGwD.jpg) +`; + const html = await markdownToHtml(value); + + const { container } = render( + , + ); + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('H1'); + expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent('H2'); + expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent('H3'); + expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('H4'); + expect(screen.getByRole('heading', { level: 5 })).toHaveTextContent('H5'); + expect(screen.getByRole('heading', { level: 6 })).toHaveTextContent('H6'); + expect(container).toHaveTextContent('Text with bold & em elements'); + expect(screen.getByRole('link', { name: 'link title' })).toHaveAttribute( + 'href', + 'http://google.com', + ); + expect(screen.getAllByRole('img').length).toBe(2); + }); + }); + + describe('Headings', () => { + for (const heading of [...Array(6).keys()]) { + it(`should render Heading ${heading + 1}`, async () => { + const value = padStart(' Title', heading + 7, '#'); + const html = await markdownToHtml(value); + + render(); + expect(screen.getByRole('heading', { level: heading + 1 })).toHaveTextContent('Title'); + }); + } + }); + + describe('Lists', () => { + it('should render lists', async () => { + const value = ` +1. ol item 1 +1. ol item 2 + * Sublist 1 + * Sublist 2 + * Sublist 3 + 1. Sub-Sublist 1 + 1. Sub-Sublist 2 + 1. Sub-Sublist 3 +1. ol item 3 +`; + const html = await markdownToHtml(value); + + const { container } = render( + , + ); + // Check for ordered and unordered lists + expect(container.querySelectorAll('ol').length).toBeGreaterThan(0); + expect(container.querySelectorAll('ul').length).toBeGreaterThan(0); + expect(screen.getByText('ol item 1')).toBeInTheDocument(); + expect(screen.getByText('Sublist 1')).toBeInTheDocument(); + expect(screen.getByText('Sub-Sublist 1')).toBeInTheDocument(); + }); + }); + + describe('Links', () => { + it('should render links', async () => { + const value = ` +I get 10 times more traffic from [Google] than from [Yahoo] or [MSN]. + + [Google]: http://google.com/ "Google" + [Yahoo]: http://search.yahoo.com/ "Yahoo Search" + [MSN]: http://search.msn.com/ "MSN Search" +`; + const html = await markdownToHtml(value); + + render(); + expect(screen.getByRole('link', { name: 'Google' })).toHaveAttribute( + 'href', + 'http://google.com/', + ); + expect(screen.getByRole('link', { name: 'Yahoo' })).toHaveAttribute( + 'href', + 'http://search.yahoo.com/', + ); + expect(screen.getByRole('link', { name: 'MSN' })).toHaveAttribute( + 'href', + 'http://search.msn.com/', + ); + }); + }); + + describe('Code', () => { + it('should render code', async () => { + const value = 'Use the `printf()` function.'; + const html = await markdownToHtml(value); + + const { container } = render( + , + ); + expect(container.querySelector('code')).toHaveTextContent('printf()'); + }); + + it('should render code 2', async () => { + const value = '``There is a literal backtick (`) here.``'; + const html = await markdownToHtml(value); + + const { container } = render( + , + ); + expect(container.querySelector('code')).toHaveTextContent( + 'There is a literal backtick (`) here.', + ); + }); + }); + + describe('HTML', () => { + it('should render HTML as is when using Markdown', async () => { + const value = ` +# Title + +
+ +
+
Test HTML content
+
Testing HTML in Markdown
+
+
+ +

Test

+`; + const html = await markdownToHtml(value); + + const { container } = render( + , + ); + expect(container.querySelector('form')).toBeInTheDocument(); + expect(container.querySelector('dl')).toBeInTheDocument(); + expect(container.querySelector('h1[style]')).toHaveTextContent('Test'); + }); + }); + }); + + describe('HTML rendering', () => { + it('should render HTML', async () => { + const value = '

Paragraph with inline element

'; + const html = await markdownToHtml(value); + + const { container } = render( + , + ); + expect(container.querySelector('p')).toHaveTextContent('Paragraph with inline element'); + expect(container.querySelector('em')).toHaveTextContent('inline'); + }); + }); + + describe('HTML sanitization', () => { + it('should sanitize HTML', async () => { + const value = ``; + const field = Map({ sanitize_preview: true }); + + const { container } = render( + , + ); + const img = container.querySelector('img'); + expect(img).toHaveAttribute('src', 'foobar.png'); + expect(img).not.toHaveAttribute('onerror'); + }); + + it('should not sanitize HTML', async () => { + const value = ``; + const field = Map({ sanitize_preview: false }); + + const { container } = render( + , + ); + const img = container.querySelector('img'); + expect(img).toHaveAttribute('src', 'foobar.png'); + expect(img).toHaveAttribute('onerror', "alert('hello')"); + }); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/index.js b/packages/decap-cms-widget-richtext/src/serializers/index.js index a329b4f8ac82..7736dc4bc2cc 100644 --- a/packages/decap-cms-widget-richtext/src/serializers/index.js +++ b/packages/decap-cms-widget-richtext/src/serializers/index.js @@ -7,6 +7,7 @@ import remarkToRehype from 'remark-rehype'; import rehypeToHtml from 'rehype-stringify'; import htmlToRehype from 'rehype-parse'; import rehypeToRemark from 'rehype-remark'; +import { Map } from 'immutable'; import remarkToRehypeShortcodes from './remarkRehypeShortcodes'; import rehypePaperEmoji from './rehypePaperEmoji'; @@ -156,7 +157,7 @@ export function remarkToMarkdown(obj, remarkPlugins, editorComponents) { */ export function markdownToHtml( markdown, - { getAsset, resolveWidget, remarkPlugins = [], editorComponents } = {}, + { getAsset, resolveWidget, remarkPlugins = [], editorComponents = Map() } = {}, ) { const mdast = markdownToRemark(markdown, remarkPlugins, editorComponents); From b4e7ab949f6bb9cb4eb4252a0a99d11228efb843 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Tue, 9 Dec 2025 11:27:25 +0100 Subject: [PATCH 31/43] fix(widget-richtext): adapt link serializers to new slate format --- .../src/serializers/remarkSlate.js | 2 +- .../src/serializers/slateRemark.js | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/decap-cms-widget-richtext/src/serializers/remarkSlate.js b/packages/decap-cms-widget-richtext/src/serializers/remarkSlate.js index fcc05c5c2f79..22b4c9db6ec8 100644 --- a/packages/decap-cms-widget-richtext/src/serializers/remarkSlate.js +++ b/packages/decap-cms-widget-richtext/src/serializers/remarkSlate.js @@ -13,7 +13,7 @@ const typeMap = { tableRow: 'table-row', tableCell: 'table-cell', thematicBreak: 'thematic-break', - link: 'link', + link: 'a', image: 'image', shortcode: 'shortcode', }; diff --git a/packages/decap-cms-widget-richtext/src/serializers/slateRemark.js b/packages/decap-cms-widget-richtext/src/serializers/slateRemark.js index 7b408f2c6df1..93e023558814 100644 --- a/packages/decap-cms-widget-richtext/src/serializers/slateRemark.js +++ b/packages/decap-cms-widget-richtext/src/serializers/slateRemark.js @@ -25,7 +25,7 @@ const typeMap = { 'table-cell': 'tableCell', break: 'break', 'thematic-break': 'thematicBreak', - link: 'link', + a: 'link', image: 'image', shortcode: 'shortcode', }; @@ -59,7 +59,7 @@ const blockTypes = [ 'table-cell', ]; -const inlineTypes = ['link', 'image', 'break']; +const inlineTypes = ['a', 'image', 'break']; const leadingWhitespaceExp = /^\s+\S/; const trailingWhitespaceExp = /(?!\S)\s+$/; @@ -102,7 +102,7 @@ export default function slateToRemark(value, { voidCodeBlock }) { return nodes.map(node => { const newNode = { ...node }; switch (node.type) { - case 'link': { + case 'a': { const updatedNodes = removeMarkFromNodes(node.children, markType); return { ...node, @@ -132,7 +132,7 @@ export default function slateToRemark(value, { voidCodeBlock }) { function getNodeMarks(node) { switch (node.type) { - case 'link': { + case 'a': { // Code marks can't always be condensed together. If all text in a link // is wrapped in a mark, this function returns that mark and the node // ends up nested inside of that mark. Code marks sometimes can't do @@ -447,9 +447,9 @@ export default function slateToRemark(value, { voidCodeBlock }) { * * Url is now stored in data for slate, so we need to pull it out. */ - case 'link': { - const { title, data } = node; - return u(typeMap[node.type], { url: data?.url, title, ...data }, children); + case 'a': { + const { title, url, data } = node; + return u(typeMap[node.type], { url, title, ...data }, children); } /** From 71c2d92e52aa60209319d9e90e50cef88d055b0b Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Tue, 9 Dec 2025 14:38:05 +0100 Subject: [PATCH 32/43] test(widget-richtext): add unit tests from markdown widget and adapt them accordingly --- .../__snapshots__/parser.spec.js.snap | 543 ++++++++++++++ .../RichtextControl/__tests__/parser.spec.js | 668 ++++++++++++++++++ .../__fixtures__/commonmarkExpected.json | 625 ++++++++++++++++ .../duplicate_marks_github_issue_3280.md | 1 + .../remarkShortcodes.spec.js.snap | 132 ++++ .../serializers/__tests__/commonmark.spec.js | 110 +++ .../src/serializers/__tests__/index.spec.js | 52 ++ .../__tests__/remarkAllowHtmlEntities.spec.js | 25 + .../__tests__/remarkAssertParents.spec.js | 171 +++++ .../remarkEscapeMarkdownEntities.spec.js | 84 +++ .../__tests__/remarkPaddedLinks.spec.js | 43 ++ .../__tests__/remarkPlugins.spec.js | 299 ++++++++ .../__tests__/remarkShortcodes.spec.js | 145 ++++ .../serializers/__tests__/remarkSlate.spec.js | 67 ++ .../remarkStripTrailingBreaks.spec.js | 23 + .../src/serializers/__tests__/slate.spec.js | 300 ++++++++ .../src/serializers/index.js | 4 +- 17 files changed, 3290 insertions(+), 2 deletions(-) create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/__tests__/__snapshots__/parser.spec.js.snap create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/__tests__/parser.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/commonmarkExpected.json create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/duplicate_marks_github_issue_3280.md create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/__snapshots__/remarkShortcodes.spec.js.snap create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/commonmark.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/index.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAllowHtmlEntities.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAssertParents.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkEscapeMarkdownEntities.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPaddedLinks.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPlugins.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkShortcodes.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkSlate.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkStripTrailingBreaks.spec.js create mode 100644 packages/decap-cms-widget-richtext/src/serializers/__tests__/slate.spec.js diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/__tests__/__snapshots__/parser.spec.js.snap b/packages/decap-cms-widget-richtext/src/RichtextControl/__tests__/__snapshots__/parser.spec.js.snap new file mode 100644 index 000000000000..906062800218 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/__tests__/__snapshots__/parser.spec.js.snap @@ -0,0 +1,543 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Compile markdown to Slate Raw AST should compile kitchen sink example 1`] = ` +Array [ + Object { + "children": Array [ + Object { + "text": "An exhibit of Markdown", + }, + ], + "type": "h1", + }, + Object { + "children": Array [ + Object { + "text": "This note demonstrates some of what Markdown is capable of doing.", + }, + ], + "type": "p", + }, + Object { + "children": Array [ + Object { + "italic": true, + "marks": Array [ + Object { + "type": "italic", + }, + ], + "text": "Note: Feel free to play with this page. Unlike regular notes, this doesn't +automatically save itself.", + }, + ], + "type": "p", + }, + Object { + "children": Array [ + Object { + "text": "Basic formatting", + }, + ], + "type": "h2", + }, + Object { + "children": Array [ + Object { + "text": "Paragraphs can be written like so. A paragraph is the basic block of Markdown. +A paragraph is what text will turn into when there is no reason it should +become anything else.", + }, + ], + "type": "p", + }, + Object { + "children": Array [ + Object { + "text": "Paragraphs must be separated by a blank line. Basic formatting of ", + }, + Object { + "italic": true, + "marks": Array [ + Object { + "type": "italic", + }, + ], + "text": "italics", + }, + Object { + "text": " and +", + }, + Object { + "bold": true, + "marks": Array [ + Object { + "type": "bold", + }, + ], + "text": "bold", + }, + Object { + "text": " is supported. This ", + }, + Object { + "italic": true, + "marks": Array [ + Object { + "type": "italic", + }, + ], + "text": "can be ", + }, + Object { + "bold": true, + "italic": true, + "marks": Array [ + Object { + "type": "italic", + }, + Object { + "type": "bold", + }, + ], + "text": "nested", + }, + Object { + "italic": true, + "marks": Array [ + Object { + "type": "italic", + }, + ], + "text": " like", + }, + Object { + "text": " so.", + }, + ], + "type": "p", + }, + Object { + "children": Array [ + Object { + "text": "Lists", + }, + ], + "type": "h2", + }, + Object { + "children": Array [ + Object { + "text": "Ordered list", + }, + ], + "type": "h3", + }, + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "text": "Item 1 2. A second item 3. Number 3 4. Ⅳ", + }, + ], + "type": "p", + }, + ], + "type": "li", + }, + ], + "data": Object { + "start": 1, + }, + "type": "ol", + }, + Object { + "children": Array [ + Object { + "italic": true, + "marks": Array [ + Object { + "type": "italic", + }, + ], + "text": "Note: the fourth item uses the Unicode character for Roman numeral four.", + }, + ], + "type": "p", + }, + Object { + "children": Array [ + Object { + "text": "Unordered list", + }, + ], + "type": "h3", + }, + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "text": "An item Another item Yet another item And there's more...", + }, + ], + "type": "p", + }, + ], + "type": "li", + }, + ], + "data": Object { + "start": null, + }, + "type": "ul", + }, + Object { + "children": Array [ + Object { + "text": "Paragraph modifiers", + }, + ], + "type": "h2", + }, + Object { + "children": Array [ + Object { + "text": "Code block", + }, + ], + "type": "h3", + }, + Object { + "children": Array [ + Object { + "text": "Code blocks are very useful for developers and other people who look at +code or other things that are written in plain text. As you can see, it +uses a fixed-width font.", + }, + ], + "data": Object { + "lang": null, + "shortcode": "code-block", + "shortcodeData": Object { + "code": "Code blocks are very useful for developers and other people who look at +code or other things that are written in plain text. As you can see, it +uses a fixed-width font.", + "lang": null, + }, + }, + "type": "shortcode", + }, + Object { + "children": Array [ + Object { + "text": "You can also make ", + }, + Object { + "code": true, + "marks": Array [ + Object { + "type": "code", + }, + ], + "text": "inline code", + }, + Object { + "text": " to add code into other things.", + }, + ], + "type": "p", + }, + Object { + "children": Array [ + Object { + "text": "Quote", + }, + ], + "type": "h3", + }, + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "text": "Here is a quote. What this is should be self explanatory. Quotes are +automatically indented when they are used.", + }, + ], + "type": "p", + }, + ], + "type": "quote", + }, + Object { + "children": Array [ + Object { + "text": "Headings", + }, + ], + "type": "h2", + }, + Object { + "children": Array [ + Object { + "text": "There are six levels of headings. They correspond with the six levels of HTML +headings. You've probably noticed them already in the page. Each level down +uses one more hash character.", + }, + ], + "type": "p", + }, + Object { + "children": Array [ + Object { + "text": "Headings ", + }, + Object { + "italic": true, + "marks": Array [ + Object { + "type": "italic", + }, + ], + "text": "can", + }, + Object { + "text": " also contain ", + }, + Object { + "bold": true, + "marks": Array [ + Object { + "type": "bold", + }, + ], + "text": "formatting", + }, + ], + "type": "h3", + }, + Object { + "children": Array [ + Object { + "text": "They can even contain ", + }, + Object { + "code": true, + "marks": Array [ + Object { + "type": "code", + }, + ], + "text": "inline code", + }, + ], + "type": "h3", + }, + Object { + "children": Array [ + Object { + "text": "Of course, demonstrating what headings look like messes up the structure of the +page.", + }, + ], + "type": "p", + }, + Object { + "children": Array [ + Object { + "text": "I don't recommend using more than three or four levels of headings here, +because, when you're smallest heading isn't too small, and you're largest +heading isn't too big, and you want each size up to look noticeably larger and +more important, there there are only so many sizes that you can use.", + }, + ], + "type": "p", + }, + Object { + "children": Array [ + Object { + "text": "URLs", + }, + ], + "type": "h2", + }, + Object { + "children": Array [ + Object { + "text": "URLs can be made in a handful of ways:", + }, + ], + "type": "p", + }, + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "text": "A named link to MarkItDown. The easiest way to do these is to select what you", + }, + ], + "type": "p", + }, + ], + "type": "li", + }, + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "text": "want to make a link and hit ", + }, + Object { + "code": true, + "marks": Array [ + Object { + "type": "code", + }, + ], + "text": "Ctrl+L", + }, + Object { + "text": ". Another named link to", + }, + ], + "type": "p", + }, + ], + "type": "li", + }, + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "text": "MarkItDown", + }, + ], + "data": Object { + "title": null, + "url": "http://www.markitdown.net/", + }, + "type": "a", + }, + Object { + "text": " Sometimes you just want a URL like", + }, + ], + "type": "p", + }, + ], + "type": "li", + }, + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "text": "http://www.markitdown.net/", + }, + ], + "data": Object { + "title": null, + "url": "http://www.markitdown.net/", + }, + "type": "a", + }, + Object { + "text": ".", + }, + ], + "type": "p", + }, + ], + "type": "li", + }, + ], + "data": Object { + "start": null, + }, + "type": "ul", + }, + Object { + "children": Array [ + Object { + "text": "Horizontal rule", + }, + ], + "type": "h2", + }, + Object { + "children": Array [ + Object { + "text": "A horizontal rule is a line that goes across the middle of the page.", + }, + ], + "type": "p", + }, + Object { + "children": Array [ + Object { + "text": "", + }, + ], + "type": "thematic-break", + }, + Object { + "children": Array [ + Object { + "text": "It's sometimes handy for breaking things up.", + }, + ], + "type": "p", + }, + Object { + "children": Array [ + Object { + "text": "Images", + }, + ], + "type": "h2", + }, + Object { + "children": Array [ + Object { + "text": "Markdown can also contain images. I'll need to add something here sometime.", + }, + ], + "type": "p", + }, + Object { + "children": Array [ + Object { + "text": "Finally", + }, + ], + "type": "h2", + }, + Object { + "children": Array [ + Object { + "text": "There's actually a lot more to Markdown than this. See the official +introduction and syntax for more information. However, be aware that this is +not using the official implementation, and this might work subtly differently + in some of the little things.", + }, + ], + "type": "p", + }, +] +`; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/__tests__/parser.spec.js b/packages/decap-cms-widget-richtext/src/RichtextControl/__tests__/parser.spec.js new file mode 100644 index 000000000000..ccb87e3648ea --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/__tests__/parser.spec.js @@ -0,0 +1,668 @@ +import { markdownToSlate } from '../../serializers'; + +const parser = markdownToSlate; + +describe('Compile markdown to Slate Raw AST', () => { + it('should compile simple markdown', () => { + const value = ` +# H1 + +sweet body +`; + expect(parser(value)).toMatchInlineSnapshot(` +Array [ + Object { + "children": Array [ + Object { + "text": "H1", + }, + ], + "type": "h1", + }, + Object { + "children": Array [ + Object { + "text": "sweet body", + }, + ], + "type": "p", + }, +] +`); + }); + + it('should compile a markdown ordered list', () => { + const value = ` +# H1 + +1. yo +2. bro +3. fro +`; + expect(parser(value)).toMatchInlineSnapshot(` +Array [ + Object { + "children": Array [ + Object { + "text": "H1", + }, + ], + "type": "h1", + }, + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "text": "yo", + }, + ], + "type": "p", + }, + ], + "type": "li", + }, + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "text": "bro", + }, + ], + "type": "p", + }, + ], + "type": "li", + }, + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "text": "fro", + }, + ], + "type": "p", + }, + ], + "type": "li", + }, + ], + "data": Object { + "start": 1, + }, + "type": "ol", + }, +] +`); + }); + + it('should compile bulleted lists', () => { + const value = ` +# H1 + +* yo +* bro +* fro +`; + expect(parser(value)).toMatchInlineSnapshot(` +Array [ + Object { + "children": Array [ + Object { + "text": "H1", + }, + ], + "type": "h1", + }, + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "text": "yo", + }, + ], + "type": "p", + }, + ], + "type": "li", + }, + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "text": "bro", + }, + ], + "type": "p", + }, + ], + "type": "li", + }, + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "text": "fro", + }, + ], + "type": "p", + }, + ], + "type": "li", + }, + ], + "data": Object { + "start": null, + }, + "type": "ul", + }, +] +`); + }); + + it('should compile multiple header levels', () => { + const value = ` +# H1 + +## H2 + +### H3 +`; + expect(parser(value)).toMatchInlineSnapshot(` +Array [ + Object { + "children": Array [ + Object { + "text": "H1", + }, + ], + "type": "h1", + }, + Object { + "children": Array [ + Object { + "text": "H2", + }, + ], + "type": "h2", + }, + Object { + "children": Array [ + Object { + "text": "H3", + }, + ], + "type": "h3", + }, +] +`); + }); + + it('should compile horizontal rules', () => { + const value = ` +# H1 + +--- + +blue moon +`; + expect(parser(value)).toMatchInlineSnapshot(` +Array [ + Object { + "children": Array [ + Object { + "text": "H1", + }, + ], + "type": "h1", + }, + Object { + "children": Array [ + Object { + "text": "", + }, + ], + "type": "thematic-break", + }, + Object { + "children": Array [ + Object { + "text": "blue moon", + }, + ], + "type": "p", + }, +] +`); + }); + + it('should compile horizontal rules', () => { + const value = ` +# H1 + +--- + +blue moon +`; + expect(parser(value)).toMatchInlineSnapshot(` +Array [ + Object { + "children": Array [ + Object { + "text": "H1", + }, + ], + "type": "h1", + }, + Object { + "children": Array [ + Object { + "text": "", + }, + ], + "type": "thematic-break", + }, + Object { + "children": Array [ + Object { + "text": "blue moon", + }, + ], + "type": "p", + }, +] +`); + }); + + it('should compile soft breaks (double space)', () => { + const value = ` +blue moon +footballs +`; + expect(parser(value)).toMatchInlineSnapshot(` +Array [ + Object { + "children": Array [ + Object { + "text": "blue moon", + }, + Object { + "children": Array [ + Object { + "text": "", + }, + ], + "data": undefined, + "type": "break", + }, + Object { + "text": "footballs", + }, + ], + "type": "p", + }, +] +`); + }); + + it('should compile images', () => { + const value = ` +![super](duper.jpg) +`; + expect(parser(value)).toMatchInlineSnapshot(` +Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "text": "", + }, + ], + "data": Object { + "alt": "super", + "title": null, + "url": "duper.jpg", + }, + "type": "image", + }, + ], + "type": "p", + }, +] +`); + }); + + it('should compile code blocks', () => { + const value = ` +\`\`\`javascript +var a = 1; +\`\`\` +`; + expect(parser(value)).toMatchInlineSnapshot(` +Array [ + Object { + "children": Array [ + Object { + "text": "var a = 1;", + }, + ], + "data": Object { + "lang": "javascript", + "shortcode": "code-block", + "shortcodeData": Object { + "code": "var a = 1;", + "lang": "javascript", + }, + }, + "type": "shortcode", + }, +] +`); + }); + + it('should compile nested inline markup', () => { + const value = ` +# Word + +This is **some *hot* content** + +perhaps **scalding** even +`; + expect(parser(value)).toMatchInlineSnapshot(` +Array [ + Object { + "children": Array [ + Object { + "text": "Word", + }, + ], + "type": "h1", + }, + Object { + "children": Array [ + Object { + "text": "This is ", + }, + Object { + "bold": true, + "marks": Array [ + Object { + "type": "bold", + }, + ], + "text": "some ", + }, + Object { + "bold": true, + "italic": true, + "marks": Array [ + Object { + "type": "bold", + }, + Object { + "type": "italic", + }, + ], + "text": "hot", + }, + Object { + "bold": true, + "marks": Array [ + Object { + "type": "bold", + }, + ], + "text": " content", + }, + ], + "type": "p", + }, + Object { + "children": Array [ + Object { + "text": "perhaps ", + }, + Object { + "bold": true, + "marks": Array [ + Object { + "type": "bold", + }, + ], + "text": "scalding", + }, + Object { + "text": " even", + }, + ], + "type": "p", + }, +] +`); + }); + + it('should compile inline code', () => { + const value = ` +# Word + +This is some sweet \`inline code\` yo! +`; + expect(parser(value)).toMatchInlineSnapshot(` +Array [ + Object { + "children": Array [ + Object { + "text": "Word", + }, + ], + "type": "h1", + }, + Object { + "children": Array [ + Object { + "text": "This is some sweet ", + }, + Object { + "code": true, + "marks": Array [ + Object { + "type": "code", + }, + ], + "text": "inline code", + }, + Object { + "text": " yo!", + }, + ], + "type": "p", + }, +] +`); + }); + + it('should compile links', () => { + const value = ` +# Word + +How far is it to [Google](https://google.com) land? +`; + expect(parser(value)).toMatchInlineSnapshot(` +Array [ + Object { + "children": Array [ + Object { + "text": "Word", + }, + ], + "type": "h1", + }, + Object { + "children": Array [ + Object { + "text": "How far is it to ", + }, + Object { + "children": Array [ + Object { + "text": "Google", + }, + ], + "data": Object { + "title": null, + "url": "https://google.com", + }, + "type": "a", + }, + Object { + "text": " land?", + }, + ], + "type": "p", + }, +] +`); + }); + + it('should compile plugins', () => { + const value = ` +![test](test.png) + +{{< test >}} +`; + expect(parser(value)).toMatchInlineSnapshot(` +Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "text": "", + }, + ], + "data": Object { + "alt": "test", + "title": null, + "url": "test.png", + }, + "type": "image", + }, + ], + "type": "p", + }, + Object { + "children": Array [ + Object { + "text": "{{< test >}}", + }, + ], + "type": "p", + }, +] +`); + }); + + it('should compile kitchen sink example', () => { + const value = ` +# An exhibit of Markdown + +This note demonstrates some of what Markdown is capable of doing. + +*Note: Feel free to play with this page. Unlike regular notes, this doesn't +automatically save itself.* + +## Basic formatting + +Paragraphs can be written like so. A paragraph is the basic block of Markdown. +A paragraph is what text will turn into when there is no reason it should +become anything else. + +Paragraphs must be separated by a blank line. Basic formatting of *italics* and +**bold** is supported. This *can be **nested** like* so. + +## Lists + +### Ordered list + +1. Item 1 2. A second item 3. Number 3 4. Ⅳ + +*Note: the fourth item uses the Unicode character for Roman numeral four.* + +### Unordered list + +* An item Another item Yet another item And there's more... + +## Paragraph modifiers + +### Code block + + Code blocks are very useful for developers and other people who look at + code or other things that are written in plain text. As you can see, it + uses a fixed-width font. + +You can also make \`inline code\` to add code into other things. + +### Quote + +> Here is a quote. What this is should be self explanatory. Quotes are +automatically indented when they are used. + +## Headings + +There are six levels of headings. They correspond with the six levels of HTML +headings. You've probably noticed them already in the page. Each level down +uses one more hash character. + +### Headings *can* also contain **formatting** + +### They can even contain \`inline code\` + +Of course, demonstrating what headings look like messes up the structure of the +page. + +I don't recommend using more than three or four levels of headings here, +because, when you're smallest heading isn't too small, and you're largest +heading isn't too big, and you want each size up to look noticeably larger and +more important, there there are only so many sizes that you can use. + +## URLs + +URLs can be made in a handful of ways: + +* A named link to MarkItDown. The easiest way to do these is to select what you +* want to make a link and hit \`Ctrl+L\`. Another named link to +* [MarkItDown](http://www.markitdown.net/) Sometimes you just want a URL like +* . + +## Horizontal rule + +A horizontal rule is a line that goes across the middle of the page. + +--- + +It's sometimes handy for breaking things up. + +## Images + +Markdown can also contain images. I'll need to add something here sometime. + +## Finally + +There's actually a lot more to Markdown than this. See the official +introduction and syntax for more information. However, be aware that this is +not using the official implementation, and this might work subtly differently + in some of the little things. +`; + expect(parser(value)).toMatchSnapshot(); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/commonmarkExpected.json b/packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/commonmarkExpected.json new file mode 100644 index 000000000000..2e74df1471fe --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/commonmarkExpected.json @@ -0,0 +1,625 @@ +{ + "\tfoo\tbaz\t\tbim\n": "NOT_TO_EQUAL", + " \tfoo\tbaz\t\tbim\n": "NOT_TO_EQUAL", + " a\ta\n ὐ\ta\n": "NOT_TO_EQUAL", + " - foo\n\n\tbar\n": "NOT_TO_EQUAL", + "- foo\n\n\t\tbar\n": "NOT_TO_EQUAL", + ">\t\tfoo\n": "NOT_TO_EQUAL", + "-\t\tfoo\n": "NOT_TO_EQUAL", + " foo\n\tbar\n": "TO_EQUAL", + " - foo\n - bar\n\t - baz\n": "NOT_TO_EQUAL", + "#\tFoo\n": "TO_EQUAL", + "*\t*\t*\t\n": "TO_ERROR", + "- `one\n- two`\n": "TO_EQUAL", + "***\n---\n___\n": "NOT_TO_EQUAL", + "+++\n": "TO_EQUAL", + "===\n": "TO_EQUAL", + "--\n**\n__\n": "TO_EQUAL", + " ***\n ***\n ***\n": "NOT_TO_EQUAL", + " ***\n": "TO_EQUAL", + "Foo\n ***\n": "TO_EQUAL", + "_____________________________________\n": "NOT_TO_EQUAL", + " - - -\n": "NOT_TO_EQUAL", + " ** * ** * ** * **\n": "NOT_TO_EQUAL", + "- - - -\n": "NOT_TO_EQUAL", + "- - - - \n": "NOT_TO_EQUAL", + "_ _ _ _ a\n\na------\n\n---a---\n": "TO_EQUAL", + " *-*\n": "NOT_TO_EQUAL", + "- foo\n***\n- bar\n": "NOT_TO_EQUAL", + "Foo\n***\nbar\n": "NOT_TO_EQUAL", + "Foo\n---\nbar\n": "TO_EQUAL", + "* Foo\n* * *\n* Bar\n": "NOT_TO_EQUAL", + "- Foo\n- * * *\n": "NOT_TO_EQUAL", + "# foo\n## foo\n### foo\n#### foo\n##### foo\n###### foo\n": "TO_EQUAL", + "####### foo\n": "TO_EQUAL", + "#5 bolt\n\n#hashtag\n": "TO_EQUAL", + "\\## foo\n": "TO_EQUAL", + "# foo *bar* \\*baz\\*\n": "NOT_TO_EQUAL", + "# foo \n": "TO_EQUAL", + " ### foo\n ## foo\n # foo\n": "TO_EQUAL", + " # foo\n": "TO_EQUAL", + "foo\n # bar\n": "TO_EQUAL", + "## foo ##\n ### bar ###\n": "TO_EQUAL", + "# foo ##################################\n##### foo ##\n": "TO_EQUAL", + "### foo ### \n": "TO_EQUAL", + "### foo ### b\n": "TO_EQUAL", + "# foo#\n": "NOT_TO_EQUAL", + "### foo \\###\n## foo #\\##\n# foo \\#\n": "NOT_TO_EQUAL", + "****\n## foo\n****\n": "NOT_TO_EQUAL", + "Foo bar\n# baz\nBar foo\n": "TO_EQUAL", + "## \n#\n### ###\n": "TO_ERROR", + "Foo *bar*\n=========\n\nFoo *bar*\n---------\n": "TO_EQUAL", + "Foo *bar\nbaz*\n====\n": "NOT_TO_EQUAL", + "Foo\n-------------------------\n\nFoo\n=\n": "TO_EQUAL", + " Foo\n---\n\n Foo\n-----\n\n Foo\n ===\n": "NOT_TO_EQUAL", + " Foo\n ---\n\n Foo\n---\n": "NOT_TO_EQUAL", + "Foo\n ---- \n": "NOT_TO_EQUAL", + "Foo\n ---\n": "TO_EQUAL", + "Foo\n= =\n\nFoo\n--- -\n": "NOT_TO_EQUAL", + "Foo \n-----\n": "TO_EQUAL", + "Foo\\\n----\n": "TO_EQUAL", + "`Foo\n----\n`\n\n\n": "NOT_TO_EQUAL", + "> Foo\n---\n": "NOT_TO_EQUAL", + "> foo\nbar\n===\n": "NOT_TO_EQUAL", + "- Foo\n---\n": "NOT_TO_EQUAL", + "Foo\nBar\n---\n": "NOT_TO_EQUAL", + "---\nFoo\n---\nBar\n---\nBaz\n": "NOT_TO_EQUAL", + "\n====\n": "TO_EQUAL", + "---\n---\n": "TO_ERROR", + "- foo\n-----\n": "NOT_TO_EQUAL", + " foo\n---\n": "NOT_TO_EQUAL", + "> foo\n-----\n": "NOT_TO_EQUAL", + "\\> foo\n------\n": "NOT_TO_EQUAL", + "Foo\n\nbar\n---\nbaz\n": "TO_EQUAL", + "Foo\nbar\n\n---\n\nbaz\n": "NOT_TO_EQUAL", + "Foo\nbar\n* * *\nbaz\n": "NOT_TO_EQUAL", + "Foo\nbar\n\\---\nbaz\n": "NOT_TO_EQUAL", + " a simple\n indented code block\n": "TO_EQUAL", + " - foo\n\n bar\n": "NOT_TO_EQUAL", + "1. foo\n\n - bar\n": "TO_EQUAL", + " \n *hi*\n\n - one\n": "NOT_TO_EQUAL", + " chunk1\n\n chunk2\n \n \n \n chunk3\n": "TO_EQUAL", + " chunk1\n \n chunk2\n": "TO_EQUAL", + "Foo\n bar\n\n": "TO_EQUAL", + " foo\nbar\n": "TO_EQUAL", + "# Heading\n foo\nHeading\n------\n foo\n----\n": "NOT_TO_EQUAL", + " foo\n bar\n": "TO_EQUAL", + "\n \n foo\n \n\n": "TO_EQUAL", + " foo \n": "TO_EQUAL", + "```\n<\n >\n```\n": "NOT_TO_EQUAL", + "~~~\n<\n >\n~~~\n": "NOT_TO_EQUAL", + "``\nfoo\n``\n": "TO_EQUAL", + "```\naaa\n~~~\n```\n": "NOT_TO_EQUAL", + "~~~\naaa\n```\n~~~\n": "NOT_TO_EQUAL", + "````\naaa\n```\n``````\n": "NOT_TO_EQUAL", + "~~~~\naaa\n~~~\n~~~~\n": "NOT_TO_EQUAL", + "```\n": "TO_EQUAL", + "`````\n\n```\naaa\n": "NOT_TO_EQUAL", + "> ```\n> aaa\n\nbbb\n": "TO_EQUAL", + "```\n\n \n```\n": "NOT_TO_EQUAL", + "```\n```\n": "TO_EQUAL", + " ```\n aaa\naaa\n```\n": "TO_EQUAL", + " ```\naaa\n aaa\naaa\n ```\n": "TO_EQUAL", + " ```\n aaa\n aaa\n aaa\n ```\n": "TO_EQUAL", + " ```\n aaa\n ```\n": "NOT_TO_EQUAL", + "```\naaa\n ```\n": "TO_EQUAL", + " ```\naaa\n ```\n": "TO_EQUAL", + "```\naaa\n ```\n": "NOT_TO_EQUAL", + "``` ```\naaa\n": "TO_EQUAL", + "~~~~~~\naaa\n~~~ ~~\n": "NOT_TO_EQUAL", + "foo\n```\nbar\n```\nbaz\n": "TO_EQUAL", + "foo\n---\n~~~\nbar\n~~~\n# baz\n": "TO_EQUAL", + "```ruby\ndef foo(x)\n return 3\nend\n```\n": "TO_EQUAL", + "~~~~ ruby startline=3 $%@#$\ndef foo(x)\n return 3\nend\n~~~~~~~\n": "TO_EQUAL", + "````;\n````\n": "TO_EQUAL", + "``` aa ```\nfoo\n": "TO_EQUAL", + "```\n``` aaa\n```\n": "NOT_TO_EQUAL", + "
\n
\n**Hello**,\n\n_world_.\n
\n
\n": "NOT_TO_EQUAL", + "\n \n \n \n
\n hi\n
\n\nokay.\n": "TO_EQUAL", + "
\n*foo*\n": "NOT_TO_EQUAL", + "
\n\n*Markdown*\n\n
\n": "TO_EQUAL", + "
\n
\n": "TO_EQUAL", + "
\n
\n": "TO_EQUAL", + "
\n*foo*\n\n*bar*\n": "NOT_TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "
\nfoo\n
\n": "TO_EQUAL", + "
\n``` c\nint x = 33;\n```\n": "NOT_TO_EQUAL", + "\n*bar*\n\n": "NOT_TO_EQUAL", + "\n*bar*\n\n": "NOT_TO_EQUAL", + "\n*bar*\n\n": "NOT_TO_EQUAL", + "\n*bar*\n": "NOT_TO_EQUAL", + "\n*foo*\n\n": "NOT_TO_EQUAL", + "\n\n*foo*\n\n\n": "TO_EQUAL", + "*foo*\n": "TO_EQUAL", + "
\nimport Text.HTML.TagSoup\n\nmain :: IO ()\nmain = print $ parseTags tags\n
\nokay\n": "TO_EQUAL", + "\nokay\n": "TO_EQUAL", + "\nh1 {color:red;}\n\np {color:blue;}\n\nokay\n": "TO_EQUAL", + "\n\nfoo\n": "TO_EQUAL", + ">
\n> foo\n\nbar\n": "TO_EQUAL", + "-
\n- foo\n": "TO_EQUAL", + "\n*foo*\n": "TO_EQUAL", + "*bar*\n*baz*\n": "NOT_TO_EQUAL", + "1. *bar*\n": "NOT_TO_EQUAL", + "\nokay\n": "TO_EQUAL", + "';\n\n?>\nokay\n": "TO_EQUAL", + "\n": "TO_EQUAL", + "\nokay\n": "NOT_TO_EQUAL", + " \n\n \n": "NOT_TO_EQUAL", + "
\n\n
\n": "NOT_TO_EQUAL", + "Foo\n
\nbar\n
\n": "TO_EQUAL", + "
\nbar\n
\n*foo*\n": "NOT_TO_EQUAL", + "Foo\n\nbaz\n": "TO_EQUAL", + "
\n\n*Emphasized* text.\n\n
\n": "TO_EQUAL", + "
\n*Emphasized* text.\n
\n": "NOT_TO_EQUAL", + "\n\n\n\n\n\n\n\n
\nHi\n
\n": "TO_EQUAL", + "\n\n \n\n \n\n \n\n
\n Hi\n
\n": "NOT_TO_EQUAL", + "[foo]: /url \"title\"\n\n[foo]\n": "TO_EQUAL", + " [foo]: \n /url \n 'the title' \n\n[foo]\n": "TO_EQUAL", + "[Foo*bar\\]]:my_(url) 'title (with parens)'\n\n[Foo*bar\\]]\n": "NOT_TO_EQUAL", + "[Foo bar]:\n\n'title'\n\n[Foo bar]\n": "TO_EQUAL", + "[foo]: /url '\ntitle\nline1\nline2\n'\n\n[foo]\n": "NOT_TO_EQUAL", + "[foo]: /url 'title\n\nwith blank line'\n\n[foo]\n": "TO_EQUAL", + "[foo]:\n/url\n\n[foo]\n": "TO_EQUAL", + "[foo]:\n\n[foo]\n": "TO_EQUAL", + "[foo]: /url\\bar\\*baz \"foo\\\"bar\\baz\"\n\n[foo]\n": "NOT_TO_EQUAL", + "[foo]\n\n[foo]: url\n": "TO_EQUAL", + "[foo]\n\n[foo]: first\n[foo]: second\n": "NOT_TO_EQUAL", + "[FOO]: /url\n\n[Foo]\n": "TO_EQUAL", + "[ΑΓΩ]: /φου\n\n[αγω]\n": "TO_EQUAL", + "[foo]: /url\n": "TO_ERROR", + "[\nfoo\n]: /url\nbar\n": "TO_EQUAL", + "[foo]: /url \"title\" ok\n": "NOT_TO_EQUAL", + "[foo]: /url\n\"title\" ok\n": "NOT_TO_EQUAL", + " [foo]: /url \"title\"\n\n[foo]\n": "NOT_TO_EQUAL", + "```\n[foo]: /url\n```\n\n[foo]\n": "TO_EQUAL", + "Foo\n[bar]: /baz\n\n[bar]\n": "TO_EQUAL", + "# [Foo]\n[foo]: /url\n> bar\n": "TO_EQUAL", + "[foo]: /foo-url \"foo\"\n[bar]: /bar-url\n \"bar\"\n[baz]: /baz-url\n\n[foo],\n[bar],\n[baz]\n": "TO_EQUAL", + "[foo]\n\n> [foo]: /url\n": "NOT_TO_EQUAL", + "aaa\n\nbbb\n": "TO_EQUAL", + "aaa\nbbb\n\nccc\nddd\n": "TO_EQUAL", + "aaa\n\n\nbbb\n": "TO_EQUAL", + " aaa\n bbb\n": "NOT_TO_EQUAL", + "aaa\n bbb\n ccc\n": "TO_EQUAL", + " aaa\nbbb\n": "NOT_TO_EQUAL", + " aaa\nbbb\n": "TO_EQUAL", + "aaa \nbbb \n": "NOT_TO_EQUAL", + " \n\naaa\n \n\n# aaa\n\n \n": "TO_EQUAL", + "> # Foo\n> bar\n> baz\n": "TO_EQUAL", + "># Foo\n>bar\n> baz\n": "TO_EQUAL", + " > # Foo\n > bar\n > baz\n": "TO_EQUAL", + " > # Foo\n > bar\n > baz\n": "NOT_TO_EQUAL", + "> # Foo\n> bar\nbaz\n": "TO_EQUAL", + "> bar\nbaz\n> foo\n": "TO_EQUAL", + "> foo\n---\n": "NOT_TO_EQUAL", + "> - foo\n- bar\n": "TO_EQUAL", + "> foo\n bar\n": "TO_EQUAL", + "> ```\nfoo\n```\n": "NOT_TO_EQUAL", + "> foo\n - bar\n": "NOT_TO_EQUAL", + ">\n": "TO_ERROR", + ">\n> \n> \n": "TO_ERROR", + ">\n> foo\n> \n": "TO_EQUAL", + "> foo\n\n> bar\n": "NOT_TO_EQUAL", + "> foo\n> bar\n": "TO_EQUAL", + "> foo\n>\n> bar\n": "TO_EQUAL", + "foo\n> bar\n": "TO_EQUAL", + "> aaa\n***\n> bbb\n": "NOT_TO_EQUAL", + "> bar\nbaz\n": "TO_EQUAL", + "> bar\n\nbaz\n": "TO_EQUAL", + "> bar\n>\nbaz\n": "NOT_TO_EQUAL", + "> > > foo\nbar\n": "TO_EQUAL", + ">>> foo\n> bar\n>>baz\n": "TO_EQUAL", + "> code\n\n> not code\n": "NOT_TO_EQUAL", + "A paragraph\nwith two lines.\n\n indented code\n\n> A block quote.\n": "TO_EQUAL", + "1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "TO_EQUAL", + "- one\n\n two\n": "NOT_TO_EQUAL", + "- one\n\n two\n": "NOT_TO_EQUAL", + " - one\n\n two\n": "NOT_TO_EQUAL", + " - one\n\n two\n": "NOT_TO_EQUAL", + " > > 1. one\n>>\n>> two\n": "NOT_TO_EQUAL", + ">>- one\n>>\n > > two\n": "TO_EQUAL", + "-one\n\n2.two\n": "TO_EQUAL", + "- foo\n\n\n bar\n": "TO_EQUAL", + "1. foo\n\n ```\n bar\n ```\n\n baz\n\n > bam\n": "TO_EQUAL", + "- Foo\n\n bar\n\n\n baz\n": "NOT_TO_EQUAL", + "123456789. ok\n": "TO_EQUAL", + "1234567890. not ok\n": "NOT_TO_EQUAL", + "0. ok\n": "NOT_TO_EQUAL", + "003. ok\n": "TO_EQUAL", + "-1. not ok\n": "TO_EQUAL", + "- foo\n\n bar\n": "TO_EQUAL", + " 10. foo\n\n bar\n": "TO_EQUAL", + " indented code\n\nparagraph\n\n more code\n": "TO_EQUAL", + "1. indented code\n\n paragraph\n\n more code\n": "TO_EQUAL", + "1. indented code\n\n paragraph\n\n more code\n": "TO_EQUAL", + " foo\n\nbar\n": "NOT_TO_EQUAL", + "- foo\n\n bar\n": "NOT_TO_EQUAL", + "- foo\n\n bar\n": "NOT_TO_EQUAL", + "-\n foo\n-\n ```\n bar\n ```\n-\n baz\n": "NOT_TO_EQUAL", + "- \n foo\n": "TO_ERROR", + "-\n\n foo\n": "NOT_TO_EQUAL", + "- foo\n-\n- bar\n": "TO_ERROR", + "- foo\n- \n- bar\n": "TO_ERROR", + "1. foo\n2.\n3. bar\n": "TO_ERROR", + "*\n": "NOT_TO_EQUAL", + "foo\n*\n\nfoo\n1.\n": "TO_EQUAL", + " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "TO_EQUAL", + " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "TO_EQUAL", + " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "TO_EQUAL", + " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "NOT_TO_EQUAL", + " 1. A paragraph\nwith two lines.\n\n indented code\n\n > A block quote.\n": "NOT_TO_EQUAL", + " 1. A paragraph\n with two lines.\n": "TO_EQUAL", + "> 1. > Blockquote\ncontinued here.\n": "TO_EQUAL", + "> 1. > Blockquote\n> continued here.\n": "TO_EQUAL", + "- foo\n - bar\n - baz\n - boo\n": "NOT_TO_EQUAL", + "- foo\n - bar\n - baz\n - boo\n": "TO_EQUAL", + "10) foo\n - bar\n": "NOT_TO_EQUAL", + "10) foo\n - bar\n": "TO_EQUAL", + "- - foo\n": "TO_EQUAL", + "1. - 2. foo\n": "TO_EQUAL", + "- # Foo\n- Bar\n ---\n baz\n": "NOT_TO_EQUAL", + "- foo\n- bar\n+ baz\n": "TO_EQUAL", + "1. foo\n2. bar\n3) baz\n": "TO_EQUAL", + "Foo\n- bar\n- baz\n": "TO_EQUAL", + "The number of windows in my house is\n14. The number of doors is 6.\n": "NOT_TO_EQUAL", + "The number of windows in my house is\n1. The number of doors is 6.\n": "TO_EQUAL", + "- foo\n\n- bar\n\n\n- baz\n": "NOT_TO_EQUAL", + "- foo\n - bar\n - baz\n\n\n bim\n": "NOT_TO_EQUAL", + "- foo\n- bar\n\n\n\n- baz\n- bim\n": "TO_EQUAL", + "- foo\n\n notcode\n\n- foo\n\n\n\n code\n": "NOT_TO_EQUAL", + "- a\n - b\n - c\n - d\n - e\n - f\n - g\n - h\n- i\n": "NOT_TO_EQUAL", + "1. a\n\n 2. b\n\n 3. c\n": "NOT_TO_EQUAL", + "- a\n- b\n\n- c\n": "NOT_TO_EQUAL", + "* a\n*\n\n* c\n": "TO_ERROR", + "- a\n- b\n\n c\n- d\n": "NOT_TO_EQUAL", + "- a\n- b\n\n [ref]: /url\n- d\n": "NOT_TO_EQUAL", + "- a\n- ```\n b\n\n\n ```\n- c\n": "NOT_TO_EQUAL", + "- a\n - b\n\n c\n- d\n": "NOT_TO_EQUAL", + "* a\n > b\n >\n* c\n": "NOT_TO_EQUAL", + "- a\n > b\n ```\n c\n ```\n- d\n": "NOT_TO_EQUAL", + "- a\n": "TO_EQUAL", + "- a\n - b\n": "NOT_TO_EQUAL", + "1. ```\n foo\n ```\n\n bar\n": "TO_EQUAL", + "* foo\n * bar\n\n baz\n": "NOT_TO_EQUAL", + "- a\n - b\n - c\n\n- d\n - e\n - f\n": "TO_EQUAL", + "`hi`lo`\n": "TO_EQUAL", + "\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~\n": "NOT_TO_EQUAL", + "\\\t\\A\\a\\ \\3\\φ\\«\n": "TO_EQUAL", + "\\*not emphasized*\n\\
not a tag\n\\[not a link](/foo)\n\\`not code`\n1\\. not a list\n\\* not a list\n\\# not a heading\n\\[foo]: /url \"not a reference\"\n": "NOT_TO_EQUAL", + "\\\\*emphasis*\n": "NOT_TO_EQUAL", + "foo\\\nbar\n": "NOT_TO_EQUAL", + "`` \\[\\` ``\n": "TO_EQUAL", + " \\[\\]\n": "TO_EQUAL", + "~~~\n\\[\\]\n~~~\n": "TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "
\n": "TO_EQUAL", + "[foo](/bar\\* \"ti\\*tle\")\n": "TO_EQUAL", + "[foo]\n\n[foo]: /bar\\* \"ti\\*tle\"\n": "TO_EQUAL", + "``` foo\\+bar\nfoo\n```\n": "TO_EQUAL", + "  & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸\n": "NOT_TO_EQUAL", + "# Ӓ Ϡ � �\n": "NOT_TO_EQUAL", + "" ആ ಫ\n": "NOT_TO_EQUAL", + "  &x; &#; &#x;\n&ThisIsNotDefined; &hi?;\n": "NOT_TO_EQUAL", + "©\n": "NOT_TO_EQUAL", + "&MadeUpEntity;\n": "NOT_TO_EQUAL", + "\n": "TO_EQUAL", + "[foo](/föö \"föö\")\n": "TO_EQUAL", + "[foo]\n\n[foo]: /föö \"föö\"\n": "TO_EQUAL", + "``` föö\nfoo\n```\n": "TO_EQUAL", + "`föö`\n": "NOT_TO_EQUAL", + " föfö\n": "NOT_TO_EQUAL", + "`foo`\n": "TO_EQUAL", + "`` foo ` bar ``\n": "TO_EQUAL", + "` `` `\n": "TO_EQUAL", + "`foo bar\n baz`\n": "TO_EQUAL", + "`a b`\n": "NOT_TO_EQUAL", + "`foo `` bar`\n": "TO_EQUAL", + "`foo\\`bar`\n": "TO_EQUAL", + "*foo`*`\n": "NOT_TO_EQUAL", + "[not a `link](/foo`)\n": "NOT_TO_EQUAL", + "``\n": "NOT_TO_EQUAL", + "`\n": "TO_EQUAL", + "``\n": "NOT_TO_EQUAL", + "`\n": "TO_EQUAL", + "```foo``\n": "NOT_TO_EQUAL", + "`foo\n": "TO_EQUAL", + "`foo``bar``\n": "NOT_TO_EQUAL", + "*foo bar*\n": "TO_EQUAL", + "a * foo bar*\n": "NOT_TO_EQUAL", + "a*\"foo\"*\n": "NOT_TO_EQUAL", + "* a *\n": "NOT_TO_EQUAL", + "foo*bar*\n": "TO_EQUAL", + "5*6*78\n": "NOT_TO_EQUAL", + "_foo bar_\n": "TO_EQUAL", + "_ foo bar_\n": "NOT_TO_EQUAL", + "a_\"foo\"_\n": "NOT_TO_EQUAL", + "foo_bar_\n": "NOT_TO_EQUAL", + "5_6_78\n": "TO_EQUAL", + "пристаням_стремятся_\n": "NOT_TO_EQUAL", + "aa_\"bb\"_cc\n": "NOT_TO_EQUAL", + "foo-_(bar)_\n": "TO_EQUAL", + "_foo*\n": "TO_EQUAL", + "*foo bar *\n": "NOT_TO_EQUAL", + "*foo bar\n*\n": "NOT_TO_EQUAL", + "*(*foo)\n": "NOT_TO_EQUAL", + "*(*foo*)*\n": "NOT_TO_EQUAL", + "*foo*bar\n": "NOT_TO_EQUAL", + "_foo bar _\n": "NOT_TO_EQUAL", + "_(_foo)\n": "TO_EQUAL", + "_(_foo_)_\n": "NOT_TO_EQUAL", + "_foo_bar\n": "TO_EQUAL", + "_пристаням_стремятся\n": "NOT_TO_EQUAL", + "_foo_bar_baz_\n": "TO_EQUAL", + "_(bar)_.\n": "TO_EQUAL", + "**foo bar**\n": "TO_EQUAL", + "** foo bar**\n": "NOT_TO_EQUAL", + "a**\"foo\"**\n": "NOT_TO_EQUAL", + "foo**bar**\n": "TO_EQUAL", + "__foo bar__\n": "TO_EQUAL", + "__ foo bar__\n": "NOT_TO_EQUAL", + "__\nfoo bar__\n": "NOT_TO_EQUAL", + "a__\"foo\"__\n": "NOT_TO_EQUAL", + "foo__bar__\n": "NOT_TO_EQUAL", + "5__6__78\n": "NOT_TO_EQUAL", + "пристаням__стремятся__\n": "NOT_TO_EQUAL", + "__foo, __bar__, baz__\n": "NOT_TO_EQUAL", + "foo-__(bar)__\n": "TO_EQUAL", + "**foo bar **\n": "NOT_TO_EQUAL", + "**(**foo)\n": "NOT_TO_EQUAL", + "*(**foo**)*\n": "TO_EQUAL", + "**Gomphocarpus (*Gomphocarpus physocarpus*, syn.\n*Asclepias physocarpa*)**\n": "TO_EQUAL", + "**foo \"*bar*\" foo**\n": "NOT_TO_EQUAL", + "**foo**bar\n": "TO_EQUAL", + "__foo bar __\n": "NOT_TO_EQUAL", + "__(__foo)\n": "NOT_TO_EQUAL", + "_(__foo__)_\n": "TO_EQUAL", + "__foo__bar\n": "NOT_TO_EQUAL", + "__пристаням__стремятся\n": "NOT_TO_EQUAL", + "__foo__bar__baz__\n": "NOT_TO_EQUAL", + "__(bar)__.\n": "TO_EQUAL", + "*foo [bar](/url)*\n": "TO_EQUAL", + "*foo\nbar*\n": "TO_EQUAL", + "_foo __bar__ baz_\n": "TO_EQUAL", + "_foo _bar_ baz_\n": "NOT_TO_EQUAL", + "__foo_ bar_\n": "NOT_TO_EQUAL", + "*foo *bar**\n": "NOT_TO_EQUAL", + "*foo **bar** baz*\n": "TO_EQUAL", + "*foo**bar**baz*\n": "TO_EQUAL", + "***foo** bar*\n": "NOT_TO_EQUAL", + "*foo **bar***\n": "NOT_TO_EQUAL", + "*foo**bar***\n": "NOT_TO_EQUAL", + "*foo **bar *baz* bim** bop*\n": "NOT_TO_EQUAL", + "*foo [*bar*](/url)*\n": "NOT_TO_EQUAL", + "** is not an empty emphasis\n": "TO_EQUAL", + "**** is not an empty strong emphasis\n": "TO_EQUAL", + "**foo [bar](/url)**\n": "TO_EQUAL", + "**foo\nbar**\n": "TO_EQUAL", + "__foo _bar_ baz__\n": "TO_EQUAL", + "__foo __bar__ baz__\n": "NOT_TO_EQUAL", + "____foo__ bar__\n": "NOT_TO_EQUAL", + "**foo **bar****\n": "NOT_TO_EQUAL", + "**foo *bar* baz**\n": "TO_EQUAL", + "**foo*bar*baz**\n": "NOT_TO_EQUAL", + "***foo* bar**\n": "TO_EQUAL", + "**foo *bar***\n": "TO_EQUAL", + "**foo *bar **baz**\nbim* bop**\n": "NOT_TO_EQUAL", + "**foo [*bar*](/url)**\n": "TO_EQUAL", + "__ is not an empty emphasis\n": "TO_EQUAL", + "____ is not an empty strong emphasis\n": "TO_EQUAL", + "foo ***\n": "TO_EQUAL", + "foo *\\**\n": "NOT_TO_EQUAL", + "foo *_*\n": "NOT_TO_EQUAL", + "foo *****\n": "NOT_TO_EQUAL", + "foo **\\***\n": "TO_EQUAL", + "foo **_**\n": "TO_EQUAL", + "**foo*\n": "TO_EQUAL", + "*foo**\n": "NOT_TO_EQUAL", + "***foo**\n": "NOT_TO_EQUAL", + "****foo*\n": "NOT_TO_EQUAL", + "**foo***\n": "NOT_TO_EQUAL", + "*foo****\n": "NOT_TO_EQUAL", + "foo ___\n": "TO_EQUAL", + "foo _\\__\n": "NOT_TO_EQUAL", + "foo _*_\n": "TO_EQUAL", + "foo _____\n": "NOT_TO_EQUAL", + "foo __\\___\n": "TO_EQUAL", + "foo __*__\n": "TO_EQUAL", + "__foo_\n": "TO_EQUAL", + "_foo__\n": "NOT_TO_EQUAL", + "___foo__\n": "NOT_TO_EQUAL", + "____foo_\n": "NOT_TO_EQUAL", + "__foo___\n": "NOT_TO_EQUAL", + "_foo____\n": "NOT_TO_EQUAL", + "**foo**\n": "TO_EQUAL", + "*_foo_*\n": "NOT_TO_EQUAL", + "__foo__\n": "TO_EQUAL", + "_*foo*_\n": "NOT_TO_EQUAL", + "****foo****\n": "NOT_TO_EQUAL", + "____foo____\n": "NOT_TO_EQUAL", + "******foo******\n": "NOT_TO_EQUAL", + "***foo***\n": "TO_EQUAL", + "_____foo_____\n": "NOT_TO_EQUAL", + "*foo _bar* baz_\n": "TO_EQUAL", + "*foo __bar *baz bim__ bam*\n": "NOT_TO_EQUAL", + "**foo **bar baz**\n": "NOT_TO_EQUAL", + "*foo *bar baz*\n": "NOT_TO_EQUAL", + "*[bar*](/url)\n": "NOT_TO_EQUAL", + "_foo [bar_](/url)\n": "NOT_TO_EQUAL", + "*\n": "NOT_TO_EQUAL", + "**\n": "NOT_TO_EQUAL", + "__\n": "NOT_TO_EQUAL", + "*a `*`*\n": "NOT_TO_EQUAL", + "_a `_`_\n": "NOT_TO_EQUAL", + "**a\n": "NOT_TO_EQUAL", + "__a\n": "NOT_TO_EQUAL", + "[link](/uri \"title\")\n": "TO_EQUAL", + "[link](/uri)\n": "TO_EQUAL", + "[link]()\n": "TO_EQUAL", + "[link](<>)\n": "TO_EQUAL", + "[link](/my uri)\n": "TO_EQUAL", + "[link]()\n": "NOT_TO_EQUAL", + "[link](foo\nbar)\n": "TO_EQUAL", + "[link]()\n": "TO_EQUAL", + "[link](\\(foo\\))\n": "TO_EQUAL", + "[link](foo(and(bar)))\n": "TO_EQUAL", + "[link](foo\\(and\\(bar\\))\n": "TO_EQUAL", + "[link]()\n": "TO_EQUAL", + "[link](foo\\)\\:)\n": "TO_EQUAL", + "[link](#fragment)\n\n[link](http://example.com#fragment)\n\n[link](http://example.com?foo=3#frag)\n": "TO_EQUAL", + "[link](foo\\bar)\n": "TO_EQUAL", + "[link](foo%20bä)\n": "TO_EQUAL", + "[link](\"title\")\n": "TO_EQUAL", + "[link](/url \"title\")\n[link](/url 'title')\n[link](/url (title))\n": "TO_EQUAL", + "[link](/url \"title \\\""\")\n": "NOT_TO_EQUAL", + "[link](/url \"title\")\n": "NOT_TO_EQUAL", + "[link](/url \"title \"and\" title\")\n": "NOT_TO_EQUAL", + "[link](/url 'title \"and\" title')\n": "NOT_TO_EQUAL", + "[link]( /uri\n \"title\" )\n": "TO_EQUAL", + "[link] (/uri)\n": "NOT_TO_EQUAL", + "[link [foo [bar]]](/uri)\n": "NOT_TO_EQUAL", + "[link] bar](/uri)\n": "TO_EQUAL", + "[link [bar](/uri)\n": "TO_EQUAL", + "[link \\[bar](/uri)\n": "NOT_TO_EQUAL", + "[link *foo **bar** `#`*](/uri)\n": "TO_EQUAL", + "[![moon](moon.jpg)](/uri)\n": "NOT_TO_EQUAL", + "[foo [bar](/uri)](/uri)\n": "NOT_TO_EQUAL", + "[foo *[bar [baz](/uri)](/uri)*](/uri)\n": "NOT_TO_EQUAL", + "![[[foo](uri1)](uri2)](uri3)\n": "NOT_TO_EQUAL", + "*[foo*](/uri)\n": "NOT_TO_EQUAL", + "[foo *bar](baz*)\n": "TO_EQUAL", + "*foo [bar* baz]\n": "TO_EQUAL", + "[foo \n": "NOT_TO_EQUAL", + "[foo`](/uri)`\n": "NOT_TO_EQUAL", + "[foo\n": "NOT_TO_EQUAL", + "[foo][bar]\n\n[bar]: /url \"title\"\n": "TO_EQUAL", + "[link [foo [bar]]][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL", + "[link \\[bar][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL", + "[link *foo **bar** `#`*][ref]\n\n[ref]: /uri\n": "TO_EQUAL", + "[![moon](moon.jpg)][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL", + "[foo [bar](/uri)][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL", + "[foo *bar [baz][ref]*][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL", + "*[foo*][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL", + "[foo *bar][ref]\n\n[ref]: /uri\n": "TO_EQUAL", + "[foo \n\n[ref]: /uri\n": "NOT_TO_EQUAL", + "[foo`][ref]`\n\n[ref]: /uri\n": "NOT_TO_EQUAL", + "[foo\n\n[ref]: /uri\n": "NOT_TO_EQUAL", + "[foo][BaR]\n\n[bar]: /url \"title\"\n": "TO_EQUAL", + "[Толпой][Толпой] is a Russian word.\n\n[ТОЛПОЙ]: /url\n": "TO_EQUAL", + "[Foo\n bar]: /url\n\n[Baz][Foo bar]\n": "TO_EQUAL", + "[foo] [bar]\n\n[bar]: /url \"title\"\n": "NOT_TO_EQUAL", + "[foo]\n[bar]\n\n[bar]: /url \"title\"\n": "NOT_TO_EQUAL", + "[foo]: /url1\n\n[foo]: /url2\n\n[bar][foo]\n": "NOT_TO_EQUAL", + "[bar][foo\\!]\n\n[foo!]: /url\n": "NOT_TO_EQUAL", + "[foo][ref[]\n\n[ref[]: /uri\n": "NOT_TO_EQUAL", + "[foo][ref[bar]]\n\n[ref[bar]]: /uri\n": "NOT_TO_EQUAL", + "[[[foo]]]\n\n[[[foo]]]: /url\n": "TO_EQUAL", + "[foo][ref\\[]\n\n[ref\\[]: /uri\n": "TO_EQUAL", + "[bar\\\\]: /uri\n\n[bar\\\\]\n": "NOT_TO_EQUAL", + "[]\n\n[]: /uri\n": "TO_EQUAL", + "[\n ]\n\n[\n ]: /uri\n": "NOT_TO_EQUAL", + "[foo][]\n\n[foo]: /url \"title\"\n": "TO_EQUAL", + "[*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n": "TO_EQUAL", + "[Foo][]\n\n[foo]: /url \"title\"\n": "TO_EQUAL", + "[foo] \n[]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", + "[foo]\n\n[foo]: /url \"title\"\n": "TO_EQUAL", + "[*foo* bar]\n\n[*foo* bar]: /url \"title\"\n": "TO_EQUAL", + "[[*foo* bar]]\n\n[*foo* bar]: /url \"title\"\n": "TO_EQUAL", + "[[bar [foo]\n\n[foo]: /url\n": "TO_EQUAL", + "[Foo]\n\n[foo]: /url \"title\"\n": "TO_EQUAL", + "[foo] bar\n\n[foo]: /url\n": "TO_EQUAL", + "\\[foo]\n\n[foo]: /url \"title\"\n": "TO_EQUAL", + "[foo*]: /url\n\n*[foo*]\n": "NOT_TO_EQUAL", + "[foo][bar]\n\n[foo]: /url1\n[bar]: /url2\n": "TO_EQUAL", + "[foo][]\n\n[foo]: /url1\n": "TO_EQUAL", + "[foo]()\n\n[foo]: /url1\n": "TO_EQUAL", + "[foo](not a link)\n\n[foo]: /url1\n": "TO_EQUAL", + "[foo][bar][baz]\n\n[baz]: /url\n": "NOT_TO_EQUAL", + "[foo][bar][baz]\n\n[baz]: /url1\n[bar]: /url2\n": "TO_EQUAL", + "[foo][bar][baz]\n\n[baz]: /url1\n[foo]: /url2\n": "NOT_TO_EQUAL", + "![foo](/url \"title\")\n": "NOT_TO_EQUAL", + "![foo *bar*]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n": "NOT_TO_EQUAL", + "![foo ![bar](/url)](/url2)\n": "NOT_TO_EQUAL", + "![foo [bar](/url)](/url2)\n": "NOT_TO_EQUAL", + "![foo *bar*][]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n": "NOT_TO_EQUAL", + "![foo *bar*][foobar]\n\n[FOOBAR]: train.jpg \"train & tracks\"\n": "NOT_TO_EQUAL", + "![foo](train.jpg)\n": "NOT_TO_EQUAL", + "My ![foo bar](/path/to/train.jpg \"title\" )\n": "NOT_TO_EQUAL", + "![foo]()\n": "NOT_TO_EQUAL", + "![](/url)\n": "NOT_TO_EQUAL", + "![foo][bar]\n\n[bar]: /url\n": "NOT_TO_EQUAL", + "![foo][bar]\n\n[BAR]: /url\n": "NOT_TO_EQUAL", + "![foo][]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", + "![*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n": "NOT_TO_EQUAL", + "![Foo][]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", + "![foo] \n[]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", + "![foo]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", + "![*foo* bar]\n\n[*foo* bar]: /url \"title\"\n": "NOT_TO_EQUAL", + "![[foo]]\n\n[[foo]]: /url \"title\"\n": "NOT_TO_EQUAL", + "![Foo]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", + "!\\[foo]\n\n[foo]: /url \"title\"\n": "TO_EQUAL", + "\\![foo]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "<>\n": "NOT_TO_EQUAL", + "< http://foo.bar >\n": "NOT_TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "http://example.com\n": "TO_EQUAL", + "foo@bar.example.com\n": "TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "TO_EQUAL", + "Foo \n": "TO_EQUAL", + "<33> <__>\n": "NOT_TO_EQUAL", + "\n": "NOT_TO_EQUAL", + " \n": "NOT_TO_EQUAL", + "< a><\nfoo>\n": "NOT_TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "foo \n": "TO_EQUAL", + "foo \n": "NOT_TO_EQUAL", + "foo foo -->\n\nfoo \n": "NOT_TO_EQUAL", + "foo \n": "TO_EQUAL", + "foo \n": "TO_EQUAL", + "foo &<]]>\n": "NOT_TO_EQUAL", + "foo \n": "TO_EQUAL", + "foo \n": "TO_EQUAL", + "\n": "NOT_TO_EQUAL", + "foo \nbaz\n": "NOT_TO_EQUAL", + "foo\\\nbaz\n": "NOT_TO_EQUAL", + "foo \nbaz\n": "NOT_TO_EQUAL", + "foo \n bar\n": "NOT_TO_EQUAL", + "foo\\\n bar\n": "NOT_TO_EQUAL", + "*foo \nbar*\n": "NOT_TO_EQUAL", + "*foo\\\nbar*\n": "NOT_TO_EQUAL", + "`code \nspan`\n": "TO_EQUAL", + "`code\\\nspan`\n": "TO_EQUAL", + "\n": "TO_EQUAL", + "\n": "TO_EQUAL", + "foo\\\n": "TO_EQUAL", + "foo \n": "TO_EQUAL", + "### foo\\\n": "TO_EQUAL", + "### foo \n": "TO_EQUAL", + "foo\nbaz\n": "TO_EQUAL", + "foo \n baz\n": "TO_EQUAL", + "hello $.;'there\n": "TO_EQUAL", + "Foo χρῆν\n": "TO_EQUAL", + "Multiple spaces\n": "TO_EQUAL" +} \ No newline at end of file diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/duplicate_marks_github_issue_3280.md b/packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/duplicate_marks_github_issue_3280.md new file mode 100644 index 000000000000..2d6cad7de1d4 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/__fixtures__/duplicate_marks_github_issue_3280.md @@ -0,0 +1 @@ +Fill to_*this*_mark, and your charge is but a penny; to_*this*_a penny more; and so on to the full glass—the Cape Horn measure, which you may gulp down for a shilling.\n\nUpon entering the place I found a number of young seamen gathered about a table, examining by a dim light divers specimens of_*skrimshander*. \ No newline at end of file diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/__snapshots__/remarkShortcodes.spec.js.snap b/packages/decap-cms-widget-richtext/src/serializers/__tests__/__snapshots__/remarkShortcodes.spec.js.snap new file mode 100644 index 000000000000..6599c9399c72 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/__snapshots__/remarkShortcodes.spec.js.snap @@ -0,0 +1,132 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`remarkParseShortcodes parse pattern with leading caret should be a remark shortcode node 1`] = ` +Object { + "children": Array [ + Object { + "data": Object { + "shortcode": "foo", + "shortcodeData": Object { + "bar": "baz", + }, + }, + "type": "shortcode", + }, + ], + "type": "root", +} +`; + +exports[`remarkParseShortcodes parse pattern with leading caret should parse multiple shortcodes 1`] = ` +Object { + "children": Array [ + Object { + "children": Array [ + Object { + "type": "text", + "value": "paragraph", + }, + ], + "type": "paragraph", + }, + Object { + "data": Object { + "shortcode": "foo", + "shortcodeData": Object { + "bar": "bar", + }, + }, + "type": "shortcode", + }, + Object { + "data": Object { + "shortcode": "foo", + "shortcodeData": Object { + "bar": "baz", + }, + }, + "type": "shortcode", + }, + Object { + "children": Array [ + Object { + "type": "text", + "value": "next para", + }, + ], + "type": "paragraph", + }, + ], + "type": "root", +} +`; + +exports[`remarkParseShortcodes parse pattern without leading caret should handle pattern without leading caret 1`] = ` +Object { + "children": Array [ + Object { + "children": Array [ + Object { + "type": "text", + "value": "paragraph", + }, + ], + "type": "paragraph", + }, + Object { + "data": Object { + "shortcode": "foo", + "shortcodeData": Object { + "bar": "baz", + }, + }, + "type": "shortcode", + }, + ], + "type": "root", +} +`; + +exports[`remarkParseShortcodes parse pattern without leading caret should parse multiple shortcodes 1`] = ` +Object { + "children": Array [ + Object { + "children": Array [ + Object { + "type": "text", + "value": "paragraph", + }, + ], + "type": "paragraph", + }, + Object { + "data": Object { + "shortcode": "foo", + "shortcodeData": Object { + "bar": "bar", + }, + }, + "type": "shortcode", + }, + Object { + "data": Object { + "shortcode": "foo", + "shortcodeData": Object { + "bar": "baz", + }, + }, + "type": "shortcode", + }, + Object { + "children": Array [ + Object { + "type": "text", + "value": "next para", + }, + ], + "type": "paragraph", + }, + ], + "type": "root", +} +`; diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/commonmark.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/commonmark.spec.js new file mode 100644 index 000000000000..230fd574e7d1 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/commonmark.spec.js @@ -0,0 +1,110 @@ +import flow from 'lodash/flow'; +import { tests as commonmarkSpec } from 'commonmark-spec'; +import * as commonmark from 'commonmark'; + +import { markdownToSlate, slateToMarkdown } from '../index.js'; + +const skips = [ + { + number: [456], + reason: 'Remark ¯\\_(ツ)_/¯', + }, + { + number: [416, 417, 424, 425, 426, 431, 457, 460, 462, 464, 467], + reason: 'Remark does not support infinite (redundant) nested marks', + }, + { + number: [455, 469, 470, 471], + reason: 'Remark parses the initial set of identical nested delimiters first', + }, + { + number: [473, 476, 478, 480], + reason: 'we convert underscores to asterisks for strong/emphasis', + }, + { number: 490, reason: 'Remark strips pointy enclosing pointy brackets from link url' }, + { number: 503, reason: 'Remark allows non-breaking space between link url and title' }, + { number: 507, reason: 'Remark allows a space between link alt and url' }, + { + number: [ + 511, 516, 525, 528, 529, 530, 532, 533, 534, 540, 541, 542, 543, 546, 548, 560, 565, 567, + ], + reason: 'we convert link references to standard links, but Remark also fails these', + }, + { + number: [569, 570, 571, 572, 573, 581, 585], + reason: 'Remark does not recognize or remove marks in image alt text', + }, + { number: 589, reason: 'Remark does not honor backslash escape of image exclamation point' }, + { number: 593, reason: 'Remark removes "mailto:" from autolink text' }, + { number: 599, reason: 'Remark does not escape all expected entities' }, + { number: 602, reason: 'Remark allows autolink emails to contain backslashes' }, +]; + +const onlys = [ + // just add the spec number, eg: + // 431, +]; + +/** + * Each test receives input markdown and output html as expected for Commonmark + * compliance. To test all of our handling in one go, we serialize the markdown + * into our Slate AST, then back to raw markdown, and finally to HTML. + */ +const reader = new commonmark.Parser(); +const writer = new commonmark.HtmlRenderer(); + +function parseWithCommonmark(markdown) { + const parsed = reader.parse(markdown); + return writer.render(parsed); +} + +const parse = flow([markdownToSlate, slateToMarkdown]); + +/** + * Passing this test suite requires 100% Commonmark compliance. There are 624 + * tests, of which we're passing about 300 as of introduction of this suite. To + * work on improving Commonmark support, update __fixtures__/commonmarkExpected.json + */ +describe.skip('Commonmark support', function () { + const specs = + onlys.length > 0 + ? commonmarkSpec.filter(({ number }) => onlys.includes(number)) + : commonmarkSpec; + specs.forEach(spec => { + const skip = skips.find(({ number }) => { + return Array.isArray(number) ? number.includes(spec.number) : number === spec.number; + }); + const specUrl = `https://spec.commonmark.org/0.29/#example-${spec.number}`; + const parsed = parse(spec.markdown); + const commonmarkParsedHtml = parseWithCommonmark(parsed); + const description = ` +${spec.section} +${specUrl} + +Spec: +${JSON.stringify(spec, null, 2)} + +Markdown input: +${spec.markdown} + +Markdown parsed through Slate/Remark and back to Markdown: +${parsed} + +HTML output: +${commonmarkParsedHtml} + +Expected HTML output: +${spec.html} + `; + if (skip) { + const showMessage = Array.isArray(skip.number) ? skip.number[0] === spec.number : true; + if (showMessage) { + //console.log(`skipping spec ${skip.number}\n${skip.reason}\n${specUrl}`); + } + } + const testFn = skip ? test.skip : test; + testFn(description, () => { + expect(commonmarkParsedHtml).toEqual(spec.html); + }); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/index.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/index.spec.js new file mode 100644 index 000000000000..33a4fac515ac --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/index.spec.js @@ -0,0 +1,52 @@ +import path from 'path'; +import fs from 'fs'; + +import { markdownToSlate, htmlToSlate } from '../'; + +describe('markdownToSlate', () => { + it('should not add duplicate identical marks under the same node (GitHub Issue 3280)', () => { + const mdast = fs.readFileSync( + path.join(__dirname, '__fixtures__', 'duplicate_marks_github_issue_3280.md'), + ); + const slate = markdownToSlate(mdast); + + expect(slate).toEqual([ + { + type: 'p', + children: [ + { + text: 'Fill to', + }, + { + italic: true, + marks: [{ type: 'italic' }], + text: 'this_mark, and your charge is but a penny; tothisa penny more; and so on to the full glass—the Cape Horn measure, which you may gulp down for a shilling.\\n\\nUpon entering the place I found a number of young seamen gathered about a table, examining by a dim light divers specimens ofskrimshander', + }, + { + text: '.', + }, + ], + }, + ]); + }); +}); + +describe('htmlToSlate', () => { + it('should preserve spaces in rich html (GitHub Issue 3727)', () => { + const html = `Bold Text regular text `; + + const actual = htmlToSlate(html); + expect(actual).toEqual({ + type: 'root', + children: [ + { + type: 'p', + children: [ + { text: 'Bold Text', bold: true, marks: [{ type: 'bold' }] }, + { text: ' regular text' }, + ], + }, + ], + }); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAllowHtmlEntities.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAllowHtmlEntities.spec.js new file mode 100644 index 000000000000..844137f0a440 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAllowHtmlEntities.spec.js @@ -0,0 +1,25 @@ +import unified from 'unified'; +import markdownToRemark from 'remark-parse'; + +import remarkAllowHtmlEntities from '../remarkAllowHtmlEntities'; + +function process(markdown) { + const mdast = unified().use(markdownToRemark).use(remarkAllowHtmlEntities).parse(markdown); + + /** + * The MDAST will look like: + * + * { type: 'root', children: [ + * { type: 'paragraph', children: [ + * // results here + * ]} + * ]} + */ + return mdast.children[0].children[0].value; +} + +describe('remarkAllowHtmlEntities', () => { + it('should not decode HTML entities', () => { + expect(process('<div>')).toEqual('<div>'); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAssertParents.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAssertParents.spec.js new file mode 100644 index 000000000000..670d5c7900ef --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkAssertParents.spec.js @@ -0,0 +1,171 @@ +import u from 'unist-builder'; + +import remarkAssertParents from '../remarkAssertParents'; + +const transform = remarkAssertParents(); + +describe('remarkAssertParents', () => { + it('should unnest invalidly nested blocks', () => { + const input = u('root', [ + u('paragraph', [ + u('paragraph', [u('text', 'Paragraph text.')]), + u('heading', { depth: 1 }, [u('text', 'Heading text.')]), + u('code', 'someCode()'), + u('blockquote', [u('text', 'Quote text.')]), + u('list', [u('listItem', [u('text', 'A list item.')])]), + u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]), + u('thematicBreak'), + ]), + ]); + + const output = u('root', [ + u('paragraph', [u('text', 'Paragraph text.')]), + u('heading', { depth: 1 }, [u('text', 'Heading text.')]), + u('code', 'someCode()'), + u('blockquote', [u('text', 'Quote text.')]), + u('list', [u('listItem', [u('text', 'A list item.')])]), + u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]), + u('thematicBreak'), + ]); + + expect(transform(input)).toEqual(output); + }); + + it('should unnest deeply nested blocks', () => { + const input = u('root', [ + u('paragraph', [ + u('paragraph', [ + u('paragraph', [ + u('paragraph', [u('text', 'Paragraph text.')]), + u('heading', { depth: 1 }, [u('text', 'Heading text.')]), + u('code', 'someCode()'), + u('blockquote', [ + u('paragraph', [u('strong', [u('heading', [u('text', 'Quote text.')])])]), + ]), + u('list', [u('listItem', [u('text', 'A list item.')])]), + u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]), + u('thematicBreak'), + ]), + ]), + ]), + ]); + + const output = u('root', [ + u('paragraph', [u('text', 'Paragraph text.')]), + u('heading', { depth: 1 }, [u('text', 'Heading text.')]), + u('code', 'someCode()'), + u('blockquote', [u('heading', [u('text', 'Quote text.')])]), + u('list', [u('listItem', [u('text', 'A list item.')])]), + u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]), + u('thematicBreak'), + ]); + + expect(transform(input)).toEqual(output); + }); + + it('should remove blocks that are emptied as a result of denesting', () => { + const input = u('root', [ + u('paragraph', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]), + ]); + + const output = u('root', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]); + + expect(transform(input)).toEqual(output); + }); + + it('should remove blocks that are emptied as a result of denesting', () => { + const input = u('root', [ + u('paragraph', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]), + ]); + + const output = u('root', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]); + + expect(transform(input)).toEqual(output); + }); + + it('should handle asymmetrical splits', () => { + const input = u('root', [ + u('paragraph', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]), + ]); + + const output = u('root', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]); + + expect(transform(input)).toEqual(output); + }); + + it('should nest invalidly nested blocks in the nearest valid ancestor', () => { + const input = u('root', [ + u('paragraph', [ + u('blockquote', [u('strong', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])])]), + ]), + ]); + + const output = u('root', [ + u('blockquote', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]), + ]); + + expect(transform(input)).toEqual(output); + }); + + it('should preserve validly nested siblings of invalidly nested blocks', () => { + const input = u('root', [ + u('paragraph', [ + u('blockquote', [ + u('strong', [ + u('text', 'Deep validly nested text a.'), + u('heading', { depth: 1 }, [u('text', 'Heading text.')]), + u('text', 'Deep validly nested text b.'), + ]), + ]), + u('text', 'Validly nested text.'), + ]), + ]); + + const output = u('root', [ + u('blockquote', [ + u('strong', [u('text', 'Deep validly nested text a.')]), + u('heading', { depth: 1 }, [u('text', 'Heading text.')]), + u('strong', [u('text', 'Deep validly nested text b.')]), + ]), + u('paragraph', [u('text', 'Validly nested text.')]), + ]); + + expect(transform(input)).toEqual(output); + }); + + it('should allow intermediate parents like list and table to contain required block children', () => { + const input = u('root', [ + u('blockquote', [ + u('list', [ + u('listItem', [ + u('table', [ + u('tableRow', [ + u('tableCell', [ + u('heading', { depth: 1 }, [u('text', 'Validly nested heading text.')]), + ]), + ]), + ]), + ]), + ]), + ]), + ]); + + const output = u('root', [ + u('blockquote', [ + u('list', [ + u('listItem', [ + u('table', [ + u('tableRow', [ + u('tableCell', [ + u('heading', { depth: 1 }, [u('text', 'Validly nested heading text.')]), + ]), + ]), + ]), + ]), + ]), + ]), + ]); + + expect(transform(input)).toEqual(output); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkEscapeMarkdownEntities.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkEscapeMarkdownEntities.spec.js new file mode 100644 index 000000000000..37ff0a85d36f --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkEscapeMarkdownEntities.spec.js @@ -0,0 +1,84 @@ +import unified from 'unified'; +import u from 'unist-builder'; + +import remarkEscapeMarkdownEntities from '../remarkEscapeMarkdownEntities'; + +function process(text) { + const tree = u('root', [u('text', text)]); + const escapedMdast = unified().use(remarkEscapeMarkdownEntities).runSync(tree); + + return escapedMdast.children[0].value; +} + +describe('remarkEscapeMarkdownEntities', () => { + it('should escape common markdown entities', () => { + expect(process('*a*')).toEqual('\\*a\\*'); + expect(process('**a**')).toEqual('\\*\\*a\\*\\*'); + expect(process('***a***')).toEqual('\\*\\*\\*a\\*\\*\\*'); + expect(process('_a_')).toEqual('\\_a\\_'); + expect(process('__a__')).toEqual('\\_\\_a\\_\\_'); + expect(process('~~a~~')).toEqual('\\~\\~a\\~\\~'); + expect(process('[]')).toEqual('\\[]'); + expect(process('[]()')).toEqual('\\[]()'); + expect(process('[a](b)')).toEqual('\\[a](b)'); + expect(process('[Test sentence.](https://www.example.com)')).toEqual( + '\\[Test sentence.](https://www.example.com)', + ); + expect(process('![a](b)')).toEqual('!\\[a](b)'); + }); + + it('should not escape inactive, single markdown entities', () => { + expect(process('a*b')).toEqual('a*b'); + expect(process('_')).toEqual('_'); + expect(process('~')).toEqual('~'); + expect(process('[')).toEqual('['); + }); + + it('should escape leading markdown entities', () => { + expect(process('#')).toEqual('\\#'); + expect(process('-')).toEqual('\\-'); + expect(process('*')).toEqual('\\*'); + expect(process('>')).toEqual('\\>'); + expect(process('=')).toEqual('\\='); + expect(process('|')).toEqual('\\|'); + expect(process('```')).toEqual('\\`\\``'); + expect(process(' ')).toEqual('\\ '); + }); + + it('should escape leading markdown entities preceded by whitespace', () => { + expect(process('\n #')).toEqual('\\#'); + expect(process(' \n-')).toEqual('\\-'); + }); + + it('should not escape leading markdown entities preceded by non-whitespace characters', () => { + expect(process('a# # b #')).toEqual('a# # b #'); + expect(process('a- - b -')).toEqual('a- - b -'); + }); + + it('should not escape html tags', () => { + expect(process('')).toEqual(''); + expect(process('a b e')).toEqual('a b e'); + }); + + it('should escape the contents of html blocks', () => { + expect(process('
*a*
')).toEqual('
\\*a\\*
'); + }); + + it('should not escape the contents of preformatted html blocks', () => { + expect(process('
*a*
')).toEqual('
*a*
'); + expect(process('')).toEqual(''); + expect(process('')).toEqual(''); + expect(process('
\n*a*\n
')).toEqual('
\n*a*\n
'); + expect(process('a b
*c*
d e')).toEqual('a b
*c*
d e'); + }); + + it('should not escape footnote references', () => { + expect(process('[^a]')).toEqual('[^a]'); + expect(process('[^1]')).toEqual('[^1]'); + }); + + it('should not escape footnotes', () => { + expect(process('[^a]:')).toEqual('[^a]:'); + expect(process('[^1]:')).toEqual('[^1]:'); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPaddedLinks.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPaddedLinks.spec.js new file mode 100644 index 000000000000..0d5cf4abdf22 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPaddedLinks.spec.js @@ -0,0 +1,43 @@ +import unified from 'unified'; +import markdownToRemark from 'remark-parse'; +import remarkToMarkdown from 'remark-stringify'; + +import remarkPaddedLinks from '../remarkPaddedLinks'; + +function input(markdown) { + return unified() + .use(markdownToRemark) + .use(remarkPaddedLinks) + .use(remarkToMarkdown) + .processSync(markdown).contents; +} + +function output(markdown) { + return unified().use(markdownToRemark).use(remarkToMarkdown).processSync(markdown).contents; +} + +describe('remarkPaddedLinks', () => { + it('should move leading and trailing spaces outside of a link', () => { + expect(input('[ a ](b)')).toEqual(output(' [a](b) ')); + }); + + it('should convert multiple leading or trailing spaces to a single space', () => { + expect(input('[ a ](b)')).toEqual(output(' [a](b) ')); + }); + + it('should work with only a leading space or only a trailing space', () => { + expect(input('[ a](b)[c ](d)')).toEqual(output(' [a](b)[c](d) ')); + }); + + it('should work for nested links', () => { + expect(input('* # a[ b ](c)d')).toEqual(output('* # a [b](c) d')); + }); + + it('should work for parents with multiple links that are not siblings', () => { + expect(input('# a[ b ](c)d **[ e ](f)**')).toEqual(output('# a [b](c) d ** [e](f) **')); + }); + + it('should work for links with arbitrarily nested children', () => { + expect(input('[ a __*b*__ _c_ ](d)')).toEqual(output(' [a __*b*__ _c_](d) ')); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPlugins.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPlugins.spec.js new file mode 100644 index 000000000000..83ccf907e8d9 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkPlugins.spec.js @@ -0,0 +1,299 @@ +import visit from 'unist-util-visit'; + +import { markdownToRemark, remarkToMarkdown } from '..'; + +describe('registered remark plugins', () => { + function withNetlifyLinks() { + return function transformer(tree) { + visit(tree, 'link', function onLink(node) { + node.url = 'https://netlify.com'; + }); + }; + } + + it('should use remark transformer plugins when converting mdast to markdown', () => { + const plugins = [withNetlifyLinks]; + const result = remarkToMarkdown( + { + type: 'root', + children: [ + { + type: 'paragraph', + children: [ + { + type: 'text', + value: 'Some ', + }, + { + type: 'emphasis', + children: [ + { + type: 'text', + value: 'important', + }, + ], + }, + { + type: 'text', + value: ' text with ', + }, + { + type: 'link', + title: null, + url: 'https://this-value-should-be-replaced.com', + children: [ + { + type: 'text', + value: 'a link', + }, + ], + }, + { + type: 'text', + value: ' in it.', + }, + ], + }, + ], + }, + plugins, + ); + expect(result).toMatchInlineSnapshot( + `"Some *important* text with [a link](https://netlify.com) in it."`, + ); + }); + + it('should use remark transformer plugins when converting markdown to mdast', () => { + const plugins = [withNetlifyLinks]; + const result = markdownToRemark( + 'Some text with [a link](https://this-value-should-be-replaced.com) in it.', + plugins, + ); + expect(result).toMatchInlineSnapshot(` +Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [], + "position": Position { + "end": Object { + "column": 16, + "line": 1, + "offset": 15, + }, + "indent": Array [], + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "text", + "value": "Some text with ", + }, + Object { + "children": Array [ + Object { + "children": Array [], + "position": Position { + "end": Object { + "column": 23, + "line": 1, + "offset": 22, + }, + "indent": Array [], + "start": Object { + "column": 17, + "line": 1, + "offset": 16, + }, + }, + "type": "text", + "value": "a link", + }, + ], + "position": Position { + "end": Object { + "column": 67, + "line": 1, + "offset": 66, + }, + "indent": Array [], + "start": Object { + "column": 16, + "line": 1, + "offset": 15, + }, + }, + "title": null, + "type": "link", + "url": "https://netlify.com", + }, + Object { + "children": Array [], + "position": Position { + "end": Object { + "column": 74, + "line": 1, + "offset": 73, + }, + "indent": Array [], + "start": Object { + "column": 67, + "line": 1, + "offset": 66, + }, + }, + "type": "text", + "value": " in it.", + }, + ], + "position": Position { + "end": Object { + "column": 74, + "line": 1, + "offset": 73, + }, + "indent": Array [], + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "paragraph", + }, + ], + "position": Object { + "end": Object { + "column": 74, + "line": 1, + "offset": 73, + }, + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "root", +} +`); + }); + + it('should use remark serializer plugins when converting mdast to markdown', () => { + function withEscapedLessThanChar() { + if (this.Compiler) { + this.Compiler.prototype.visitors.text = node => { + return node.value.replace(/ { + const settings = { + emphasis: '_', + bullet: '-', + }; + + const plugins = [{ settings }]; + const result = remarkToMarkdown( + { + type: 'root', + children: [ + { + type: 'paragraph', + children: [ + { + type: 'text', + value: 'Some ', + }, + { + type: 'emphasis', + children: [ + { + type: 'text', + value: 'important', + }, + ], + }, + { + type: 'text', + value: ' points:', + }, + ], + }, + { + type: 'list', + ordered: false, + start: null, + spread: false, + children: [ + { + type: 'listItem', + spread: false, + checked: null, + children: [ + { + type: 'paragraph', + children: [ + { + type: 'text', + value: 'One', + }, + ], + }, + ], + }, + { + type: 'listItem', + spread: false, + checked: null, + children: [ + { + type: 'paragraph', + children: [ + { + type: 'text', + value: 'Two', + }, + ], + }, + ], + }, + ], + }, + ], + }, + plugins, + ); + expect(result).toMatchInlineSnapshot(` +"Some _important_ points: + +- One +- Two" +`); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkShortcodes.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkShortcodes.spec.js new file mode 100644 index 000000000000..de17e092abfd --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkShortcodes.spec.js @@ -0,0 +1,145 @@ +import { Map, OrderedMap } from 'immutable'; +import unified from 'unified'; +import markdownToRemarkPlugin from 'remark-parse'; + +import { remarkParseShortcodes, getLinesWithOffsets } from '../remarkShortcodes'; + +function process(value, plugins) { + return unified() + .use(markdownToRemarkPlugin, { fences: true, commonmark: true }) + .use(remarkParseShortcodes, { plugins }) + .parse(value); +} + +function EditorComponent({ id = 'foo', fromBlock = jest.fn(), pattern }) { + return { + id, + fromBlock, + pattern, + }; +} + +describe('remarkParseShortcodes', () => { + describe('pattern matching', () => { + it('should match multiline shortcodes', () => { + const editorComponent = EditorComponent({ pattern: /^foo\nbar$/ }); + process('foo\nbar', Map({ [editorComponent.id]: editorComponent })); + expect(editorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['foo\nbar'])); + }); + it('should match multiline shortcodes with empty lines', () => { + const editorComponent = EditorComponent({ pattern: /^foo\n\nbar$/ }); + process('foo\n\nbar', Map({ [editorComponent.id]: editorComponent })); + expect(editorComponent.fromBlock).toHaveBeenCalledWith( + expect.arrayContaining(['foo\n\nbar']), + ); + }); + it('should match shortcodes based on order of occurrence in value', () => { + const fooEditorComponent = EditorComponent({ id: 'foo', pattern: /foo/ }); + const barEditorComponent = EditorComponent({ id: 'bar', pattern: /bar/ }); + process( + 'foo\n\nbar', + OrderedMap([ + [barEditorComponent.id, barEditorComponent], + [fooEditorComponent.id, fooEditorComponent], + ]), + ); + expect(fooEditorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['foo'])); + }); + it('should match shortcodes based on order of occurrence in value even when some use line anchors', () => { + const barEditorComponent = EditorComponent({ id: 'bar', pattern: /bar/ }); + const bazEditorComponent = EditorComponent({ id: 'baz', pattern: /^baz$/ }); + process( + 'foo\n\nbar\n\nbaz', + OrderedMap([ + [bazEditorComponent.id, bazEditorComponent], + [barEditorComponent.id, barEditorComponent], + ]), + ); + expect(barEditorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['bar'])); + }); + }); + describe('parse', () => { + describe('pattern with leading caret', () => { + it('should be a remark shortcode node', () => { + const editorComponent = EditorComponent({ + pattern: /^foo (?.+)$/, + fromBlock: ({ groups }) => ({ bar: groups.bar }), + }); + const mdast = process('foo baz', Map({ [editorComponent.id]: editorComponent })); + expect(removePositions(mdast)).toMatchSnapshot(); + }); + it('should parse multiple shortcodes', () => { + const editorComponent = EditorComponent({ + pattern: /foo (?.+)/, + fromBlock: ({ groups }) => ({ bar: groups.bar }), + }); + const mdast = process( + 'paragraph\n\nfoo bar\n\nfoo baz\n\nnext para', + Map({ [editorComponent.id]: editorComponent }), + ); + expect(removePositions(mdast)).toMatchSnapshot(); + }); + }); + describe('pattern without leading caret', () => { + it('should handle pattern without leading caret', () => { + const editorComponent = EditorComponent({ + pattern: /foo (?.+)/, + fromBlock: ({ groups }) => ({ bar: groups.bar }), + }); + const mdast = process( + 'paragraph\n\nfoo baz', + Map({ [editorComponent.id]: editorComponent }), + ); + expect(removePositions(mdast)).toMatchSnapshot(); + }); + it('should parse multiple shortcodes', () => { + const editorComponent = EditorComponent({ + pattern: /foo (?.+)/, + fromBlock: ({ groups }) => ({ bar: groups.bar }), + }); + const mdast = process( + 'paragraph\n\nfoo bar\n\nfoo baz\n\nnext para', + Map({ [editorComponent.id]: editorComponent }), + ); + expect(removePositions(mdast)).toMatchSnapshot(); + }); + }); + }); + + function removePositions(obj) { + if (Array.isArray(obj)) { + return obj.map(removePositions); + } + if (obj && typeof obj === 'object') { + // eslint-disable-next-line no-unused-vars + const { position, ...rest } = obj; + const result = {}; + for (const key in rest) { + result[key] = removePositions(rest[key]); + } + return result; + } + return obj; + } +}); + +describe('getLinesWithOffsets', () => { + test('should split into lines', () => { + const value = ' line1\n\nline2 \n\n line3 \n\n'; + + const lines = getLinesWithOffsets(value); + expect(lines).toEqual([ + { line: ' line1', start: 0 }, + { line: 'line2', start: 8 }, + { line: ' line3', start: 16 }, + { line: '', start: 30 }, + ]); + }); + + test('should return single item on no match', () => { + const value = ' line1 '; + + const lines = getLinesWithOffsets(value); + expect(lines).toEqual([{ line: ' line1', start: 0 }]); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkSlate.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkSlate.spec.js new file mode 100644 index 000000000000..d55f4416c7df --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkSlate.spec.js @@ -0,0 +1,67 @@ +import { mergeAdjacentTexts } from '../remarkSlate'; +describe('remarkSlate', () => { + describe('mergeAdjacentTexts', () => { + it('should handle empty array', () => { + const children = []; + expect(mergeAdjacentTexts(children)).toBe(children); + }); + + it('should merge adjacent texts with same marks', () => { + const children = [ + { text: '
', marks: [] }, + { text: 'Netlify', marks: [] }, + { text: '', marks: [] }, + ]; + + expect(mergeAdjacentTexts(children)).toEqual([ + { + text: 'Netlify', + marks: [], + }, + ]); + }); + + it('should not merge adjacent texts with different marks', () => { + const children = [ + { text: '', marks: [] }, + { text: 'Netlify', marks: ['b'] }, + { text: '', marks: [] }, + ]; + + expect(mergeAdjacentTexts(children)).toEqual(children); + }); + + it('should handle mixed children array', () => { + const children = [ + { object: 'inline' }, + { text: '', marks: [] }, + { text: 'Netlify', marks: [] }, + { text: '', marks: [] }, + { object: 'inline' }, + { text: '', marks: [] }, + { text: 'Netlify', marks: ['b'] }, + { text: '', marks: [] }, + { text: '', marks: [] }, + { object: 'inline' }, + { text: '', marks: [] }, + ]; + + expect(mergeAdjacentTexts(children)).toEqual([ + { object: 'inline' }, + { + text: 'Netlify', + marks: [], + }, + { object: 'inline' }, + { text: '', marks: [] }, + { text: 'Netlify', marks: ['b'] }, + { + text: '', + marks: [], + }, + { object: 'inline' }, + { text: '', marks: [] }, + ]); + }); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkStripTrailingBreaks.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkStripTrailingBreaks.spec.js new file mode 100644 index 000000000000..67ff12b9689a --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkStripTrailingBreaks.spec.js @@ -0,0 +1,23 @@ +import unified from 'unified'; +import u from 'unist-builder'; + +import remarkStripTrailingBreaks from '../remarkStripTrailingBreaks'; + +function process(children) { + const tree = u('root', children); + const strippedMdast = unified().use(remarkStripTrailingBreaks).runSync(tree); + + return strippedMdast.children; +} + +describe('remarkStripTrailingBreaks', () => { + it('should remove trailing breaks at the end of a block', () => { + expect(process([u('break')])).toEqual([]); + expect(process([u('break'), u('text', '\n \n')])).toEqual([u('text', '\n \n')]); + expect(process([u('text', 'a'), u('break')])).toEqual([u('text', 'a')]); + }); + + it('should not remove trailing breaks that are not at the end of a block', () => { + expect(process([u('break'), u('text', 'a')])).toEqual([u('break'), u('text', 'a')]); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/slate.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/slate.spec.js new file mode 100644 index 000000000000..eae9b8e0f060 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/slate.spec.js @@ -0,0 +1,300 @@ +/** @jsx h */ + +import flow from 'lodash/flow'; + +import h from '../../../test-helpers/h'; +import { markdownToSlate, slateToMarkdown } from '../index'; + +const process = flow([markdownToSlate, slateToMarkdown]); + +describe('slate', () => { + it('should not decode encoded html entities in inline code', () => { + expect(process('<div>')).toEqual( + '<div>', + ); + }); + + it('should parse non-text children of mark nodes', () => { + expect(process('**a[b](c)d**')).toEqual('**a[b](c)d**'); + expect(process('**[a](b)**')).toEqual('**[a](b)**'); + expect(process('**![a](b)**')).toEqual('**![a](b)**'); + expect(process('_`a`_')).toEqual('*`a`*'); + }); + + it('should handle unstyled code nodes adjacent to styled code nodes', () => { + expect(process('`foo`***`bar`***')).toEqual('`foo`***`bar`***'); + }); + + it('should handle styled code nodes adjacent to non-code text', () => { + expect(process('_`a`b_')).toEqual('*`a`b*'); + expect(process('_`a`**b**_')).toEqual('*`a`**b***'); + }); + + it('should condense adjacent, identically styled text and inline nodes', () => { + expect(process('**a ~~b~~~~c~~**')).toEqual('**a ~~bc~~**'); + expect(process('**a ~~b~~~~[c](d)~~**')).toEqual('**a ~~b[c](d)~~**'); + }); + + it('should handle nested markdown entities', () => { + expect(process('**a**b**c**')).toEqual('**a**b**c**'); + expect(process('**a _b_ c**')).toEqual('**a *b* c**'); + expect(process('*`a`*')).toEqual('*`a`*'); + }); + + it('should parse inline images as images', () => { + expect(process('a ![b](c)')).toEqual('a ![b](c)'); + }); + + it('should not escape markdown entities in html', () => { + expect(process('*')).toEqual('*'); + }); + + it('should wrap break tags in surrounding marks', () => { + expect(process('*a \nb*')).toEqual('*a\\\nb*'); + }); + + // slateAst no longer valid + + it('should not output empty headers in markdown', () => { + // prettier-ignore + const slateAst = ( + + + foo + + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"foo"`); + }); + + it('should not output empty marks in markdown', () => { + // prettier-ignore + const slateAst = ( + + + + foobar + baz + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"foobarbaz"`); + }); + + it('should not produce invalid markdown when a styled block has trailing whitespace', () => { + // prettier-ignore + const slateAst = ( + + + foo bar bim bam + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"**foo** bar **bim *bam***"`); + }); + + it('should not produce invalid markdown when a styled block has leading whitespace', () => { + // prettier-ignore + const slateAst = ( + + + foo bar + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"foo **bar**"`); + }); + + it('should group adjacent marks into a single mark when possible', () => { + // prettier-ignore + const slateAst = ( + + + shared mark + + link + + {' '} + not shared mark + + another + link + + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot( + `"**shared mark*[link](link)*** **not shared mark***[another **link**](link)*"`, + ); + }); + + describe('links', () => { + it('should handle inline code in link content', () => { + // prettier-ignore + const slateAst = ( + + + + foo + + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"[\`foo\`](link)"`); + }); + }); + + describe('code marks', () => { + it('can contain other marks', () => { + // prettier-ignore + const slateAst = ( + + + foo + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"***\`foo\`***"`); + }); + + it('can be condensed when no other marks are present', () => { + // prettier-ignore + const slateAst = ( + + + foo + bar + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"\`foo\`"`); + }); + }); + + describe('with nested styles within a single word', () => { + it('should not produce invalid markdown when a bold word has italics applied to a smaller part', () => { + // prettier-ignore + const slateAst = ( + + + h + e + y + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"**h*e*y**"`); + }); + + it('should not produce invalid markdown when an italic word has bold applied to a smaller part', () => { + // prettier-ignore + const slateAst = ( + + + h + e + y + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"*h**e**y*"`); + }); + + it('should handle italics inside bold inside strikethrough', () => { + // prettier-ignore + const slateAst = ( + + + h + e + l + l + o + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"~~h**e*l*l**o~~"`); + }); + + it('should handle bold inside italics inside strikethrough', () => { + // prettier-ignore + const slateAst = ( + + + h + e + l + l + o + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"~~h*e**l**l*o~~"`); + }); + + it('should handle strikethrough inside italics inside bold', () => { + // prettier-ignore + const slateAst = ( + + + h + e + l + l + o + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"**h*e~~l~~l*o**"`); + }); + + it('should handle italics inside strikethrough inside bold', () => { + // prettier-ignore + const slateAst = ( + + + h + e + l + l + o + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"**h~~e*l*l~~o**"`); + }); + + it('should handle strikethrough inside bold inside italics', () => { + // prettier-ignore + const slateAst = ( + + + h + e + l + l + o + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"*h**e~~l~~l**o*"`); + }); + + it('should handle bold inside strikethrough inside italics', () => { + // prettier-ignore + const slateAst = ( + + + h + e + l + l + o + + + ); + expect(slateToMarkdown(slateAst.children)).toMatchInlineSnapshot(`"*h~~e**l**l~~o*"`); + }); + }); +}); diff --git a/packages/decap-cms-widget-richtext/src/serializers/index.js b/packages/decap-cms-widget-richtext/src/serializers/index.js index 7736dc4bc2cc..fc61151675e5 100644 --- a/packages/decap-cms-widget-richtext/src/serializers/index.js +++ b/packages/decap-cms-widget-richtext/src/serializers/index.js @@ -60,7 +60,7 @@ import slateToRemark from './slateRemark'; /** * Deserialize a Markdown string to an MDAST. */ -export function markdownToRemark(markdown, remarkPlugins, editorComponents) { +export function markdownToRemark(markdown, remarkPlugins = [], editorComponents = Map()) { const processor = unified() .use(markdownToRemarkPlugin, { fences: true, commonmark: true }) .use(markdownToRemarkRemoveTokenizers, { inlineTokenizers: ['url'] }) @@ -205,7 +205,7 @@ export function htmlToSlate(html) { */ export function markdownToSlate( markdown, - { voidCodeBlock, remarkPlugins = [], editorComponents } = {}, + { voidCodeBlock, remarkPlugins = [], editorComponents = Map() } = {}, ) { const mdast = markdownToRemark(markdown, remarkPlugins, editorComponents); From 50c22a918b21d6b5bfa511533a6fbdc6e41c83c0 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Wed, 10 Dec 2025 14:55:35 +0100 Subject: [PATCH 33/43] fix(widget-richtext): codeblock & basic elements - pass voidCodeBlock to serializers - change elements for rendering paragraphs and headings --- .../src/RichtextControl/VisualEditor.js | 10 +++++++--- .../components/Element/HeadingElement.js | 9 +++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index 0267a04a55ba..ff054d3bca16 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -76,12 +76,16 @@ export default function VisualEditor(props) { function handleChange({ value }) { // console.log('handleChange', value); - const mdValue = slateToMarkdown(value, {}, editorComponents); + const mdValue = slateToMarkdown( + value, + { voidCodeBlock: !!codeBlockComponent }, + editorComponents, + ); onChange(mdValue); } const initialValue = props.value - ? markdownToSlate(props.value, { editorComponents }) + ? markdownToSlate(props.value, { editorComponents, voidCodeBlock: !!codeBlockComponent }) : emptyValue; const editor = usePlateEditor({ @@ -90,7 +94,7 @@ export default function VisualEditor(props) { [BoldPlugin.key]: withProps(PlateLeaf, { as: 'b' }), [CodePlugin.key]: CodeLeaf, [ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }), - [ParagraphPlugin.key]: ParagraphElement, + [ParagraphPlugin.key]: withProps(ParagraphElement, { as: 'p' }), [KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }), [KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }), [KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }), diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/HeadingElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/HeadingElement.js index 975b34800f41..8c4e025bd938 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/HeadingElement.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/HeadingElement.js @@ -1,5 +1,4 @@ import React from 'react'; -import { PlateElement } from 'platejs/react'; import styled from '@emotion/styled'; const headingVariants = { @@ -39,11 +38,9 @@ function HeadingElement({ variant = 'h1', children, ...props }) { const { element, editor } = props; const isFirstBlock = element === editor.children[0]; return ( - - - {children} - - + + {children} + ); } From dfe76787acf33d597a4fde7d9caff0cf0bfab13d Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Thu, 11 Dec 2025 09:27:00 +0100 Subject: [PATCH 34/43] feat(widget-richtext): handle keyboard shortcuts for links, headings and strikethrough --- .../src/RichtextControl/VisualEditor.js | 40 +++++++++++++++---- .../components/Leaf/CodeLeaf.js | 5 +-- .../components/Toolbar/LinkToolbarButton.js | 10 +---- .../src/RichtextControl/linkHandler.js | 10 +++++ 4 files changed, 46 insertions(+), 19 deletions(-) create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/linkHandler.js diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index ff054d3bca16..71bfbf80efc4 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -1,7 +1,13 @@ import React, { useEffect } from 'react'; import { KEYS } from 'platejs'; import { usePlateEditor, Plate, ParagraphPlugin, PlateLeaf } from 'platejs/react'; -import { BoldPlugin, ItalicPlugin, CodePlugin, HeadingPlugin } from '@platejs/basic-nodes/react'; +import { + BoldPlugin, + ItalicPlugin, + CodePlugin, + HeadingPlugin, + StrikethroughPlugin, +} from '@platejs/basic-nodes/react'; import { ListPlugin } from '@platejs/list-classic/react'; import { LinkPlugin } from '@platejs/link/react'; import { ClassNames, css } from '@emotion/react'; @@ -23,6 +29,7 @@ import ExtendedBlockquotePlugin from './plugins/ExtendedBlockquotePlugin'; import ShortcodePlugin from './plugins/ShortcodePlugin'; import defaultEmptyBlock from './defaultEmptyBlock'; import { mergeMediaConfig } from './mergeMediaConfig'; +import { handleLinkClick } from './linkHandler'; function editorStyles({ minimal }) { return css` @@ -66,10 +73,6 @@ export default function VisualEditor(props) { console.log('handleBlockClick'); } - function handleLinkClick() { - console.log('handleLinkClick'); - } - function handleToggleMode() { onMode('raw'); } @@ -94,6 +97,7 @@ export default function VisualEditor(props) { [BoldPlugin.key]: withProps(PlateLeaf, { as: 'b' }), [CodePlugin.key]: CodeLeaf, [ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }), + [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: 's' }), [ParagraphPlugin.key]: withProps(ParagraphElement, { as: 'p' }), [KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }), [KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }), @@ -108,13 +112,35 @@ export default function VisualEditor(props) { }, plugins: [ ParagraphPlugin, - HeadingPlugin, + HeadingPlugin.configure({ + shortcuts: { + h1: { keys: 'mod+1', handler: () => editor.tf.toggleBlock('h1') }, + h2: { keys: 'mod+2', handler: () => editor.tf.toggleBlock('h2') }, + h3: { keys: 'mod+3', handler: () => editor.tf.toggleBlock('h3') }, + h4: { keys: 'mod+4', handler: () => editor.tf.toggleBlock('h4') }, + h5: { keys: 'mod+5', handler: () => editor.tf.toggleBlock('h5') }, + h6: { keys: 'mod+6', handler: () => editor.tf.toggleBlock('h6')}, + }, + }), BoldPlugin, ItalicPlugin, - CodePlugin, + StrikethroughPlugin.configure({ + shortcuts: { toggle: { keys: 'mod+shift+s' } }, + }), + CodePlugin.configure({ + shortcuts: { toggle: { keys: 'mod+shift+c' } }, + }), ListPlugin, LinkPlugin.configure({ node: { component: LinkElement }, + shortcuts: { + toggleLink: { + keys: 'mod+k', + handler: () => { + handleLinkClick({ editor, t }); + }, + }, + }, }), ExtendedBlockquotePlugin.configure({ node: { component: BlockquoteElement }, diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Leaf/CodeLeaf.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Leaf/CodeLeaf.js index 37545a8a78a6..6d4c4d9cdade 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Leaf/CodeLeaf.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Leaf/CodeLeaf.js @@ -1,7 +1,6 @@ import React from 'react'; import styled from '@emotion/styled'; import { colors, lengths } from 'decap-cms-ui-default'; -import { PlateLeaf } from 'platejs/react'; const StyledCode = styled.code` background-color: ${colors.background}; @@ -12,9 +11,7 @@ const StyledCode = styled.code` function CodeLeaf({ children, ...props }) { return ( - - {children} - + {children} ); } diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/LinkToolbarButton.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/LinkToolbarButton.js index 5b11df72bc6b..000c605a38bc 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/LinkToolbarButton.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Toolbar/LinkToolbarButton.js @@ -1,9 +1,9 @@ import React from 'react'; import { useEditorRef } from 'platejs/react'; import { useLinkToolbarButton, useLinkToolbarButtonState } from '@platejs/link/react'; -import { unwrapLink, upsertLink } from '@platejs/link'; import ToolbarButton from './ToolbarButton'; +import { handleLinkClick } from '../../linkHandler'; function LinkToolbarButton({ t, ...rest }) { const state = useLinkToolbarButtonState(); @@ -14,13 +14,7 @@ function LinkToolbarButton({ t, ...rest }) { const editor = useEditorRef(); function handleClick() { - const url = window.prompt(t('editor.editorWidgets.markdown.linkPrompt'), ''); - - if (url) { - upsertLink(editor, { url, skipValidation: true }); - } else if (url == '') { - unwrapLink(editor); - } + handleLinkClick({ editor, t }); } return ; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/linkHandler.js b/packages/decap-cms-widget-richtext/src/RichtextControl/linkHandler.js new file mode 100644 index 000000000000..41e765b06ac8 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/linkHandler.js @@ -0,0 +1,10 @@ +import { unwrapLink, upsertLink } from '@platejs/link'; + +export function handleLinkClick({ editor, t }) { + const url = window.prompt(t('editor.editorWidgets.markdown.linkPrompt'), ''); + if (url) { + upsertLink(editor, { url, skipValidation: true }); + } else if (url == '') { + unwrapLink(editor); + } +} From 86774c5eb558818e76a43e9bd6b3c07f9417a782 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Thu, 11 Dec 2025 10:01:12 +0100 Subject: [PATCH 35/43] test(widget-richtext): add 6/8 e2e tests copied and adapted from markdown (wip) --- cypress/e2e/richtext_widget_backspace_spec.js | 96 ++++++++++++++ .../e2e/richtext_widget_code_block_spec.js | 117 ++++++++++++++++++ cypress/e2e/richtext_widget_enter_spec.js | 113 +++++++++++++++++ cypress/e2e/richtext_widget_hotkeys_spec.js | 107 ++++++++++++++++ cypress/e2e/richtext_widget_link_spec.js | 71 +++++++++++ cypress/e2e/richtext_widget_marks_spec.js | 37 ++++++ cypress/plugins/index.js | 19 +++ 7 files changed, 560 insertions(+) create mode 100644 cypress/e2e/richtext_widget_backspace_spec.js create mode 100644 cypress/e2e/richtext_widget_code_block_spec.js create mode 100644 cypress/e2e/richtext_widget_enter_spec.js create mode 100644 cypress/e2e/richtext_widget_hotkeys_spec.js create mode 100644 cypress/e2e/richtext_widget_link_spec.js create mode 100644 cypress/e2e/richtext_widget_marks_spec.js diff --git a/cypress/e2e/richtext_widget_backspace_spec.js b/cypress/e2e/richtext_widget_backspace_spec.js new file mode 100644 index 000000000000..9a8076dace22 --- /dev/null +++ b/cypress/e2e/richtext_widget_backspace_spec.js @@ -0,0 +1,96 @@ +import '../utils/dismiss-local-backup'; + +describe('Markdown widget', () => { + + before(() => { + Cypress.config('defaultCommandTimeout', 4000); + cy.task('setupBackend', { backend: 'test' }); + cy.task('useRichTextWidget'); + }); + + beforeEach(() => { + cy.loginAndNewPost(); + cy.clearMarkdownEditorContent(); + }); + + after(() => { + cy.task('teardownBackend', { backend: 'test' }); + }); + + // describe('pressing backspace', () => { + it('sets non-default block to default when empty', () => { + cy.focused() + .clickHeadingOneButton() + .backspace() + .confirmMarkdownEditorContent(` +

+ `); + }); + it('moves to previous block when no character left to delete', () => { + cy.focused() + .type('foo') + .enter() + .clickHeadingOneButton() + .type('a') + .backspace({times: 2}) + .confirmMarkdownEditorContent(` +

foo

+ `); + }); + // behaviour change: resets the block to default + it('does nothing at start of first block in document when non-empty and non-default', () => { + cy.focused() + .clickHeadingOneButton() + .type('foo') + .setCursorBefore('foo') + .backspace({ times: 4 }) + .confirmMarkdownEditorContent(` +

foo

+ `); + }); + // behaviour change: also resets the block to default (as in previous test) + it('deletes individual characters in middle of non-empty non-default block in document', () => { + cy.focused() + .clickHeadingOneButton() + .type('foo') + .setCursorAfter('fo') + .backspace({ times: 3 }) + .confirmMarkdownEditorContent(` +

o

+ `); + }); + it('at beginning of non-first block, moves default block content to previous block', () => { + cy.focused() + .clickHeadingOneButton() + .type('foo') + .enter() + .type('bar') + .setCursorBefore('bar') + .backspace() + .confirmMarkdownEditorContent(` +

foobar

+ `); + }); + it('at beginning of non-first block, moves non-default block content to previous block', () => { + cy.focused() + .type('foo') + .enter() + .clickHeadingOneButton() + .type('bar') + .enter() + .clickHeadingTwoButton() + .type('baz') + .setCursorBefore('baz') + .backspace() + .confirmMarkdownEditorContent(` +

foo

+

barbaz

+ `) + .setCursorBefore('bar') + .backspace() + .confirmMarkdownEditorContent(` +

foobarbaz

+ `); + // }); + }); +}); diff --git a/cypress/e2e/richtext_widget_code_block_spec.js b/cypress/e2e/richtext_widget_code_block_spec.js new file mode 100644 index 000000000000..366e81eb684a --- /dev/null +++ b/cypress/e2e/richtext_widget_code_block_spec.js @@ -0,0 +1,117 @@ +import { oneLineTrim, stripIndent } from 'common-tags'; +import '../utils/dismiss-local-backup'; + +describe('Markdown widget code block', () => { + before(() => { + Cypress.config('defaultCommandTimeout', 4000); + cy.task('setupBackend', { backend: 'test' }); + cy.task('useRichTextWidget'); + }); + + beforeEach(() => { + cy.loginAndNewPost(); + cy.clearMarkdownEditorContent(); + }); + + after(() => { + cy.task('teardownBackend', { backend: 'test' }); + }); + describe('code block', () => { + // behaviour change: changes how the raw editor is rendered - single block mode + it('outputs code', () => { + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy + .insertCodeBlock() + .type('foo') + .enter() + .type('bar') + .confirmMarkdownEditorContent( + ` + ${codeBlock(` + foo + bar + `)} + `, + ) + .wait(500) + .clickModeToggle().confirmRawEditorContent('``` foo bar ```'); + }); + }); +}); + +function codeBlock(content) { + const lines = stripIndent(content) + .split('\n') + .map( + (line, idx) => ` +
+
+
${idx + 1}
+
+
${line}
+
+ `, + ) + .join(''); + + return oneLineTrim` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
xxxxxxxxxx
+
+
+
+
+
 
+
+
+ ${lines} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +  + + +
+
+
+
+ `; +} diff --git a/cypress/e2e/richtext_widget_enter_spec.js b/cypress/e2e/richtext_widget_enter_spec.js new file mode 100644 index 000000000000..b3230491990f --- /dev/null +++ b/cypress/e2e/richtext_widget_enter_spec.js @@ -0,0 +1,113 @@ +import '../utils/dismiss-local-backup'; + +describe('Richtext widget breaks', () => { + before(() => { + Cypress.config('defaultCommandTimeout', 4000); + cy.task('setupBackend', { backend: 'test' }); + cy.task('useRichTextWidget'); + }); + + beforeEach(() => { + cy.loginAndNewPost(); + cy.clearMarkdownEditorContent(); + }); + + after(() => { + cy.task('teardownBackend', { backend: 'test' }); + }); + + describe('pressing enter', () => { + it('creates new default block from empty block', () => { + cy.focused() + .enter() + .confirmMarkdownEditorContent(` +

+

+ `); + }); + it('creates new default block when selection collapsed at end of block', () => { + cy.focused() + .type('foo') + .enter() + .confirmMarkdownEditorContent(` +

foo

+

+ `); + }); + it('creates new default block when selection collapsed at end of non-default block', () => { + cy.clickHeadingOneButton() + .type('foo') + .enter() + .confirmMarkdownEditorContent(` +

foo

+

+ `); + }); + // behaviour change: plate now creates the new default block before the non-default block + it('creates new default block when selection collapsed in empty non-default block', () => { + cy.clickHeadingOneButton() + .enter() + .confirmMarkdownEditorContent(` +

+

+ `); + }); + // behaviour change: plate now creates the new default block before the non-default block + it('splits block into two same-type blocks when collapsed selection at block start', () => { + cy.clickHeadingOneButton() + .type('foo') + .setCursorBefore('foo') + .enter() + .confirmMarkdownEditorContent(` +

+

foo

+ `); + }); + // behaviour change: plate now splits into default block + it('splits block into two same-type blocks when collapsed in middle of selection at block start', () => { + cy.clickHeadingOneButton() + .type('foo') + .setCursorBefore('oo') + .enter() + .confirmMarkdownEditorContent(` +

f

+

oo

+ `); + }); + it('deletes selected content and splits to same-type block when selection is expanded', () => { + cy.clickHeadingOneButton() + .type('foo bar') + .setSelection('o b') + .enter() + .confirmMarkdownEditorContent(` +

fo

+

ar

+ `); + }); + }); + + // skipped: cypress type event does not trigger actual keyboard event for the Plate to detect + describe.skip('pressing shift+enter', () => { + it('creates line break', () => { + cy.focused() + .enter({ shift: true }) + .confirmMarkdownEditorContent(` +

+ a
+

+ `); + }); + it('creates consecutive line break', () => { + cy.focused() + .enter({ shift: true, times: 4 }) + .confirmMarkdownEditorContent(` +

+
+
+
+
+

+ `); + }); + }); +}); diff --git a/cypress/e2e/richtext_widget_hotkeys_spec.js b/cypress/e2e/richtext_widget_hotkeys_spec.js new file mode 100644 index 000000000000..4adeccc15498 --- /dev/null +++ b/cypress/e2e/richtext_widget_hotkeys_spec.js @@ -0,0 +1,107 @@ +import '../utils/dismiss-local-backup'; +import {HOT_KEY_MAP} from "../utils/constants"; +const headingNumberToWord = ['', 'one', 'two', 'three', 'four', 'five', 'six']; +const isMac = Cypress.platform === 'darwin'; +const modifierKey = isMac ? '{meta}' : '{ctrl}'; +// eslint-disable-next-line func-style +const replaceMod = (str) => str.replace(/mod\+/g, modifierKey).replace(/shift\+/g, '{shift}'); + +describe('Markdown widget hotkeys', () => { + describe('hot keys', () => { + before(() => { + Cypress.config('defaultCommandTimeout', 4000); + cy.task('setupBackend', { backend: 'test' }); + cy.task('useRichTextWidget'); + }); + + beforeEach(() => { + cy.loginAndNewPost(); + cy.clearMarkdownEditorContent(); + cy.focused() + .type('foo') + .setSelection('foo').as('selection'); + }); + + after(() => { + cy.task('teardownBackend', { backend: 'test' }); + }); + + describe('bold', () => { + it('pressing mod+b bolds the text', () => { + cy.get('@selection') + .type(replaceMod(HOT_KEY_MAP['bold'])) + .confirmMarkdownEditorContent(` +

+ foo +

+ `) + .type(replaceMod(HOT_KEY_MAP['bold'])); + }); + }); + + describe('italic', () => { + it('pressing mod+i italicizes the text', () => { + cy.get('@selection') + .type(replaceMod(HOT_KEY_MAP['italic'])) + .confirmMarkdownEditorContent(` +

+ foo +

+ `) + .type(replaceMod(HOT_KEY_MAP['italic'])); + }); + }); + + describe('strikethrough', () => { + it('pressing mod+shift+s displays a strike through the text', () => { + cy.get('@selection') + .type(replaceMod(HOT_KEY_MAP['strikethrough'])) + .confirmMarkdownEditorContent(` +

+ foo +

+ `).type(replaceMod(HOT_KEY_MAP['strikethrough'])); + }); + }); + + describe('code', () => { + it('pressing mod+shift+c displays a code block around the text', () => { + cy.get('@selection') + .type(replaceMod(HOT_KEY_MAP['code'])) + .confirmMarkdownEditorContent(` +

+ foo +

+ `).type(replaceMod(HOT_KEY_MAP['code'])); + }); + }); + + describe('link', () => { + before(() => { + + }); + it('pressing mod+k transforms the text to a link', () => { + cy.window().then((win) => { + cy.get('@selection') + .type(replaceMod(HOT_KEY_MAP['link'])) + cy.stub(win, 'prompt').returns('https://google.com'); + cy.confirmMarkdownEditorContent('

foo

') + .type(replaceMod(HOT_KEY_MAP['link'])); + }); + + + }); + }); + + describe('headings', () => { + for (let i = 1; i <= 6; i++) { + it(`pressing mod+${i} transforms the text to a heading`, () => { + cy.get('@selection') + .type(replaceMod(HOT_KEY_MAP[`heading-${headingNumberToWord[i]}`])) + .confirmMarkdownEditorContent(`foo`) + .type(replaceMod(HOT_KEY_MAP[`heading-${headingNumberToWord[i]}`])) + }); + } + }); + }); +}); diff --git a/cypress/e2e/richtext_widget_link_spec.js b/cypress/e2e/richtext_widget_link_spec.js new file mode 100644 index 000000000000..3826b534a5dd --- /dev/null +++ b/cypress/e2e/richtext_widget_link_spec.js @@ -0,0 +1,71 @@ +import '../utils/dismiss-local-backup'; + +describe('Markdown widget link', () => { + before(() => { + Cypress.config('defaultCommandTimeout', 4000); + cy.task('setupBackend', { backend: 'test' }); + cy.task('useRichTextWidget'); + }); + + beforeEach(() => { + cy.loginAndNewPost(); + cy.clearMarkdownEditorContent(); + }); + + after(() => { + cy.task('teardownBackend', { backend: 'test' }); + }); + + describe('link', () => { + it('can add a new valid link', () => { + const link = 'https://www.decapcms.org/'; + cy.window().then(win => { + cy.stub(win, 'prompt').returns(link); + }); + cy.focused().clickLinkButton(); + + cy.confirmMarkdownEditorContent(`

${link}

`); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + cy.clickModeToggle(); + + cy.confirmRawEditorContent(`<${link}>`); + }); + + it('can add a new invalid link', () => { + const link = 'www.decapcms.org'; + cy.window().then(win => { + cy.stub(win, 'prompt').returns(link); + }); + cy.focused().clickLinkButton(); + + cy.confirmMarkdownEditorContent(`

${link}

`); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + cy.clickModeToggle(); + + cy.confirmRawEditorContent(`[${link}](${link})`); + }); + + it('can select existing text as link', () => { + const link = 'https://www.decapcms.org'; + cy.window().then(win => { + cy.stub(win, 'prompt').returns(link); + }); + + const text = 'Decap CMS'; + cy.focused() + .getMarkdownEditor() + .type(text) + .setSelection(text) + .clickLinkButton(); + + cy.confirmMarkdownEditorContent(`

${text}

`); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(300); + cy.clickModeToggle(); + + cy.confirmRawEditorContent(`[${text}](${link})`); + }); + }); +}); diff --git a/cypress/e2e/richtext_widget_marks_spec.js b/cypress/e2e/richtext_widget_marks_spec.js new file mode 100644 index 000000000000..f711cc1c5936 --- /dev/null +++ b/cypress/e2e/richtext_widget_marks_spec.js @@ -0,0 +1,37 @@ +import '../utils/dismiss-local-backup'; + +describe('Markdown widget', () => { + describe('code mark', () => { + before(() => { + Cypress.config('defaultCommandTimeout', 4000); + cy.task('setupBackend', { backend: 'test' }); + cy.task('useRichTextWidget'); + }); + + beforeEach(() => { + cy.loginAndNewPost(); + cy.clearMarkdownEditorContent(); + }); + + after(() => { + cy.task('teardownBackend', { backend: 'test' }); + }); + + describe('toolbar button', () => { + it('can combine code mark with other marks', () => { + cy.clickItalicButton() + .type('foo') + .setSelection('oo') + .clickCodeButton() + .confirmMarkdownEditorContent(` +

+ f + + oo + +

+ `); + }); + }); + }); +}); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index 13a6ab505495..10988a8ba14d 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -174,6 +174,25 @@ module.exports = async (on, config) => { return null; }, + async useRichTextWidget() { + console.log('Updating config to use richtext widget'); + await updateConfig(current => { + if (current.collections) { + current.collections = current.collections.map(collection => { + if (collection.fields) { + collection.fields = collection.fields.map(field => { + if (field.widget === 'markdown') { + return { ...field, widget: 'richtext' }; + } + return field; + }); + } + return collection; + }); + } + }); + return null; + }, }); addMatchImageSnapshotPlugin(on, config); From 586e718259f9c84aac8772ff8df43a99573c96c1 Mon Sep 17 00:00:00 2001 From: Anze Demsar Date: Thu, 11 Dec 2025 10:16:31 +0100 Subject: [PATCH 36/43] style(widget-richtext): lint code --- .../src/RichtextControl/VisualEditor.js | 2 +- .../src/RichtextControl/components/Leaf/CodeLeaf.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index 71bfbf80efc4..328e396628d8 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -119,7 +119,7 @@ export default function VisualEditor(props) { h3: { keys: 'mod+3', handler: () => editor.tf.toggleBlock('h3') }, h4: { keys: 'mod+4', handler: () => editor.tf.toggleBlock('h4') }, h5: { keys: 'mod+5', handler: () => editor.tf.toggleBlock('h5') }, - h6: { keys: 'mod+6', handler: () => editor.tf.toggleBlock('h6')}, + h6: { keys: 'mod+6', handler: () => editor.tf.toggleBlock('h6') }, }, }), BoldPlugin, diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Leaf/CodeLeaf.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Leaf/CodeLeaf.js index 6d4c4d9cdade..66fd0033e93c 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Leaf/CodeLeaf.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Leaf/CodeLeaf.js @@ -11,7 +11,9 @@ const StyledCode = styled.code` function CodeLeaf({ children, ...props }) { return ( - {children} + + {children} + ); } From b55ae26c7e5892c457e0003c05cae5846e3bd454 Mon Sep 17 00:00:00 2001 From: Martin Jagodic Date: Fri, 30 Jan 2026 08:31:21 +0100 Subject: [PATCH 37/43] feat: add table preview in visual editor (#8) * feat: add table preview in visual editor * fix: format --- .../src/MarkdownControl/renderers.js | 2 +- .../src/RichtextControl/VisualEditor.js | 4 +++ .../components/Element/TableCellElement.js | 18 ++++++++++ .../components/Element/TableElement.js | 18 ++++++++++ .../components/Element/TableRowElement.js | 11 ++++++ .../RichtextControl/plugins/TablePlugin.js | 36 +++++++++++++++++++ 6 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/TableCellElement.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/TableElement.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/TableRowElement.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/plugins/TablePlugin.js diff --git a/packages/decap-cms-widget-markdown/src/MarkdownControl/renderers.js b/packages/decap-cms-widget-markdown/src/MarkdownControl/renderers.js index 9bc0e4c810de..10b0fc0217e4 100644 --- a/packages/decap-cms-widget-markdown/src/MarkdownControl/renderers.js +++ b/packages/decap-cms-widget-markdown/src/MarkdownControl/renderers.js @@ -97,7 +97,7 @@ const StyledTable = styled.table` `; const StyledTd = styled.td` - border: 2px solid black; + border: 1px solid black; padding: 8px; text-align: left; `; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index 328e396628d8..d7ac5973b13c 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -27,6 +27,7 @@ import BlockquoteElement from './components/Element/BlockquoteElement'; import LinkElement from './components/Element/LinkElement'; import ExtendedBlockquotePlugin from './plugins/ExtendedBlockquotePlugin'; import ShortcodePlugin from './plugins/ShortcodePlugin'; +import { TablePlugin, TableRowPlugin, TableCellPlugin } from './plugins/TablePlugin'; import defaultEmptyBlock from './defaultEmptyBlock'; import { mergeMediaConfig } from './mergeMediaConfig'; import { handleLinkClick } from './linkHandler'; @@ -146,6 +147,9 @@ export default function VisualEditor(props) { node: { component: BlockquoteElement }, }), ShortcodePlugin, + TablePlugin, + TableRowPlugin, + TableCellPlugin, ], value: initialValue, }); diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/TableCellElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/TableCellElement.js new file mode 100644 index 000000000000..ee98decc5231 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/TableCellElement.js @@ -0,0 +1,18 @@ +import React from 'react'; +import styled from '@emotion/styled'; + +const StyledTd = styled.td` + border: 1px solid black; + padding: 8px; + text-align: left; +`; + +function TableCellElement({ children, attributes, nodeProps }) { + return ( + + {children} + + ); +} + +export default TableCellElement; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/TableElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/TableElement.js new file mode 100644 index 000000000000..9654e6a46238 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/TableElement.js @@ -0,0 +1,18 @@ +import React from 'react'; +import styled from '@emotion/styled'; + +const StyledTable = styled.table` + border-collapse: collapse; + margin-bottom: 16px; + width: 100%; +`; + +function TableElement({ children, attributes, nodeProps }) { + return ( + + {children} + + ); +} + +export default TableElement; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/TableRowElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/TableRowElement.js new file mode 100644 index 000000000000..bdc3d65b6a4c --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/TableRowElement.js @@ -0,0 +1,11 @@ +import React from 'react'; + +function TableRowElement({ children, attributes, nodeProps }) { + return ( + + {children} + + ); +} + +export default TableRowElement; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/TablePlugin.js b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/TablePlugin.js new file mode 100644 index 000000000000..8e291e52f411 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/TablePlugin.js @@ -0,0 +1,36 @@ +import { createSlatePlugin } from 'platejs'; +import { toPlatePlugin } from 'platejs/react'; + +import TableElement from '../components/Element/TableElement'; +import TableRowElement from '../components/Element/TableRowElement'; +import TableCellElement from '../components/Element/TableCellElement'; + +const tablePlugin = createSlatePlugin({ + key: 'table', + node: { + isElement: true, + component: TableElement, + }, +}); + +const tableRowPlugin = createSlatePlugin({ + key: 'table-row', + node: { + isElement: true, + component: TableRowElement, + }, +}); + +const tableCellPlugin = createSlatePlugin({ + key: 'table-cell', + node: { + isElement: true, + component: TableCellElement, + }, +}); + +const TablePlugin = toPlatePlugin(tablePlugin); +const TableRowPlugin = toPlatePlugin(tableRowPlugin); +const TableCellPlugin = toPlatePlugin(tableCellPlugin); + +export { TablePlugin, TableRowPlugin, TableCellPlugin }; From fbf9bb12897a1408f0fa1b995ba8726642731cb6 Mon Sep 17 00:00:00 2001 From: Martin Jagodic Date: Wed, 25 Feb 2026 09:30:47 +0100 Subject: [PATCH 38/43] Apply suggestions from code review Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com> --- packages/decap-cms-widget-markdown/src/serializers/index.js | 4 +--- .../src/RichtextControl/VisualEditor.js | 2 +- .../src/RichtextControl/components/Element/ListElement.js | 4 +--- .../src/RichtextControl/linkHandler.js | 2 +- .../src/RichtextControl/plugins/ExtendedBlockquotePlugin.js | 2 +- .../decap-cms-widget-richtext/src/__tests__/renderer.spec.js | 4 ++-- packages/decap-cms-widget-richtext/src/schema.js | 1 + packages/decap-cms-widget-richtext/src/serializers/index.js | 4 +--- 8 files changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/decap-cms-widget-markdown/src/serializers/index.js b/packages/decap-cms-widget-markdown/src/serializers/index.js index 9ba30ed116ea..4384ec4f1b11 100644 --- a/packages/decap-cms-widget-markdown/src/serializers/index.js +++ b/packages/decap-cms-widget-markdown/src/serializers/index.js @@ -220,10 +220,8 @@ export function markdownToSlate(markdown, { voidCodeBlock, remarkPlugins = [] } * trees. */ export function slateToMarkdown(raw, { voidCodeBlock, remarkPlugins = [] } = {}) { - console.log('old raw', raw); const mdast = slateToRemark(raw, { voidCodeBlock }); - console.log('old mdast', mdast); const markdown = remarkToMarkdown(mdast, remarkPlugins); - console.log('old md', markdown); + return markdown; } diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index d7ac5973b13c..f8374c922240 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -79,7 +79,7 @@ export default function VisualEditor(props) { } function handleChange({ value }) { - // console.log('handleChange', value); + const mdValue = slateToMarkdown( value, { voidCodeBlock: !!codeBlockComponent }, diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ListElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ListElement.js index 3224890c1c8a..acd3e05a6c56 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ListElement.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ListElement.js @@ -2,10 +2,8 @@ import React from 'react'; import styled from '@emotion/styled'; import { PlateElement } from 'platejs/react'; -const bottomMargin = '16px'; - const StyledList = styled.li` - margin-bottom: ${bottomMargin}; + margin-bottom: 16px; padding-left: 30px; `; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/linkHandler.js b/packages/decap-cms-widget-richtext/src/RichtextControl/linkHandler.js index 41e765b06ac8..1a962185947c 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/linkHandler.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/linkHandler.js @@ -4,7 +4,7 @@ export function handleLinkClick({ editor, t }) { const url = window.prompt(t('editor.editorWidgets.markdown.linkPrompt'), ''); if (url) { upsertLink(editor, { url, skipValidation: true }); - } else if (url == '') { + } else if (url === '') { unwrapLink(editor); } } diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ExtendedBlockquotePlugin.js b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ExtendedBlockquotePlugin.js index 2c034399bcc3..15bf6bb8c289 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ExtendedBlockquotePlugin.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ExtendedBlockquotePlugin.js @@ -8,7 +8,7 @@ function isWithinBlockquote(editor, entry) { } function queryNode(editor, entry, { empty, first, start, collapsed }) { - console.log('collapsed', editor.api.isCollapsed()); + return ( (!empty || editor.api.isEmpty(entry[1])) && (!first || !PathApi.hasPrevious(entry[1])) && diff --git a/packages/decap-cms-widget-richtext/src/__tests__/renderer.spec.js b/packages/decap-cms-widget-richtext/src/__tests__/renderer.spec.js index 074264c60ea6..94ee6cef1fe2 100644 --- a/packages/decap-cms-widget-richtext/src/__tests__/renderer.spec.js +++ b/packages/decap-cms-widget-richtext/src/__tests__/renderer.spec.js @@ -191,7 +191,7 @@ I get 10 times more traffic from [Google] than from [Yahoo] or [MSN]. }); describe('HTML sanitization', () => { - it('should sanitize HTML', async () => { + it('should sanitize HTML', () => { const value = ``; const field = Map({ sanitize_preview: true }); @@ -208,7 +208,7 @@ I get 10 times more traffic from [Google] than from [Yahoo] or [MSN]. expect(img).not.toHaveAttribute('onerror'); }); - it('should not sanitize HTML', async () => { + it('should not sanitize HTML', () => { const value = ``; const field = Map({ sanitize_preview: false }); diff --git a/packages/decap-cms-widget-richtext/src/schema.js b/packages/decap-cms-widget-richtext/src/schema.js index 19fda27d797f..146380792406 100644 --- a/packages/decap-cms-widget-richtext/src/schema.js +++ b/packages/decap-cms-widget-richtext/src/schema.js @@ -8,6 +8,7 @@ export default { enum: [ 'bold', 'italic', + 'strikethrough', 'code', 'link', 'heading-one', diff --git a/packages/decap-cms-widget-richtext/src/serializers/index.js b/packages/decap-cms-widget-richtext/src/serializers/index.js index fc61151675e5..474600b26efc 100644 --- a/packages/decap-cms-widget-richtext/src/serializers/index.js +++ b/packages/decap-cms-widget-richtext/src/serializers/index.js @@ -227,10 +227,8 @@ export function markdownToSlate( * trees. */ export function slateToMarkdown(raw, { voidCodeBlock, remarkPlugins = [] } = {}, editorComponents) { - console.log('new raw', raw); const mdast = slateToRemark(raw, { voidCodeBlock }, editorComponents); - console.log('new mdast', mdast); const markdown = remarkToMarkdown(mdast, remarkPlugins, editorComponents); - console.log('new md', markdown); + return markdown; } From 02f54af3e5cf0ebd59b91c28af9be0c022904631 Mon Sep 17 00:00:00 2001 From: Martin Jagodic Date: Wed, 25 Feb 2026 09:32:13 +0100 Subject: [PATCH 39/43] Apply suggestion from @yanthomasdev Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com> --- packages/decap-cms-widget-richtext/src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/decap-cms-widget-richtext/src/index.js b/packages/decap-cms-widget-richtext/src/index.js index 1d06f360be96..62340b8ad13a 100644 --- a/packages/decap-cms-widget-richtext/src/index.js +++ b/packages/decap-cms-widget-richtext/src/index.js @@ -12,5 +12,5 @@ function Widget(opts = {}) { }; } -export const DecapCmsWidgetMarkdown = { Widget, controlComponent, previewComponent }; -export default DecapCmsWidgetMarkdown; +export const DecapCmsWidgetRichtext = { Widget, controlComponent, previewComponent }; +export default DecapCmsWidgetRichtext; From 759e0372a9f8e3be229526b6ff0babc8f26bf19f Mon Sep 17 00:00:00 2001 From: Martin Jagodic Date: Wed, 25 Feb 2026 09:43:01 +0100 Subject: [PATCH 40/43] refactor(VisualEditor): remove unused handleBlockClick function and its reference --- .../src/RichtextControl/VisualEditor.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index f8374c922240..71533c360148 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -70,10 +70,6 @@ export default function VisualEditor(props) { mergeMediaConfig(editorComponents, field); - function handleBlockClick() { - console.log('handleBlockClick'); - } - function handleToggleMode() { onMode('raw'); } @@ -175,7 +171,6 @@ export default function VisualEditor(props) { Date: Wed, 25 Feb 2026 09:57:36 +0100 Subject: [PATCH 41/43] fix(RichtextControl): remove unnecessary ref attributes from editor divs --- packages/decap-cms-widget-richtext/src/RichtextControl.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl.js b/packages/decap-cms-widget-richtext/src/RichtextControl.js index dbb9866979ab..3bfd9d92c0c3 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl.js @@ -70,7 +70,7 @@ export default class RichtextControl extends React.Component { const visualEditor = ( -
+
+
Date: Wed, 25 Feb 2026 10:03:29 +0100 Subject: [PATCH 42/43] fix: format --- .../src/RichtextControl/VisualEditor.js | 1 - .../src/RichtextControl/plugins/ExtendedBlockquotePlugin.js | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index 71533c360148..8c5d5494d985 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -75,7 +75,6 @@ export default function VisualEditor(props) { } function handleChange({ value }) { - const mdValue = slateToMarkdown( value, { voidCodeBlock: !!codeBlockComponent }, diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ExtendedBlockquotePlugin.js b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ExtendedBlockquotePlugin.js index 15bf6bb8c289..3e177d642ada 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ExtendedBlockquotePlugin.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ExtendedBlockquotePlugin.js @@ -8,7 +8,6 @@ function isWithinBlockquote(editor, entry) { } function queryNode(editor, entry, { empty, first, start, collapsed }) { - return ( (!empty || editor.api.isEmpty(entry[1])) && (!first || !PathApi.hasPrevious(entry[1])) && From e8b150bdd32af7ad5c3f6039be3bafb11ce97ba3 Mon Sep 17 00:00:00 2001 From: Martin Jagodic Date: Wed, 25 Feb 2026 10:20:25 +0100 Subject: [PATCH 43/43] fix(RichtextControl): add missing onAddAsset and getAsset props to component --- .../decap-cms-widget-richtext/src/RichtextControl.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl.js b/packages/decap-cms-widget-richtext/src/RichtextControl.js index 3bfd9d92c0c3..e83acecf7705 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl.js @@ -12,6 +12,8 @@ const MODE_STORAGE_KEY = 'cms.md-mode'; export default class RichtextControl extends React.Component { static propTypes = { onChange: PropTypes.func.isRequired, + onAddAsset: PropTypes.func.isRequired, + getAsset: PropTypes.func.isRequired, classNameWrapper: PropTypes.string.isRequired, editorControl: PropTypes.elementType.isRequired, value: PropTypes.string, @@ -20,6 +22,10 @@ export default class RichtextControl extends React.Component { t: PropTypes.func.isRequired, isDisabled: PropTypes.bool, }; + + static defaultProps = { + value: '', + }; constructor(props) { super(props); const preferredMode = localStorage.getItem(MODE_STORAGE_KEY) ?? 'rich_text'; @@ -62,6 +68,8 @@ export default class RichtextControl extends React.Component { getEditorComponents, editorControl, onChange, + onAddAsset, + getAsset, value, } = this.props; @@ -80,6 +88,8 @@ export default class RichtextControl extends React.Component { onMode={this.handleMode} isShowModeToggle={isShowModeToggle} onChange={onChange} + onAddAsset={onAddAsset} + getAsset={getAsset} pendingFocus={pendingFocus && this.setFocusReceived} value={value} /> @@ -91,6 +101,8 @@ export default class RichtextControl extends React.Component {