From bdbcb6fde0426f54589108b90fcb153afe12129b Mon Sep 17 00:00:00 2001 From: Shubham-Lal Date: Tue, 9 Jul 2024 22:51:12 +0530 Subject: [PATCH 1/9] client: add wisiwyg editor --- client/package-lock.json | 387 ++++++++++++++++++ client/package.json | 1 + client/src/App.tsx | 7 +- .../src/components/sidebar/left-sidebar.css | 1 + client/src/globals.css | 4 - client/src/pages/create-debate/editor.tsx | 116 ++++++ client/src/pages/create-debate/index.tsx | 63 ++- client/src/pages/create-debate/styles.css | 110 +++++ 8 files changed, 680 insertions(+), 9 deletions(-) create mode 100644 client/src/pages/create-debate/editor.tsx create mode 100644 client/src/pages/create-debate/styles.css diff --git a/client/package-lock.json b/client/package-lock.json index eb23d7d..4b32efc 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12,6 +12,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^5.0.1", + "react-quill": "^2.0.0", "react-router-dom": "^6.22.1", "sonner": "^1.4.41", "zustand": "^4.5.1" @@ -986,6 +987,14 @@ "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", "devOptional": true }, + "node_modules/@types/quill": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz", + "integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==", + "dependencies": { + "parchment": "^1.1.2" + } + }, "node_modules/@types/react": { "version": "18.2.58", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.58.tgz", @@ -1329,6 +1338,24 @@ "node": ">=8" } }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1354,6 +1381,14 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1415,12 +1450,63 @@ } } }, + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -1445,6 +1531,25 @@ "node": ">=6.0.0" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", @@ -1680,12 +1785,27 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==" + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -1815,6 +1935,40 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1904,6 +2058,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -1919,6 +2084,64 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -1969,6 +2192,35 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2008,6 +2260,21 @@ "node": ">=8" } }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2086,6 +2353,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2199,6 +2471,29 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2255,6 +2550,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parchment": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -2387,6 +2687,32 @@ } ] }, + "node_modules/quill": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz", + "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==", + "dependencies": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.2", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + } + }, + "node_modules/quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "dependencies": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -2418,6 +2744,20 @@ "react": "*" } }, + "node_modules/react-quill": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz", + "integrity": "sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==", + "dependencies": { + "@types/quill": "^1.3.10", + "lodash": "^4.17.4", + "quill": "^1.3.7" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, "node_modules/react-router": { "version": "6.22.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.1.tgz", @@ -2448,6 +2788,23 @@ "react-dom": ">=16.8" } }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2560,6 +2917,36 @@ "node": ">=10" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/client/package.json b/client/package.json index 6bffc6f..55bf6e6 100644 --- a/client/package.json +++ b/client/package.json @@ -15,6 +15,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^5.0.1", + "react-quill": "^2.0.0", "react-router-dom": "^6.22.1", "sonner": "^1.4.41", "zustand": "^4.5.1" diff --git a/client/src/App.tsx b/client/src/App.tsx index 3b16aa5..3d909f7 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -2,7 +2,7 @@ import "./App.css" import { useRef, useState, useEffect } from "react" import { Routes, Route, Navigate } from "react-router-dom" import { Toaster } from "sonner" -import { ProtectedRoute } from "./ProtectedRoute" +// import { ProtectedRoute } from "./ProtectedRoute" import { Theme, useNavStore } from "./store/useNavStore" import { AuthTab, useAuthStore } from "./store/useAuthStore" import handleAutoLogin from "./utils/handleAutoLogin" @@ -23,7 +23,7 @@ export default function App() { const { expand, sidebar, setSidebar } = useNavStore(); useEffect(() => { - document.body.setAttribute('data-theme', localStorage.getItem('theme') === Theme.Light? Theme.Light : Theme.Dark); + document.body.setAttribute('data-theme', localStorage.getItem('theme') === Theme.Light ? Theme.Light : Theme.Dark); handleAutoLogin(setRoute, setUser, setIsAuthenticated, setAuthTab); }, [setRoute, setUser, setIsAuthenticated, setAuthTab]); @@ -65,7 +65,8 @@ export default function App() { } /> } /> } /> - } /> + {/* } /> */} + } /> } /> } /> } /> diff --git a/client/src/components/sidebar/left-sidebar.css b/client/src/components/sidebar/left-sidebar.css index 46a0c32..4ab4024 100644 --- a/client/src/components/sidebar/left-sidebar.css +++ b/client/src/components/sidebar/left-sidebar.css @@ -32,6 +32,7 @@ } #left-sidebar ul { + list-style: none; max-width: fit-content; height: 100%; max-height: 100dvh; diff --git a/client/src/globals.css b/client/src/globals.css index ab589ed..5571d61 100644 --- a/client/src/globals.css +++ b/client/src/globals.css @@ -131,10 +131,6 @@ input { font-family: inherit; } -ul { - list-style: none; -} - @keyframes shake { 0%, diff --git a/client/src/pages/create-debate/editor.tsx b/client/src/pages/create-debate/editor.tsx new file mode 100644 index 0000000..930b775 --- /dev/null +++ b/client/src/pages/create-debate/editor.tsx @@ -0,0 +1,116 @@ +import { useState, useRef } from 'react'; + +export default function CreateDebatePage() { + const [content, setContent] = useState(''); + const textAreaRef = useRef(null); + + const applyFormatting = (tag: string) => { + const textarea = textAreaRef.current; + if (textarea) { + const start = textarea.selectionStart || 0; + const end = textarea.selectionEnd || 0; + + const selectedText = content.substring(start, end); + let newText; + + switch (tag) { + case 'p': + case 'b': + case 'i': + case 's': + case 'code': + case 'pre': + newText = content.slice(0, start) + `<${tag}>${selectedText}` + content.slice(end); + break; + case 'h1': + case 'h2': + case 'h3': + case 'h4': + case 'h5': + case 'h6': + newText = content.slice(0, start) + `<${tag}>${selectedText}` + content.slice(end); + break; + case 'ul': + newText = content.slice(0, start) + `
  • ${selectedText}
` + content.slice(end); + break; + case 'ol': + newText = content.slice(0, start) + `
  1. ${selectedText}
` + content.slice(end); + break; + default: + newText = content; + break; + } + + setContent(newText); + + setTimeout(() => { + let newCursorPosition; + switch (tag) { + case 'p': + case 'b': + case 'i': + case 's': + newCursorPosition = start === end ? start + tag.length + 2 : start + 6 + selectedText.length + tag.length; + break; + case 'code': + newCursorPosition = start === end ? start + tag.length + 2 : start + 13 + selectedText.length; + break; + case 'pre': + newCursorPosition = start === end ? start + tag.length + 8 : start + 24 + selectedText.length; + break; + case 'h1': + case 'h2': + case 'h3': + case 'h4': + case 'h5': + case 'h6': + newCursorPosition = start === end ? start + tag.length + 2 : start + selectedText.length + tag.length + 7; + break; + case 'ul': + case 'ol': + newCursorPosition = start === end ? start + 8 : start + 13 + selectedText.length; + break; + default: + newCursorPosition = start; + break; + } + + if (textarea) { + textarea.focus(); + textarea.setSelectionRange(newCursorPosition, newCursorPosition); + } + }, 0); + } + }; + + return ( +
+
+ + + + + + + + + + + + + + +
+ +
+ ) } \ No newline at end of file diff --git a/client/src/pages/create-debate/style.css b/client/src/pages/create-debate/style.css index 061f1bb..f836bba 100644 --- a/client/src/pages/create-debate/style.css +++ b/client/src/pages/create-debate/style.css @@ -1,110 +1,20 @@ -.quill { - border-radius: 5px; - overflow: hidden; +.hidden { + display: none; } -.ql-toolbar.ql-snow { - background-color: #eaecec; +#create .sceditor-container { + width: 100% !important; + height: 500px !important; + /* height: calc(100dvh - 40px) !important; */ } -.ql-editor { - min-height: calc(100dvh - 40px - 67px); - background: #fefcfc; - color: #000000; -} - -@media screen and (max-width: 1597px) { - .ql-editor { - min-height: calc(100dvh - 40px - 91px); - } -} - -@media screen and (min-width: 1188px) { - .quill.h-full .ql-editor { - min-height: calc(100dvh - 40px - 43px); - } -} - -@media screen and (max-width: 1138px) { - .ql-editor { - min-height: calc(100dvh - 40px - 115px); - } -} - -@media screen and (min-width: 1024px) and (max-width: 1187px) { - .quill.h-full .ql-editor { - min-height: calc(100dvh - 40px - 67px); - } -} - -@media screen and (max-width: 1024px) { - .ql-editor { - min-height: calc(100dvh - 40px - 91px); - } -} - -@media screen and (max-width: 910px) { - .ql-editor { - min-height: calc(100dvh - 40px - 115px); - } -} - -@media screen and (max-width: 767px) { - .ql-editor { - min-height: calc(100dvh - 40px - 67px); - } -} - -@media screen and (max-width: 752px) { - .ql-editor { - min-height: calc(100dvh - 40px - 91px); - } -} - -@media screen and (max-width: 535px) { - .ql-editor { - min-height: calc(100dvh - 40px - 115px); - } +.sceditor-button-print, +.sceditor-button-maximize { + display: none !important; } @media screen and (max-width: 480px) { - .ql-editor { - min-height: calc(100dvh - 70px - 70px - 91px); - } -} - -@media screen and (max-width: 434px) { - .ql-editor { - min-height: calc(100dvh - 70px - 70px - 115px); - } -} - -@media screen and (max-width: 350px) { - .ql-editor { - min-height: calc(100dvh - 70px - 70px - 139px); - } -} - -@media screen and (max-width: 306px) { - .ql-editor { - min-height: calc(100dvh - 70px - 70px - 163px); - } -} - -@media screen and (max-width: 264px) { - .ql-editor { - min-height: calc(100dvh - 70px - 70px - 187px); - } -} - -@media screen and (max-width: 246px) { - .ql-editor { - min-height: calc(100dvh - 70px - 70px - 211px); - } -} - -@media screen and (max-width: 221px) { - .ql-editor { - min-height: calc(100dvh - 70px - 70px - 235px); + #create .sceditor-container { + height: calc(100dvh - 70px - 70px) !important; } } \ No newline at end of file From aa4833a6084b71464b70a9a34a421a1a9e618ea9 Mon Sep 17 00:00:00 2001 From: Shubham-Lal Date: Wed, 10 Jul 2024 12:33:27 +0530 Subject: [PATCH 7/9] fix: editor not showing --- client/src/pages/create-debate/index.tsx | 10 +- client/src/pages/create-debate/style.css | 4 +- client/src/sceditor/README.md | 138 + .../sceditor/development/formats/bbcode.js | 2646 ++++ .../src/sceditor/development/formats/xhtml.js | 1269 ++ .../sceditor/development/icons/material.js | 132 + .../sceditor/development/icons/monocons.js | 112 + .../development/jquery.sceditor.bbcode.js | 12209 ++++++++++++++++ .../sceditor/development/jquery.sceditor.js | 9563 ++++++++++++ .../development/jquery.sceditor.xhtml.js | 10832 ++++++++++++++ .../development/plugins/alternative-lists.js | 157 + .../sceditor/development/plugins/autosave.js | 110 + .../development/plugins/autoyoutube.js | 106 + .../sceditor/development/plugins/dragdrop.js | 222 + .../sceditor/development/plugins/format.js | 127 + .../sceditor/development/plugins/plaintext.js | 78 + .../src/sceditor/development/plugins/undo.js | 372 + .../sceditor/development/plugins/v1compat.js | 97 + client/src/sceditor/development/sceditor.js | 9494 ++++++++++++ .../development/themes/content/default.css | 85 + .../sceditor/development/themes/default.css | 530 + .../development/themes/defaultdark.css | 548 + .../sceditor/development/themes/famfamfam.png | Bin 0 -> 4583 bytes .../sceditor/development/themes/modern.css | 604 + .../development/themes/office-toolbar.css | 596 + .../sceditor/development/themes/office.css | 618 + .../sceditor/development/themes/square.css | 619 + client/src/sceditor/emoticons/alien.png | Bin 0 -> 756 bytes client/src/sceditor/emoticons/angel.png | Bin 0 -> 1182 bytes client/src/sceditor/emoticons/angry.png | Bin 0 -> 781 bytes client/src/sceditor/emoticons/blink.png | Bin 0 -> 972 bytes client/src/sceditor/emoticons/blush.png | Bin 0 -> 865 bytes client/src/sceditor/emoticons/cheerful.png | Bin 0 -> 753 bytes client/src/sceditor/emoticons/cool.png | Bin 0 -> 965 bytes client/src/sceditor/emoticons/credits.txt | 9 + client/src/sceditor/emoticons/cwy.png | Bin 0 -> 877 bytes client/src/sceditor/emoticons/devil.png | Bin 0 -> 1012 bytes client/src/sceditor/emoticons/dizzy.png | Bin 0 -> 991 bytes client/src/sceditor/emoticons/emoticons.json | 37 + client/src/sceditor/emoticons/ermm.png | Bin 0 -> 983 bytes client/src/sceditor/emoticons/face.png | Bin 0 -> 793 bytes client/src/sceditor/emoticons/getlost.png | Bin 0 -> 792 bytes client/src/sceditor/emoticons/grin.png | Bin 0 -> 867 bytes client/src/sceditor/emoticons/happy.png | Bin 0 -> 792 bytes client/src/sceditor/emoticons/heart.png | Bin 0 -> 572 bytes client/src/sceditor/emoticons/kissing.png | Bin 0 -> 793 bytes client/src/sceditor/emoticons/laughing.png | Bin 0 -> 912 bytes client/src/sceditor/emoticons/ninja.png | Bin 0 -> 694 bytes client/src/sceditor/emoticons/pinch.png | Bin 0 -> 804 bytes client/src/sceditor/emoticons/pouty.png | Bin 0 -> 799 bytes client/src/sceditor/emoticons/sad.png | Bin 0 -> 789 bytes client/src/sceditor/emoticons/shocked.png | Bin 0 -> 780 bytes client/src/sceditor/emoticons/sick.png | Bin 0 -> 783 bytes client/src/sceditor/emoticons/sideways.png | Bin 0 -> 788 bytes client/src/sceditor/emoticons/silly.png | Bin 0 -> 930 bytes client/src/sceditor/emoticons/sleeping.png | Bin 0 -> 1039 bytes client/src/sceditor/emoticons/smile.png | Bin 0 -> 983 bytes client/src/sceditor/emoticons/tongue.png | Bin 0 -> 981 bytes client/src/sceditor/emoticons/unsure.png | Bin 0 -> 763 bytes client/src/sceditor/emoticons/w00t.png | Bin 0 -> 718 bytes client/src/sceditor/emoticons/wassat.png | Bin 0 -> 810 bytes client/src/sceditor/emoticons/whistling.png | Bin 0 -> 1072 bytes client/src/sceditor/emoticons/wink.png | Bin 0 -> 791 bytes client/src/sceditor/emoticons/wub.png | Bin 0 -> 1010 bytes client/src/sceditor/example.html | 91 + client/src/sceditor/languages/ar.js | 68 + client/src/sceditor/languages/ca.js | 68 + client/src/sceditor/languages/cn.js | 68 + client/src/sceditor/languages/cs.js | 71 + client/src/sceditor/languages/de.js | 61 + client/src/sceditor/languages/el.js | 68 + client/src/sceditor/languages/en-US.js | 7 + client/src/sceditor/languages/en.js | 12 + client/src/sceditor/languages/es.js | 68 + client/src/sceditor/languages/et.js | 57 + client/src/sceditor/languages/fa.js | 69 + client/src/sceditor/languages/fi.js | 70 + client/src/sceditor/languages/fr.js | 70 + client/src/sceditor/languages/gl.js | 68 + client/src/sceditor/languages/hu.js | 69 + client/src/sceditor/languages/id.js | 68 + client/src/sceditor/languages/it.js | 72 + client/src/sceditor/languages/ja.js | 71 + client/src/sceditor/languages/lt.js | 68 + client/src/sceditor/languages/nb.js | 70 + client/src/sceditor/languages/nl.js | 72 + client/src/sceditor/languages/pl.js | 68 + client/src/sceditor/languages/pt-BR.js | 67 + client/src/sceditor/languages/pt.js | 69 + client/src/sceditor/languages/ru.js | 60 + client/src/sceditor/languages/sk.js | 70 + client/src/sceditor/languages/sv.js | 58 + client/src/sceditor/languages/template.js | 80 + client/src/sceditor/languages/tr.js | 66 + client/src/sceditor/languages/tw.js | 68 + client/src/sceditor/languages/uk.js | 57 + client/src/sceditor/languages/vi.js | 68 + .../src/sceditor/minified/formats/bbcode.js | 2 + client/src/sceditor/minified/formats/xhtml.js | 2 + .../src/sceditor/minified/icons/material.js | 2 + .../src/sceditor/minified/icons/monocons.js | 2 + .../minified/jquery.sceditor.bbcode.min.js | 2 + .../sceditor/minified/jquery.sceditor.min.js | 2 + .../minified/jquery.sceditor.xhtml.min.js | 2 + .../minified/plugins/alternative-lists.js | 2 + .../src/sceditor/minified/plugins/autosave.js | 2 + .../sceditor/minified/plugins/autoyoutube.js | 2 + .../src/sceditor/minified/plugins/dragdrop.js | 2 + .../src/sceditor/minified/plugins/format.js | 2 + .../sceditor/minified/plugins/plaintext.js | 2 + client/src/sceditor/minified/plugins/undo.js | 2 + .../src/sceditor/minified/plugins/v1compat.js | 2 + client/src/sceditor/minified/sceditor.min.js | 2 + .../minified/themes/content/default.min.css | 1 + .../sceditor/minified/themes/default.min.css | 1 + .../minified/themes/defaultdark.min.css | 1 + .../sceditor/minified/themes/famfamfam.png | Bin 0 -> 4583 bytes .../sceditor/minified/themes/modern.min.css | 1 + .../minified/themes/office-toolbar.min.css | 1 + .../sceditor/minified/themes/office.min.css | 1 + .../sceditor/minified/themes/square.min.css | 1 + 121 files changed, 53493 insertions(+), 7 deletions(-) create mode 100644 client/src/sceditor/README.md create mode 100644 client/src/sceditor/development/formats/bbcode.js create mode 100644 client/src/sceditor/development/formats/xhtml.js create mode 100644 client/src/sceditor/development/icons/material.js create mode 100644 client/src/sceditor/development/icons/monocons.js create mode 100644 client/src/sceditor/development/jquery.sceditor.bbcode.js create mode 100644 client/src/sceditor/development/jquery.sceditor.js create mode 100644 client/src/sceditor/development/jquery.sceditor.xhtml.js create mode 100644 client/src/sceditor/development/plugins/alternative-lists.js create mode 100644 client/src/sceditor/development/plugins/autosave.js create mode 100644 client/src/sceditor/development/plugins/autoyoutube.js create mode 100644 client/src/sceditor/development/plugins/dragdrop.js create mode 100644 client/src/sceditor/development/plugins/format.js create mode 100644 client/src/sceditor/development/plugins/plaintext.js create mode 100644 client/src/sceditor/development/plugins/undo.js create mode 100644 client/src/sceditor/development/plugins/v1compat.js create mode 100644 client/src/sceditor/development/sceditor.js create mode 100644 client/src/sceditor/development/themes/content/default.css create mode 100644 client/src/sceditor/development/themes/default.css create mode 100644 client/src/sceditor/development/themes/defaultdark.css create mode 100644 client/src/sceditor/development/themes/famfamfam.png create mode 100644 client/src/sceditor/development/themes/modern.css create mode 100644 client/src/sceditor/development/themes/office-toolbar.css create mode 100644 client/src/sceditor/development/themes/office.css create mode 100644 client/src/sceditor/development/themes/square.css create mode 100644 client/src/sceditor/emoticons/alien.png create mode 100644 client/src/sceditor/emoticons/angel.png create mode 100644 client/src/sceditor/emoticons/angry.png create mode 100644 client/src/sceditor/emoticons/blink.png create mode 100644 client/src/sceditor/emoticons/blush.png create mode 100644 client/src/sceditor/emoticons/cheerful.png create mode 100644 client/src/sceditor/emoticons/cool.png create mode 100644 client/src/sceditor/emoticons/credits.txt create mode 100644 client/src/sceditor/emoticons/cwy.png create mode 100644 client/src/sceditor/emoticons/devil.png create mode 100644 client/src/sceditor/emoticons/dizzy.png create mode 100644 client/src/sceditor/emoticons/emoticons.json create mode 100644 client/src/sceditor/emoticons/ermm.png create mode 100644 client/src/sceditor/emoticons/face.png create mode 100644 client/src/sceditor/emoticons/getlost.png create mode 100644 client/src/sceditor/emoticons/grin.png create mode 100644 client/src/sceditor/emoticons/happy.png create mode 100644 client/src/sceditor/emoticons/heart.png create mode 100644 client/src/sceditor/emoticons/kissing.png create mode 100644 client/src/sceditor/emoticons/laughing.png create mode 100644 client/src/sceditor/emoticons/ninja.png create mode 100644 client/src/sceditor/emoticons/pinch.png create mode 100644 client/src/sceditor/emoticons/pouty.png create mode 100644 client/src/sceditor/emoticons/sad.png create mode 100644 client/src/sceditor/emoticons/shocked.png create mode 100644 client/src/sceditor/emoticons/sick.png create mode 100644 client/src/sceditor/emoticons/sideways.png create mode 100644 client/src/sceditor/emoticons/silly.png create mode 100644 client/src/sceditor/emoticons/sleeping.png create mode 100644 client/src/sceditor/emoticons/smile.png create mode 100644 client/src/sceditor/emoticons/tongue.png create mode 100644 client/src/sceditor/emoticons/unsure.png create mode 100644 client/src/sceditor/emoticons/w00t.png create mode 100644 client/src/sceditor/emoticons/wassat.png create mode 100644 client/src/sceditor/emoticons/whistling.png create mode 100644 client/src/sceditor/emoticons/wink.png create mode 100644 client/src/sceditor/emoticons/wub.png create mode 100644 client/src/sceditor/example.html create mode 100644 client/src/sceditor/languages/ar.js create mode 100644 client/src/sceditor/languages/ca.js create mode 100644 client/src/sceditor/languages/cn.js create mode 100644 client/src/sceditor/languages/cs.js create mode 100644 client/src/sceditor/languages/de.js create mode 100644 client/src/sceditor/languages/el.js create mode 100644 client/src/sceditor/languages/en-US.js create mode 100644 client/src/sceditor/languages/en.js create mode 100644 client/src/sceditor/languages/es.js create mode 100644 client/src/sceditor/languages/et.js create mode 100644 client/src/sceditor/languages/fa.js create mode 100644 client/src/sceditor/languages/fi.js create mode 100644 client/src/sceditor/languages/fr.js create mode 100644 client/src/sceditor/languages/gl.js create mode 100644 client/src/sceditor/languages/hu.js create mode 100644 client/src/sceditor/languages/id.js create mode 100644 client/src/sceditor/languages/it.js create mode 100644 client/src/sceditor/languages/ja.js create mode 100644 client/src/sceditor/languages/lt.js create mode 100644 client/src/sceditor/languages/nb.js create mode 100644 client/src/sceditor/languages/nl.js create mode 100644 client/src/sceditor/languages/pl.js create mode 100644 client/src/sceditor/languages/pt-BR.js create mode 100644 client/src/sceditor/languages/pt.js create mode 100644 client/src/sceditor/languages/ru.js create mode 100644 client/src/sceditor/languages/sk.js create mode 100644 client/src/sceditor/languages/sv.js create mode 100644 client/src/sceditor/languages/template.js create mode 100644 client/src/sceditor/languages/tr.js create mode 100644 client/src/sceditor/languages/tw.js create mode 100644 client/src/sceditor/languages/uk.js create mode 100644 client/src/sceditor/languages/vi.js create mode 100644 client/src/sceditor/minified/formats/bbcode.js create mode 100644 client/src/sceditor/minified/formats/xhtml.js create mode 100644 client/src/sceditor/minified/icons/material.js create mode 100644 client/src/sceditor/minified/icons/monocons.js create mode 100644 client/src/sceditor/minified/jquery.sceditor.bbcode.min.js create mode 100644 client/src/sceditor/minified/jquery.sceditor.min.js create mode 100644 client/src/sceditor/minified/jquery.sceditor.xhtml.min.js create mode 100644 client/src/sceditor/minified/plugins/alternative-lists.js create mode 100644 client/src/sceditor/minified/plugins/autosave.js create mode 100644 client/src/sceditor/minified/plugins/autoyoutube.js create mode 100644 client/src/sceditor/minified/plugins/dragdrop.js create mode 100644 client/src/sceditor/minified/plugins/format.js create mode 100644 client/src/sceditor/minified/plugins/plaintext.js create mode 100644 client/src/sceditor/minified/plugins/undo.js create mode 100644 client/src/sceditor/minified/plugins/v1compat.js create mode 100644 client/src/sceditor/minified/sceditor.min.js create mode 100644 client/src/sceditor/minified/themes/content/default.min.css create mode 100644 client/src/sceditor/minified/themes/default.min.css create mode 100644 client/src/sceditor/minified/themes/defaultdark.min.css create mode 100644 client/src/sceditor/minified/themes/famfamfam.png create mode 100644 client/src/sceditor/minified/themes/modern.min.css create mode 100644 client/src/sceditor/minified/themes/office-toolbar.min.css create mode 100644 client/src/sceditor/minified/themes/office.min.css create mode 100644 client/src/sceditor/minified/themes/square.min.css diff --git a/client/src/pages/create-debate/index.tsx b/client/src/pages/create-debate/index.tsx index 15228b8..7bdfd9c 100644 --- a/client/src/pages/create-debate/index.tsx +++ b/client/src/pages/create-debate/index.tsx @@ -10,14 +10,14 @@ export default function CreateDebatePage() { try { const link = document.createElement('link'); link.rel = 'stylesheet'; - link.href = 'node_modules/sceditor/minified/themes/default.min.css'; + link.href = '/src/sceditor/minified/themes/default.min.css'; document.head.appendChild(link); const sceditorScript = document.createElement('script'); - sceditorScript.src = 'node_modules/sceditor/minified/sceditor.min.js'; + sceditorScript.src = '/src/sceditor/minified/sceditor.min.js'; sceditorScript.onload = () => { const formatScript = document.createElement('script'); - formatScript.src = 'node_modules/sceditor/minified/formats/xhtml.js'; + formatScript.src = '/src/sceditor/minified/formats/xhtml.js'; formatScript.onload = () => { if (textareaRef.current) { fetch('/src/pages/create-debate/emoticons.json') @@ -32,8 +32,8 @@ export default function CreateDebatePage() { (window as any).sceditor.create(textareaRef.current, { format: 'xhtml', - style: 'node_modules/sceditor/minified/themes/default.min.css', - emoticonsRoot: 'node_modules/sceditor/emoticons/', + style: '/src/sceditor/minified/themes/default.min.css', + emoticonsRoot: '/src/sceditor/emoticons/', emoticons: emoticonsConfig }); diff --git a/client/src/pages/create-debate/style.css b/client/src/pages/create-debate/style.css index f836bba..fe55f3b 100644 --- a/client/src/pages/create-debate/style.css +++ b/client/src/pages/create-debate/style.css @@ -4,8 +4,8 @@ #create .sceditor-container { width: 100% !important; - height: 500px !important; - /* height: calc(100dvh - 40px) !important; */ + /* height: 500px !important; */ + height: calc(100dvh - 40px) !important; } .sceditor-button-print, diff --git a/client/src/sceditor/README.md b/client/src/sceditor/README.md new file mode 100644 index 0000000..7926a55 --- /dev/null +++ b/client/src/sceditor/README.md @@ -0,0 +1,138 @@ +# [SCEditor](http://www.sceditor.com/) + +[![Build Status](https://github.com/samclarke/SCEditor/workflows/Node.js%20CI/badge.svg)](https://travis-ci.org/samclarke/SCEditor) +[![SemVer](http://img.shields.io/:semver-✓-brightgreen.svg)](http://semver.org) +[![License](http://img.shields.io/npm/l/sceditor.svg)](https://github.com/samclarke/SCEditor/blob/master/LICENSE.md) + +A lightweight WYSIWYG BBCode and XHTML editor. + +[![SCEditor preview](https://cdn.rawgit.com/samclarke/SCEditor/49c696b8/preview.svg)](https://www.sceditor.com/) + +For more information visit [sceditor.com](http://www.sceditor.com/) + + +## Usage + +Include the SCEditor JavaScript: + +```html + + + + +``` + +Then to convert a textarea into SCEditor, simply do: + +```js +var textarea = document.getElementById('id-of-textarea'); + +sceditor.create(textarea, { + format: 'xhtml', + style: 'minified/themes/content/default.min.css' +}); +``` + +or for a BBCode WYSIWYG editor do: + +```js +var textarea = document.getElementById('id-of-textarea'); + +sceditor.create(textarea, { + format: 'bbcode', + style: 'minified/themes/content/default.min.css' +}); +``` + +Finally, to get the contents of the editor: + +```js +var textarea = document.getElementById("id-of-textarea"); + +sceditor.instance(textarea).val(); +``` + + +## Options + +For a full list of options, see the [options documentation](http://www.sceditor.com/documentation/options/). + + + +## Building and testing + +You will need [Grunt](http://gruntjs.com/) installed to run the build/tests. To install Grunt run: + +```bash +npm install -g grunt-cli +``` + +Next, to install the SCEditor dev dependencies run: + +```bash +npm install +``` + +That's it! You can now build and test SCEditor with the following commands: + +```bash +# Minify the JS and convert the LESS to CSS +grunt build + +# Run the linter, unit tests and coverage +grunt test + +# Creates the final distributable ZIP file +grunt release +``` + +You can also run the dev server to test changes without having to do a full +build by running: + +```bash +npm run dev +``` + +and then going to http://localhost:9000/tests/ + + +## Contribute + +Any contributions and/or pull requests would be welcome. + +Themes, translations, bug reports, bug fixes and donations are greatly appreciated. + + + +## Donate + +If you would like to make a donation you can via +[PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=AVJSF5NEETYYG) +or via [Flattr](http://flattr.com/thing/400345/SCEditor) + + + +## License + +SCEditor is licensed under the [MIT](/LICENSE.md) license: + + +Copyright (C) 2011 - 2017 Sam Clarke and contributors – sceditor.com + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +## Credits + +**Nomicons: The Full Monty Emoticons by:** +Oscar Gruno, aka Nominell v. 2.0 -> oscargruno@mac.com +Andy Fedosjeenko, aka Nightwolf -> bobo@animevanguard.com + +**Icons by:** +Mark James (http://www.famfamfam.com/lab/icons/silk/) +Licensed under the [Creative Commons CC-BY license](http://creativecommons.org/licenses/by/3.0/). diff --git a/client/src/sceditor/development/formats/bbcode.js b/client/src/sceditor/development/formats/bbcode.js new file mode 100644 index 0000000..4a25c2f --- /dev/null +++ b/client/src/sceditor/development/formats/bbcode.js @@ -0,0 +1,2646 @@ +/** + * SCEditor BBCode Plugin + * http://www.sceditor.com/ + * + * Copyright (C) 2011-2017, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * @fileoverview SCEditor BBCode Format + * @author Sam Clarke + */ +(function (sceditor) { + /*eslint max-depth: off*/ + 'use strict'; + + var escapeEntities = sceditor.escapeEntities; + var escapeUriScheme = sceditor.escapeUriScheme; + var dom = sceditor.dom; + var utils = sceditor.utils; + + var css = dom.css; + var attr = dom.attr; + var is = dom.is; + var extend = utils.extend; + var each = utils.each; + + var EMOTICON_DATA_ATTR = 'data-sceditor-emoticon'; + + var getEditorCommand = sceditor.command.get; + + var QuoteType = { + /** @lends BBCodeParser.QuoteType */ + /** + * Always quote the attribute value + * @type {Number} + */ + always: 1, + + /** + * Never quote the attributes value + * @type {Number} + */ + never: 2, + + /** + * Only quote the attributes value when it contains spaces to equals + * @type {Number} + */ + auto: 3 + }; + + var defaultCommandsOverrides = { + bold: { + txtExec: ['[b]', '[/b]'] + }, + italic: { + txtExec: ['[i]', '[/i]'] + }, + underline: { + txtExec: ['[u]', '[/u]'] + }, + strike: { + txtExec: ['[s]', '[/s]'] + }, + subscript: { + txtExec: ['[sub]', '[/sub]'] + }, + superscript: { + txtExec: ['[sup]', '[/sup]'] + }, + left: { + txtExec: ['[left]', '[/left]'] + }, + center: { + txtExec: ['[center]', '[/center]'] + }, + right: { + txtExec: ['[right]', '[/right]'] + }, + justify: { + txtExec: ['[justify]', '[/justify]'] + }, + font: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('font')._dropDown( + editor, + caller, + function (fontName) { + editor.insertText( + '[font=' + fontName + ']', + '[/font]' + ); + } + ); + } + }, + size: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('size')._dropDown( + editor, + caller, + function (fontSize) { + editor.insertText( + '[size=' + fontSize + ']', + '[/size]' + ); + } + ); + } + }, + color: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('color')._dropDown( + editor, + caller, + function (color) { + editor.insertText( + '[color=' + color + ']', + '[/color]' + ); + } + ); + } + }, + bulletlist: { + txtExec: function (caller, selected) { + this.insertText( + '[ul]\n[li]' + + selected.split(/\r?\n/).join('[/li]\n[li]') + + '[/li]\n[/ul]' + ); + } + }, + orderedlist: { + txtExec: function (caller, selected) { + this.insertText( + '[ol]\n[li]' + + selected.split(/\r?\n/).join('[/li]\n[li]') + + '[/li]\n[/ol]' + ); + } + }, + table: { + txtExec: ['[table][tr][td]', '[/td][/tr][/table]'] + }, + horizontalrule: { + txtExec: ['[hr]'] + }, + code: { + txtExec: ['[code]', '[/code]'] + }, + image: { + txtExec: function (caller, selected) { + var editor = this; + + getEditorCommand('image')._dropDown( + editor, + caller, + selected, + function (url, width, height) { + var attrs = ''; + + if (width) { + attrs += ' width=' + width; + } + + if (height) { + attrs += ' height=' + height; + } + + editor.insertText( + '[img' + attrs + ']' + url + '[/img]' + ); + } + ); + } + }, + email: { + txtExec: function (caller, selected) { + var editor = this; + + getEditorCommand('email')._dropDown( + editor, + caller, + function (url, text) { + editor.insertText( + '[email=' + url + ']' + + (text || selected || url) + + '[/email]' + ); + } + ); + } + }, + link: { + txtExec: function (caller, selected) { + var editor = this; + + getEditorCommand('link')._dropDown( + editor, + caller, + function (url, text) { + editor.insertText( + '[url=' + url + ']' + + (text || selected || url) + + '[/url]' + ); + } + ); + } + }, + quote: { + txtExec: ['[quote]', '[/quote]'] + }, + youtube: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('youtube')._dropDown( + editor, + caller, + function (id) { + editor.insertText('[youtube]' + id + '[/youtube]'); + } + ); + } + }, + rtl: { + txtExec: ['[rtl]', '[/rtl]'] + }, + ltr: { + txtExec: ['[ltr]', '[/ltr]'] + } + }; + + var bbcodeHandlers = { + // START_COMMAND: Bold + b: { + tags: { + b: null, + strong: null + }, + styles: { + // 401 is for FF 3.5 + 'font-weight': ['bold', 'bolder', '401', '700', '800', '900'] + }, + format: '[b]{0}[/b]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Italic + i: { + tags: { + i: null, + em: null + }, + styles: { + 'font-style': ['italic', 'oblique'] + }, + format: '[i]{0}[/i]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Underline + u: { + tags: { + u: null + }, + styles: { + 'text-decoration': ['underline'] + }, + format: '[u]{0}[/u]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Strikethrough + s: { + tags: { + s: null, + strike: null + }, + styles: { + 'text-decoration': ['line-through'] + }, + format: '[s]{0}[/s]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Subscript + sub: { + tags: { + sub: null + }, + format: '[sub]{0}[/sub]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Superscript + sup: { + tags: { + sup: null + }, + format: '[sup]{0}[/sup]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Font + font: { + tags: { + font: { + face: null + } + }, + styles: { + 'font-family': null + }, + quoteType: QuoteType.never, + format: function (element, content) { + var font; + + if (!is(element, 'font') || !(font = attr(element, 'face'))) { + font = css(element, 'font-family'); + } + + return '[font=' + _stripQuotes(font) + ']' + + content + '[/font]'; + }, + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Size + size: { + tags: { + font: { + size: null + } + }, + styles: { + 'font-size': null + }, + format: function (element, content) { + var fontSize = attr(element, 'size'), + size = 2; + + if (!fontSize) { + fontSize = css(element, 'fontSize'); + } + + // Most browsers return px value but IE returns 1-7 + if (fontSize.indexOf('px') > -1) { + // convert size to an int + fontSize = fontSize.replace('px', '') - 0; + + if (fontSize < 12) { + size = 1; + } + if (fontSize > 15) { + size = 3; + } + if (fontSize > 17) { + size = 4; + } + if (fontSize > 23) { + size = 5; + } + if (fontSize > 31) { + size = 6; + } + if (fontSize > 47) { + size = 7; + } + } else { + size = fontSize; + } + + return '[size=' + size + ']' + content + '[/size]'; + }, + html: '{!0}' + }, + // END_COMMAND + + // START_COMMAND: Color + color: { + tags: { + font: { + color: null + } + }, + styles: { + color: null + }, + quoteType: QuoteType.never, + format: function (elm, content) { + var color; + + if (!is(elm, 'font') || !(color = attr(elm, 'color'))) { + color = elm.style.color || css(elm, 'color'); + } + + return '[color=' + _normaliseColour(color) + ']' + + content + '[/color]'; + }, + html: function (token, attrs, content) { + return '' + content + ''; + } + }, + // END_COMMAND + + // START_COMMAND: Lists + ul: { + tags: { + ul: null + }, + breakStart: true, + isInline: false, + skipLastLineBreak: true, + format: '[ul]{0}[/ul]', + html: '
    {0}
' + }, + list: { + breakStart: true, + isInline: false, + skipLastLineBreak: true, + html: '
    {0}
' + }, + ol: { + tags: { + ol: null + }, + breakStart: true, + isInline: false, + skipLastLineBreak: true, + format: '[ol]{0}[/ol]', + html: '
    {0}
' + }, + li: { + tags: { + li: null + }, + isInline: false, + closedBy: ['/ul', '/ol', '/list', '*', 'li'], + format: '[li]{0}[/li]', + html: '
  • {0}
  • ' + }, + '*': { + isInline: false, + closedBy: ['/ul', '/ol', '/list', '*', 'li'], + html: '
  • {0}
  • ' + }, + // END_COMMAND + + // START_COMMAND: Table + table: { + tags: { + table: null + }, + isInline: false, + isHtmlInline: true, + skipLastLineBreak: true, + format: '[table]{0}[/table]', + html: '{0}
    ' + }, + tr: { + tags: { + tr: null + }, + isInline: false, + skipLastLineBreak: true, + format: '[tr]{0}[/tr]', + html: '{0}' + }, + th: { + tags: { + th: null + }, + allowsEmpty: true, + isInline: false, + format: '[th]{0}[/th]', + html: '{0}' + }, + td: { + tags: { + td: null + }, + allowsEmpty: true, + isInline: false, + format: '[td]{0}[/td]', + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Emoticons + emoticon: { + allowsEmpty: true, + tags: { + img: { + src: null, + 'data-sceditor-emoticon': null + } + }, + format: function (element, content) { + return attr(element, EMOTICON_DATA_ATTR) + content; + }, + html: '{0}' + }, + // END_COMMAND + + // START_COMMAND: Horizontal Rule + hr: { + tags: { + hr: null + }, + allowsEmpty: true, + isSelfClosing: true, + isInline: false, + format: '[hr]{0}', + html: '
    ' + }, + // END_COMMAND + + // START_COMMAND: Image + img: { + allowsEmpty: true, + tags: { + img: { + src: null + } + }, + allowedChildren: ['#'], + quoteType: QuoteType.never, + format: function (element, content) { + var width, height, + attribs = '', + style = function (name) { + return element.style ? element.style[name] : null; + }; + + // check if this is an emoticon image + if (attr(element, EMOTICON_DATA_ATTR)) { + return content; + } + + width = attr(element, 'width') || style('width'); + height = attr(element, 'height') || style('height'); + + // only add width and height if one is specified + if ((element.complete && (width || height)) || + (width && height)) { + + attribs = '=' + dom.width(element) + 'x' + + dom.height(element); + } + + return '[img' + attribs + ']' + attr(element, 'src') + '[/img]'; + }, + html: function (token, attrs, content) { + var undef, width, height, match, + attribs = ''; + + // handle [img width=340 height=240]url[/img] + width = attrs.width; + height = attrs.height; + + // handle [img=340x240]url[/img] + if (attrs.defaultattr) { + match = attrs.defaultattr.split(/x/i); + + width = match[0]; + height = (match.length === 2 ? match[1] : match[0]); + } + + if (width !== undef) { + attribs += ' width="' + escapeEntities(width, true) + '"'; + } + + if (height !== undef) { + attribs += ' height="' + escapeEntities(height, true) + '"'; + } + + return ''; + } + }, + // END_COMMAND + + // START_COMMAND: URL + url: { + allowsEmpty: true, + tags: { + a: { + href: null + } + }, + quoteType: QuoteType.never, + format: function (element, content) { + var url = attr(element, 'href'); + + // make sure this link is not an e-mail, + // if it is return e-mail BBCode + if (url.substr(0, 7) === 'mailto:') { + return '[email="' + url.substr(7) + '"]' + + content + '[/email]'; + } + + return '[url=' + url + ']' + content + '[/url]'; + }, + html: function (token, attrs, content) { + attrs.defaultattr = + escapeEntities(attrs.defaultattr, true) || content; + + return '' + + content + ''; + } + }, + // END_COMMAND + + // START_COMMAND: E-mail + email: { + quoteType: QuoteType.never, + html: function (token, attrs, content) { + return '' + content + ''; + } + }, + // END_COMMAND + + // START_COMMAND: Quote + quote: { + tags: { + blockquote: null + }, + isInline: false, + quoteType: QuoteType.never, + format: function (element, content) { + var authorAttr = 'data-author'; + var author = ''; + var cite; + var children = element.children; + + for (var i = 0; !cite && i < children.length; i++) { + if (is(children[i], 'cite')) { + cite = children[i]; + } + } + + if (cite || attr(element, authorAttr)) { + author = cite && cite.textContent || + attr(element, authorAttr); + + attr(element, authorAttr, author); + + if (cite) { + element.removeChild(cite); + } + + content = this.elementToBbcode(element); + author = '=' + author.replace(/(^\s+|\s+$)/g, ''); + + if (cite) { + element.insertBefore(cite, element.firstChild); + } + } + + return '[quote' + author + ']' + content + '[/quote]'; + }, + html: function (token, attrs, content) { + if (attrs.defaultattr) { + content = '' + escapeEntities(attrs.defaultattr) + + '' + content; + } + + return '
    ' + content + '
    '; + } + }, + // END_COMMAND + + // START_COMMAND: Code + code: { + tags: { + code: null + }, + isInline: false, + allowedChildren: ['#', '#newline'], + format: '[code]{0}[/code]', + html: '{0}' + }, + // END_COMMAND + + + // START_COMMAND: Left + left: { + styles: { + 'text-align': [ + 'left', + '-webkit-left', + '-moz-left', + '-khtml-left' + ] + }, + isInline: false, + allowsEmpty: true, + format: '[left]{0}[/left]', + html: '
    {0}
    ' + }, + // END_COMMAND + + // START_COMMAND: Centre + center: { + styles: { + 'text-align': [ + 'center', + '-webkit-center', + '-moz-center', + '-khtml-center' + ] + }, + isInline: false, + allowsEmpty: true, + format: '[center]{0}[/center]', + html: '
    {0}
    ' + }, + // END_COMMAND + + // START_COMMAND: Right + right: { + styles: { + 'text-align': [ + 'right', + '-webkit-right', + '-moz-right', + '-khtml-right' + ] + }, + isInline: false, + allowsEmpty: true, + format: '[right]{0}[/right]', + html: '
    {0}
    ' + }, + // END_COMMAND + + // START_COMMAND: Justify + justify: { + styles: { + 'text-align': [ + 'justify', + '-webkit-justify', + '-moz-justify', + '-khtml-justify' + ] + }, + isInline: false, + allowsEmpty: true, + format: '[justify]{0}[/justify]', + html: '
    {0}
    ' + }, + // END_COMMAND + + // START_COMMAND: YouTube + youtube: { + allowsEmpty: true, + tags: { + iframe: { + 'data-youtube-id': null + } + }, + format: function (element, content) { + element = attr(element, 'data-youtube-id'); + + return element ? '[youtube]' + element + '[/youtube]' : content; + }, + html: '' + }, + // END_COMMAND + + + // START_COMMAND: Rtl + rtl: { + styles: { + direction: ['rtl'] + }, + isInline: false, + format: '[rtl]{0}[/rtl]', + html: '
    {0}
    ' + }, + // END_COMMAND + + // START_COMMAND: Ltr + ltr: { + styles: { + direction: ['ltr'] + }, + isInline: false, + format: '[ltr]{0}[/ltr]', + html: '
    {0}
    ' + }, + // END_COMMAND + + // this is here so that commands above can be removed + // without having to remove the , after the last one. + // Needed for IE. + ignore: {} + }; + + /** + * Formats a string replacing {name} with the values of + * obj.name properties. + * + * If there is no property for the specified {name} then + * it will be left intact. + * + * @param {string} str + * @param {Object} obj + * @return {string} + * @since 2.0.0 + */ + function formatBBCodeString(str, obj) { + return str.replace(/\{([^}]+)\}/g, function (match, group) { + var undef, + escape = true; + + if (group.charAt(0) === '!') { + escape = false; + group = group.substring(1); + } + + if (group === '0') { + escape = false; + } + + if (obj[group] === undef) { + return match; + } + + return escape ? escapeEntities(obj[group], true) : obj[group]; + }); + } + + function isFunction(fn) { + return typeof fn === 'function'; + } + + /** + * Removes any leading or trailing quotes ('") + * + * @return string + * @since v1.4.0 + */ + function _stripQuotes(str) { + return str ? + str.replace(/\\(.)/g, '$1').replace(/^(["'])(.*?)\1$/, '$2') : str; + } + + /** + * Formats a string replacing {0}, {1}, {2}, ect. with + * the params provided + * + * @param {string} str The string to format + * @param {...string} arg The strings to replace + * @return {string} + * @since v1.4.0 + */ + function _formatString(str) { + var undef; + var args = arguments; + + return str.replace(/\{(\d+)\}/g, function (_, matchNum) { + return args[matchNum - 0 + 1] !== undef ? + args[matchNum - 0 + 1] : + '{' + matchNum + '}'; + }); + } + + var TOKEN_OPEN = 'open'; + var TOKEN_CONTENT = 'content'; + var TOKEN_NEWLINE = 'newline'; + var TOKEN_CLOSE = 'close'; + + + /* + * @typedef {Object} TokenizeToken + * @property {string} type + * @property {string} name + * @property {string} val + * @property {Object.} attrs + * @property {array} children + * @property {TokenizeToken} closing + */ + + /** + * Tokenize token object + * + * @param {string} type The type of token this is, + * should be one of tokenType + * @param {string} name The name of this token + * @param {string} val The originally matched string + * @param {array} attrs Any attributes. Only set on + * TOKEN_TYPE_OPEN tokens + * @param {array} children Any children of this token + * @param {TokenizeToken} closing This tokens closing tag. + * Only set on TOKEN_TYPE_OPEN tokens + * @class {TokenizeToken} + * @name {TokenizeToken} + * @memberOf BBCodeParser.prototype + */ + // eslint-disable-next-line max-params + function TokenizeToken(type, name, val, attrs, children, closing) { + var base = this; + + base.type = type; + base.name = name; + base.val = val; + base.attrs = attrs || {}; + base.children = children || []; + base.closing = closing || null; + }; + + TokenizeToken.prototype = { + /** @lends BBCodeParser.prototype.TokenizeToken */ + /** + * Clones this token + * + * @return {TokenizeToken} + */ + clone: function () { + var base = this; + + return new TokenizeToken( + base.type, + base.name, + base.val, + extend({}, base.attrs), + [], + base.closing ? base.closing.clone() : null + ); + }, + /** + * Splits this token at the specified child + * + * @param {TokenizeToken} splitAt The child to split at + * @return {TokenizeToken} The right half of the split token or + * empty clone if invalid splitAt lcoation + */ + splitAt: function (splitAt) { + var offsetLength; + var base = this; + var clone = base.clone(); + var offset = base.children.indexOf(splitAt); + + if (offset > -1) { + // Work out how many items are on the right side of the split + // to pass to splice() + offsetLength = base.children.length - offset; + clone.children = base.children.splice(offset, offsetLength); + } + + return clone; + } + }; + + + /** + * SCEditor BBCode parser class + * + * @param {Object} options + * @class BBCodeParser + * @name BBCodeParser + * @since v1.4.0 + */ + function BBCodeParser(options) { + var base = this; + + base.opts = extend({}, BBCodeParser.defaults, options); + + /** + * Takes a BBCode string and splits it into open, + * content and close tags. + * + * It does no checking to verify a tag has a matching open + * or closing tag or if the tag is valid child of any tag + * before it. For that the tokens should be passed to the + * parse function. + * + * @param {string} str + * @return {array} + * @memberOf BBCodeParser.prototype + */ + base.tokenize = function (str) { + var matches, type, i; + var tokens = []; + // The token types in reverse order of precedence + // (they're looped in reverse) + var tokenTypes = [ + { + type: TOKEN_CONTENT, + regex: /^([^\[\r\n]+|\[)/ + }, + { + type: TOKEN_NEWLINE, + regex: /^(\r\n|\r|\n)/ + }, + { + type: TOKEN_OPEN, + regex: /^\[[^\[\]]+\]/ + }, + // Close must come before open as they are + // the same except close has a / at the start. + { + type: TOKEN_CLOSE, + regex: /^\[\/[^\[\]]+\]/ + } + ]; + + strloop: + while (str.length) { + i = tokenTypes.length; + while (i--) { + type = tokenTypes[i].type; + + // Check if the string matches any of the tokens + if (!(matches = str.match(tokenTypes[i].regex)) || + !matches[0]) { + continue; + } + + // Add the match to the tokens list + tokens.push(tokenizeTag(type, matches[0])); + + // Remove the match from the string + str = str.substr(matches[0].length); + + // The token has been added so start again + continue strloop; + } + + // If there is anything left in the string which doesn't match + // any of the tokens then just assume it's content and add it. + if (str.length) { + tokens.push(tokenizeTag(TOKEN_CONTENT, str)); + } + + str = ''; + } + + return tokens; + }; + + /** + * Extracts the name an params from a tag + * + * @param {string} type + * @param {string} val + * @return {Object} + * @private + */ + function tokenizeTag(type, val) { + var matches, attrs, name, + openRegex = /\[([^\]\s=]+)(?:([^\]]+))?\]/, + closeRegex = /\[\/([^\[\]]+)\]/; + + // Extract the name and attributes from opening tags and + // just the name from closing tags. + if (type === TOKEN_OPEN && (matches = val.match(openRegex))) { + name = lower(matches[1]); + + if (matches[2] && (matches[2] = matches[2].trim())) { + attrs = tokenizeAttrs(matches[2]); + } + } + + if (type === TOKEN_CLOSE && + (matches = val.match(closeRegex))) { + name = lower(matches[1]); + } + + if (type === TOKEN_NEWLINE) { + name = '#newline'; + } + + // Treat all tokens without a name and + // all unknown BBCodes as content + if (!name || ((type === TOKEN_OPEN || type === TOKEN_CLOSE) && + !bbcodeHandlers[name])) { + + type = TOKEN_CONTENT; + name = '#'; + } + + return new TokenizeToken(type, name, val, attrs); + } + + /** + * Extracts the individual attributes from a string containing + * all the attributes. + * + * @param {string} attrs + * @return {Object} Assoc array of attributes + * @private + */ + function tokenizeAttrs(attrs) { + var matches, + /* + ([^\s=]+) Anything that's not a space or equals + = Equals sign = + (?: + (?: + (["']) The opening quote + ( + (?:\\\2|[^\2])*? Anything that isn't the + unescaped opening quote + ) + \2 The opening quote again which + will close the string + ) + | If not a quoted string then match + ( + (?:.(?!\s\S+=))*.? Anything that isn't part of + [space][non-space][=] which + would be a new attribute + ) + ) + */ + attrRegex = /([^\s=]+)=(?:(?:(["'])((?:\\\2|[^\2])*?)\2)|((?:.(?!\s\S+=))*.))/g, + ret = {}; + + // if only one attribute then remove the = from the start and + // strip any quotes + if (attrs.charAt(0) === '=' && attrs.indexOf('=', 1) < 0) { + ret.defaultattr = _stripQuotes(attrs.substr(1)); + } else { + if (attrs.charAt(0) === '=') { + attrs = 'defaultattr' + attrs; + } + + // No need to strip quotes here, the regex will do that. + while ((matches = attrRegex.exec(attrs))) { + ret[lower(matches[1])] = + _stripQuotes(matches[3]) || matches[4]; + } + } + + return ret; + } + + /** + * Parses a string into an array of BBCodes + * + * @param {string} str + * @param {boolean} preserveNewLines If to preserve all new lines, not + * strip any based on the passed + * formatting options + * @return {array} Array of BBCode objects + * @memberOf BBCodeParser.prototype + */ + base.parse = function (str, preserveNewLines) { + var ret = parseTokens(base.tokenize(str)); + var opts = base.opts; + + if (opts.fixInvalidNesting) { + fixNesting(ret); + } + + normaliseNewLines(ret, null, preserveNewLines); + + if (opts.removeEmptyTags) { + removeEmpty(ret); + } + + return ret; + }; + + /** + * Checks if an array of TokenizeToken's contains the + * specified token. + * + * Checks the tokens name and type match another tokens + * name and type in the array. + * + * @param {string} name + * @param {string} type + * @param {array} arr + * @return {Boolean} + * @private + */ + function hasTag(name, type, arr) { + var i = arr.length; + + while (i--) { + if (arr[i].type === type && arr[i].name === name) { + return true; + } + } + + return false; + } + + /** + * Checks if the child tag is allowed as one + * of the parent tags children. + * + * @param {TokenizeToken} parent + * @param {TokenizeToken} child + * @return {Boolean} + * @private + */ + function isChildAllowed(parent, child) { + var parentBBCode = parent ? bbcodeHandlers[parent.name] : {}, + allowedChildren = parentBBCode.allowedChildren; + + if (base.opts.fixInvalidChildren && allowedChildren) { + return allowedChildren.indexOf(child.name || '#') > -1; + } + + return true; + } + + // TODO: Tidy this parseTokens() function up a bit. + /** + * Parses an array of tokens created by tokenize() + * + * @param {array} toks + * @return {array} Parsed tokens + * @see tokenize() + * @private + */ + function parseTokens(toks) { + var token, bbcode, curTok, clone, i, next, + cloned = [], + output = [], + openTags = [], + /** + * Returns the currently open tag or undefined + * @return {TokenizeToken} + */ + currentTag = function () { + return last(openTags); + }, + /** + * Adds a tag to either the current tags children + * or to the output array. + * @param {TokenizeToken} token + * @private + */ + addTag = function (token) { + if (currentTag()) { + currentTag().children.push(token); + } else { + output.push(token); + } + }, + /** + * Checks if this tag closes the current tag + * @param {string} name + * @return {Void} + */ + closesCurrentTag = function (name) { + return currentTag() && + (bbcode = bbcodeHandlers[currentTag().name]) && + bbcode.closedBy && + bbcode.closedBy.indexOf(name) > -1; + }; + + while ((token = toks.shift())) { + next = toks[0]; + + /* + * Fixes any invalid children. + * + * If it is an element which isn't allowed as a child of it's + * parent then it will be converted to content of the parent + * element. i.e. + * [code]Code [b]only[/b] allows text.[/code] + * Will become: + * Code [b]only[/b] allows text. + * Instead of: + * Code only allows text. + */ + // Ignore tags that can't be children + if (!isChildAllowed(currentTag(), token)) { + + // exclude closing tags of current tag + if (token.type !== TOKEN_CLOSE || !currentTag() || + token.name !== currentTag().name) { + token.name = '#'; + token.type = TOKEN_CONTENT; + } + } + + switch (token.type) { + case TOKEN_OPEN: + // Check it this closes a parent, + // e.g. for lists [*]one [*]two + if (closesCurrentTag(token.name)) { + openTags.pop(); + } + + addTag(token); + bbcode = bbcodeHandlers[token.name]; + + // If this tag is not self closing and it has a closing + // tag then it is open and has children so add it to the + // list of open tags. If has the closedBy property then + // it is closed by other tags so include everything as + // it's children until one of those tags is reached. + if (bbcode && !bbcode.isSelfClosing && + (bbcode.closedBy || + hasTag(token.name, TOKEN_CLOSE, toks))) { + openTags.push(token); + } else if (!bbcode || !bbcode.isSelfClosing) { + token.type = TOKEN_CONTENT; + } + break; + + case TOKEN_CLOSE: + // check if this closes the current tag, + // e.g. [/list] would close an open [*] + if (currentTag() && token.name !== currentTag().name && + closesCurrentTag('/' + token.name)) { + + openTags.pop(); + } + + // If this is closing the currently open tag just pop + // the close tag off the open tags array + if (currentTag() && token.name === currentTag().name) { + currentTag().closing = token; + openTags.pop(); + + // If this is closing an open tag that is the parent of + // the current tag then clone all the tags including the + // current one until reaching the parent that is being + // closed. Close the parent and then add the clones back + // in. + } else if (hasTag(token.name, TOKEN_OPEN, openTags)) { + + // Remove the tag from the open tags + while ((curTok = openTags.pop())) { + + // If it's the tag that is being closed then + // discard it and break the loop. + if (curTok.name === token.name) { + curTok.closing = token; + break; + } + + // Otherwise clone this tag and then add any + // previously cloned tags as it's children + clone = curTok.clone(); + + if (cloned.length) { + clone.children.push(last(cloned)); + } + + cloned.push(clone); + } + + // Place block linebreak before cloned tags + if (next && next.type === TOKEN_NEWLINE) { + bbcode = bbcodeHandlers[token.name]; + if (bbcode && bbcode.isInline === false) { + addTag(next); + toks.shift(); + } + } + + // Add the last cloned child to the now current tag + // (the parent of the tag which was being closed) + addTag(last(cloned)); + + // Add all the cloned tags to the open tags list + i = cloned.length; + while (i--) { + openTags.push(cloned[i]); + } + + cloned.length = 0; + + // This tag is closing nothing so treat it as content + } else { + token.type = TOKEN_CONTENT; + addTag(token); + } + break; + + case TOKEN_NEWLINE: + // handle things like + // [*]list\nitem\n[*]list1 + // where it should come out as + // [*]list\nitem[/*]\n[*]list1[/*] + // instead of + // [*]list\nitem\n[/*][*]list1[/*] + if (currentTag() && next && closesCurrentTag( + (next.type === TOKEN_CLOSE ? '/' : '') + + next.name + )) { + // skip if the next tag is the closing tag for + // the option tag, i.e. [/*] + if (!(next.type === TOKEN_CLOSE && + next.name === currentTag().name)) { + bbcode = bbcodeHandlers[currentTag().name]; + + if (bbcode && bbcode.breakAfter) { + openTags.pop(); + } else if (bbcode && + bbcode.isInline === false && + base.opts.breakAfterBlock && + bbcode.breakAfter !== false) { + openTags.pop(); + } + } + } + + addTag(token); + break; + + default: // content + addTag(token); + break; + } + } + + return output; + } + + /** + * Normalise all new lines + * + * Removes any formatting new lines from the BBCode + * leaving only content ones. I.e. for a list: + * + * [list] + * [*] list item one + * with a line break + * [*] list item two + * [/list] + * + * would become + * + * [list] [*] list item one + * with a line break [*] list item two [/list] + * + * Which makes it easier to convert to HTML or add + * the formatting new lines back in when converting + * back to BBCode + * + * @param {array} children + * @param {TokenizeToken} parent + * @param {boolean} onlyRemoveBreakAfter + * @return {void} + */ + function normaliseNewLines(children, parent, onlyRemoveBreakAfter) { + var token, left, right, parentBBCode, bbcode, + removedBreakEnd, removedBreakBefore, remove; + var childrenLength = children.length; + // TODO: this function really needs tidying up + if (parent) { + parentBBCode = bbcodeHandlers[parent.name]; + } + + var i = childrenLength; + while (i--) { + if (!(token = children[i])) { + continue; + } + + if (token.type === TOKEN_NEWLINE) { + left = i > 0 ? children[i - 1] : null; + right = i < childrenLength - 1 ? children[i + 1] : null; + remove = false; + + // Handle the start and end new lines + // e.g. [tag]\n and \n[/tag] + if (!onlyRemoveBreakAfter && parentBBCode && + parentBBCode.isSelfClosing !== true) { + // First child of parent so must be opening line break + // (breakStartBlock, breakStart) e.g. [tag]\n + if (!left) { + if (parentBBCode.isInline === false && + base.opts.breakStartBlock && + parentBBCode.breakStart !== false) { + remove = true; + } + + if (parentBBCode.breakStart) { + remove = true; + } + // Last child of parent so must be end line break + // (breakEndBlock, breakEnd) + // e.g. \n[/tag] + // remove last line break (breakEndBlock, breakEnd) + } else if (!removedBreakEnd && !right) { + if (parentBBCode.isInline === false && + base.opts.breakEndBlock && + parentBBCode.breakEnd !== false) { + remove = true; + } + + if (parentBBCode.breakEnd) { + remove = true; + } + + removedBreakEnd = remove; + } + } + + if (left && left.type === TOKEN_OPEN) { + if ((bbcode = bbcodeHandlers[left.name])) { + if (!onlyRemoveBreakAfter) { + if (bbcode.isInline === false && + base.opts.breakAfterBlock && + bbcode.breakAfter !== false) { + remove = true; + } + + if (bbcode.breakAfter) { + remove = true; + } + } else if (bbcode.isInline === false) { + remove = true; + } + } + } + + if (!onlyRemoveBreakAfter && !removedBreakBefore && + right && right.type === TOKEN_OPEN) { + + if ((bbcode = bbcodeHandlers[right.name])) { + if (bbcode.isInline === false && + base.opts.breakBeforeBlock && + bbcode.breakBefore !== false) { + remove = true; + } + + if (bbcode.breakBefore) { + remove = true; + } + + removedBreakBefore = remove; + + if (remove) { + children.splice(i, 1); + continue; + } + } + } + + if (remove) { + children.splice(i, 1); + } + + // reset double removedBreakBefore removal protection. + // This is needed for cases like \n\n[\tag] where + // only 1 \n should be removed but without this they both + // would be. + removedBreakBefore = false; + } else if (token.type === TOKEN_OPEN) { + normaliseNewLines(token.children, token, + onlyRemoveBreakAfter); + } + } + } + + /** + * Fixes any invalid nesting. + * + * If it is a block level element inside 1 or more inline elements + * then those inline elements will be split at the point where the + * block level is and the block level element placed between the split + * parts. i.e. + * [inline]A[blocklevel]B[/blocklevel]C[/inline] + * Will become: + * [inline]A[/inline][blocklevel]B[/blocklevel][inline]C[/inline] + * + * @param {array} children + * @param {array} [parents] Null if there is no parents + * @param {boolea} [insideInline] If inside an inline element + * @param {array} [rootArr] Root array if there is one + * @return {array} + * @private + */ + function fixNesting(children, parents, insideInline, rootArr) { + var token, i, parent, parentIndex, parentParentChildren, right; + + var isInline = function (token) { + var bbcode = bbcodeHandlers[token.name]; + + return !bbcode || bbcode.isInline !== false; + }; + + parents = parents || []; + rootArr = rootArr || children; + + // This must check the length each time as it can change when + // tokens are moved to fix the nesting. + for (i = 0; i < children.length; i++) { + if (!(token = children[i]) || token.type !== TOKEN_OPEN) { + continue; + } + + if (insideInline && !isInline(token)) { + // if this is a blocklevel element inside an inline one then + // split the parent at the block level element + parent = last(parents); + right = parent.splitAt(token); + + parentParentChildren = parents.length > 1 ? + parents[parents.length - 2].children : rootArr; + + // If parent inline is allowed inside this tag, clone it and + // wrap this tags children in it. + if (isChildAllowed(token, parent)) { + var clone = parent.clone(); + clone.children = token.children; + token.children = [clone]; + } + + parentIndex = parentParentChildren.indexOf(parent); + if (parentIndex > -1) { + // remove the block level token from the right side of + // the split inline element + right.children.splice(0, 1); + + // insert the block level token and the right side after + // the left side of the inline token + parentParentChildren.splice( + parentIndex + 1, 0, token, right + ); + + // If token is a block and is followed by a newline, + // then move the newline along with it to the new parent + var next = right.children[0]; + if (next && next.type === TOKEN_NEWLINE) { + if (!isInline(token)) { + right.children.splice(0, 1); + parentParentChildren.splice( + parentIndex + 2, 0, next + ); + } + } + + // return to parents loop as the + // children have now increased + return; + } + + } + + parents.push(token); + + fixNesting( + token.children, + parents, + insideInline || isInline(token), + rootArr + ); + + parents.pop(); + } + } + + /** + * Removes any empty BBCodes which are not allowed to be empty. + * + * @param {array} tokens + * @private + */ + function removeEmpty(tokens) { + var token, bbcode; + + /** + * Checks if all children are whitespace or not + * @private + */ + var isTokenWhiteSpace = function (children) { + var j = children.length; + + while (j--) { + var type = children[j].type; + + if (type === TOKEN_OPEN || type === TOKEN_CLOSE) { + return false; + } + + if (type === TOKEN_CONTENT && + /\S|\u00A0/.test(children[j].val)) { + return false; + } + } + + return true; + }; + + var i = tokens.length; + while (i--) { + // So skip anything that isn't a tag since only tags can be + // empty, content can't + if (!(token = tokens[i]) || token.type !== TOKEN_OPEN) { + continue; + } + + bbcode = bbcodeHandlers[token.name]; + + // Remove any empty children of this tag first so that if they + // are all removed this one doesn't think it's not empty. + removeEmpty(token.children); + + if (isTokenWhiteSpace(token.children) && bbcode && + !bbcode.isSelfClosing && !bbcode.allowsEmpty) { + tokens.splice.apply(tokens, [i, 1].concat(token.children)); + } + } + } + + /** + * Converts a BBCode string to HTML + * + * @param {string} str + * @param {boolean} preserveNewLines If to preserve all new lines, not + * strip any based on the passed + * formatting options + * @return {string} + * @memberOf BBCodeParser.prototype + */ + base.toHTML = function (str, preserveNewLines) { + return convertToHTML(base.parse(str, preserveNewLines), true); + }; + + base.toHTMLFragment = function (str, preserveNewLines) { + return convertToHTML(base.parse(str, preserveNewLines), false); + }; + + /** + * @private + */ + function convertToHTML(tokens, isRoot) { + var undef, token, bbcode, content, html, needsBlockWrap, + blockWrapOpen, isInline, lastChild, + ret = ''; + + isInline = function (bbcode) { + return (!bbcode || (bbcode.isHtmlInline !== undef ? + bbcode.isHtmlInline : bbcode.isInline)) !== false; + }; + + while (tokens.length > 0) { + if (!(token = tokens.shift())) { + continue; + } + + if (token.type === TOKEN_OPEN) { + lastChild = token.children[token.children.length - 1] || {}; + bbcode = bbcodeHandlers[token.name]; + needsBlockWrap = isRoot && isInline(bbcode); + content = convertToHTML(token.children, false); + + if (bbcode && bbcode.html) { + // Only add a line break to the end if this is + // blocklevel and the last child wasn't block-level + if (!isInline(bbcode) && + isInline(bbcodeHandlers[lastChild.name]) && + !bbcode.isPreFormatted && + !bbcode.skipLastLineBreak) { + // Add placeholder br to end of block level + // elements + content += '
    '; + } + + if (!isFunction(bbcode.html)) { + token.attrs['0'] = content; + html = formatBBCodeString( + bbcode.html, + token.attrs + ); + } else { + html = bbcode.html.call( + base, + token, + token.attrs, + content + ); + } + } else { + html = token.val + content + + (token.closing ? token.closing.val : ''); + } + } else if (token.type === TOKEN_NEWLINE) { + if (!isRoot) { + ret += '
    '; + continue; + } + + // If not already in a block wrap then start a new block + if (!blockWrapOpen) { + ret += '
    '; + } + + ret += '
    '; + + // Normally the div acts as a line-break with by moving + // whatever comes after onto a new line. + // If this is the last token, add an extra line-break so it + // shows as there will be nothing after it. + if (!tokens.length) { + ret += '
    '; + } + + ret += '
    \n'; + blockWrapOpen = false; + continue; + // content + } else { + needsBlockWrap = isRoot; + html = escapeEntities(token.val, true); + } + + if (needsBlockWrap && !blockWrapOpen) { + ret += '
    '; + blockWrapOpen = true; + } else if (!needsBlockWrap && blockWrapOpen) { + ret += '
    \n'; + blockWrapOpen = false; + } + + ret += html; + } + + if (blockWrapOpen) { + ret += '\n'; + } + + return ret; + } + + /** + * Takes a BBCode string, parses it then converts it back to BBCode. + * + * This will auto fix the BBCode and format it with the specified + * options. + * + * @param {string} str + * @param {boolean} preserveNewLines If to preserve all new lines, not + * strip any based on the passed + * formatting options + * @return {string} + * @memberOf BBCodeParser.prototype + */ + base.toBBCode = function (str, preserveNewLines) { + return convertToBBCode(base.parse(str, preserveNewLines)); + }; + + /** + * Converts parsed tokens back into BBCode with the + * formatting specified in the options and with any + * fixes specified. + * + * @param {array} toks Array of parsed tokens from base.parse() + * @return {string} + * @private + */ + function convertToBBCode(toks) { + var token, attr, bbcode, isBlock, isSelfClosing, quoteType, + breakBefore, breakStart, breakEnd, breakAfter, + ret = ''; + + while (toks.length > 0) { + if (!(token = toks.shift())) { + continue; + } + // TODO: tidy this + bbcode = bbcodeHandlers[token.name]; + isBlock = !(!bbcode || bbcode.isInline !== false); + isSelfClosing = bbcode && bbcode.isSelfClosing; + + breakBefore = (isBlock && base.opts.breakBeforeBlock && + bbcode.breakBefore !== false) || + (bbcode && bbcode.breakBefore); + + breakStart = (isBlock && !isSelfClosing && + base.opts.breakStartBlock && + bbcode.breakStart !== false) || + (bbcode && bbcode.breakStart); + + breakEnd = (isBlock && base.opts.breakEndBlock && + bbcode.breakEnd !== false) || + (bbcode && bbcode.breakEnd); + + breakAfter = (isBlock && base.opts.breakAfterBlock && + bbcode.breakAfter !== false) || + (bbcode && bbcode.breakAfter); + + quoteType = (bbcode ? bbcode.quoteType : null) || + base.opts.quoteType || QuoteType.auto; + + if (!bbcode && token.type === TOKEN_OPEN) { + ret += token.val; + + if (token.children) { + ret += convertToBBCode(token.children); + } + + if (token.closing) { + ret += token.closing.val; + } + } else if (token.type === TOKEN_OPEN) { + if (breakBefore) { + ret += '\n'; + } + + // Convert the tag and it's attributes to BBCode + ret += '[' + token.name; + if (token.attrs) { + if (token.attrs.defaultattr) { + ret += '=' + quote( + token.attrs.defaultattr, + quoteType, + 'defaultattr' + ); + + delete token.attrs.defaultattr; + } + + for (attr in token.attrs) { + if (token.attrs.hasOwnProperty(attr)) { + ret += ' ' + attr + '=' + + quote(token.attrs[attr], quoteType, attr); + } + } + } + ret += ']'; + + if (breakStart) { + ret += '\n'; + } + + // Convert the tags children to BBCode + if (token.children) { + ret += convertToBBCode(token.children); + } + + // add closing tag if not self closing + if (!isSelfClosing && !bbcode.excludeClosing) { + if (breakEnd) { + ret += '\n'; + } + + ret += '[/' + token.name + ']'; + } + + if (breakAfter) { + ret += '\n'; + } + + // preserve whatever was recognized as the + // closing tag if it is a self closing tag + if (token.closing && isSelfClosing) { + ret += token.closing.val; + } + } else { + ret += token.val; + } + } + + return ret; + } + + /** + * Quotes an attribute + * + * @param {string} str + * @param {BBCodeParser.QuoteType} quoteType + * @param {string} name + * @return {string} + * @private + */ + function quote(str, quoteType, name) { + var needsQuotes = /\s|=/.test(str); + + if (isFunction(quoteType)) { + return quoteType(str, name); + } + + if (quoteType === QuoteType.never || + (quoteType === QuoteType.auto && !needsQuotes)) { + return str; + } + + return '"' + str.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'; + } + + /** + * Returns the last element of an array or null + * + * @param {array} arr + * @return {Object} Last element + * @private + */ + function last(arr) { + if (arr.length) { + return arr[arr.length - 1]; + } + + return null; + } + + /** + * Converts a string to lowercase. + * + * @param {string} str + * @return {string} Lowercase version of str + * @private + */ + function lower(str) { + return str.toLowerCase(); + } + }; + + /** + * Quote type + * @type {Object} + * @class QuoteType + * @name BBCodeParser.QuoteType + * @since 1.4.0 + */ + BBCodeParser.QuoteType = QuoteType; + + /** + * Default BBCode parser options + * @type {Object} + */ + BBCodeParser.defaults = { + /** + * If to add a new line before block level elements + * + * @type {Boolean} + */ + breakBeforeBlock: false, + + /** + * If to add a new line after the start of block level elements + * + * @type {Boolean} + */ + breakStartBlock: false, + + /** + * If to add a new line before the end of block level elements + * + * @type {Boolean} + */ + breakEndBlock: false, + + /** + * If to add a new line after block level elements + * + * @type {Boolean} + */ + breakAfterBlock: true, + + /** + * If to remove empty tags + * + * @type {Boolean} + */ + removeEmptyTags: true, + + /** + * If to fix invalid nesting, + * i.e. block level elements inside inline elements. + * + * @type {Boolean} + */ + fixInvalidNesting: true, + + /** + * If to fix invalid children. + * i.e. A tag which is inside a parent that doesn't + * allow that type of tag. + * + * @type {Boolean} + */ + fixInvalidChildren: true, + + /** + * Attribute quote type + * + * @type {BBCodeParser.QuoteType} + * @since 1.4.1 + */ + quoteType: QuoteType.auto, + + /** + * Whether to use strict matching on attributes and styles. + * + * When true this will perform AND matching requiring all tag + * attributes and styles to match. + * + * When false will perform OR matching and will match if any of + * a tags attributes or styles match. + * + * @type {Boolean} + * @since 3.1.0 + */ + strictMatch: false + }; + + /** + * Converts a number 0-255 to hex. + * + * Will return 00 if number is not a valid number. + * + * @param {any} number + * @return {string} + * @private + */ + function toHex(number) { + number = parseInt(number, 10); + + if (isNaN(number)) { + return '00'; + } + + number = Math.max(0, Math.min(number, 255)).toString(16); + + return number.length < 2 ? '0' + number : number; + } + + /** + * Normalises a CSS colour to hex #xxxxxx format + * + * @param {string} colorStr + * @return {string} + * @private + */ + function _normaliseColour(colorStr) { + var match; + + colorStr = colorStr || '#000'; + + // rgb(n,n,n); + if ((match = + colorStr.match(/rgb\((\d{1,3}),\s*?(\d{1,3}),\s*?(\d{1,3})\)/i))) { + return '#' + + toHex(match[1]) + + toHex(match[2]) + + toHex(match[3]); + } + + // expand shorthand + if ((match = colorStr.match(/#([0-9a-f])([0-9a-f])([0-9a-f])\s*?$/i))) { + return '#' + + match[1] + match[1] + + match[2] + match[2] + + match[3] + match[3]; + } + + return colorStr; + } + + /** + * SCEditor BBCode format + * @since 2.0.0 + */ + function bbcodeFormat() { + var base = this; + + base.stripQuotes = _stripQuotes; + + /** + * cache of all the tags pointing to their bbcodes to enable + * faster lookup of which bbcode a tag should have + * @private + */ + var tagsToBBCodes = {}; + + /** + * Allowed children of specific HTML tags. Empty array if no + * children other than text nodes are allowed + * @private + */ + var validChildren = { + ul: ['li', 'ol', 'ul'], + ol: ['li', 'ol', 'ul'], + table: ['tr'], + tr: ['td', 'th'], + code: ['br', 'p', 'div'] + }; + + /** + * Populates tagsToBBCodes and stylesToBBCodes for easier lookups + * + * @private + */ + function buildBbcodeCache() { + each(bbcodeHandlers, function (bbcode, handler) { + var + isBlock = handler.isInline === false, + tags = bbcodeHandlers[bbcode].tags, + styles = bbcodeHandlers[bbcode].styles; + + if (styles) { + tagsToBBCodes['*'] = tagsToBBCodes['*'] || {}; + tagsToBBCodes['*'][isBlock] = + tagsToBBCodes['*'][isBlock] || {}; + tagsToBBCodes['*'][isBlock][bbcode] = [ + ['style', Object.entries(styles)] + ]; + } + + if (tags) { + each(tags, function (tag, values) { + if (values && values.style) { + values.style = Object.entries(values.style); + } + + tagsToBBCodes[tag] = tagsToBBCodes[tag] || {}; + tagsToBBCodes[tag][isBlock] = + tagsToBBCodes[tag][isBlock] || {}; + tagsToBBCodes[tag][isBlock][bbcode] = + values && Object.entries(values); + }); + } + }); + }; + + /** + * Handles adding newlines after block level elements + * + * @param {HTMLElement} element The element to convert + * @param {string} content The tags text content + * @return {string} + * @private + */ + function handleBlockNewlines(element, content) { + var tag = element.nodeName.toLowerCase(); + var isInline = dom.isInline; + if (!isInline(element, true) || tag === 'br') { + var isLastBlockChild, parent, parentLastChild, + previousSibling = element.previousSibling; + + // Skips selection makers and ignored elements + // Skip empty inline elements + while (previousSibling && + previousSibling.nodeType === 1 && + !is(previousSibling, 'br') && + isInline(previousSibling, true) && + !previousSibling.firstChild) { + previousSibling = previousSibling.previousSibling; + } + + // If it's the last block of an inline that is the last + // child of a block then it shouldn't cause a line break + //
    + do { + parent = element.parentNode; + parentLastChild = parent && parent.lastChild; + + isLastBlockChild = parentLastChild === element; + element = parent; + } while (parent && isLastBlockChild && isInline(parent, true)); + + // If this block is: + // * Not the last child of a block level element + // * Is a
  • tag (lists are blocks) + if (!isLastBlockChild || tag === 'li') { + content += '\n'; + } + + // Check for: + // texttext + // + // The second opening opening tag should cause a + // line break because the previous sibing is inline. + if (tag !== 'br' && previousSibling && + !is(previousSibling, 'br') && + isInline(previousSibling, true)) { + content = '\n' + content; + } + } + + return content; + } + + /** + * Handles a HTML tag and finds any matching BBCodes + * + * @param {HTMLElement} element The element to convert + * @param {string} content The Tags text content + * @param {boolean} blockLevel + * @return {string} Content with any matching BBCode tags + * wrapped around it. + * @private + */ + function handleTags(element, content, blockLevel) { + function isStyleMatch(style) { + var property = style[0]; + var values = style[1]; + var val = dom.getStyle(element, property); + var parent = element.parentNode; + + // if the parent has the same style use that instead of this one + // so you don't end up with [i]parent[i]child[/i][/i] + if (!val || parent && dom.hasStyle(parent, property, val)) { + return false; + } + + return !values || values.includes(val); + } + + function createAttributeMatch(isStrict) { + return function (attribute) { + var name = attribute[0]; + var value = attribute[1]; + + // code tags should skip most styles + if (name === 'style' && element.nodeName === 'CODE') { + return false; + } + + if (name === 'style' && value) { + return value[isStrict ? 'every' : 'some'](isStyleMatch); + } else { + var val = attr(element, name); + + return val && (!value || value.includes(val)); + } + }; + } + + function handleTag(tag) { + if (!tagsToBBCodes[tag] || !tagsToBBCodes[tag][blockLevel]) { + return; + } + + // loop all bbcodes for this tag + each(tagsToBBCodes[tag][blockLevel], function (bbcode, attrs) { + var fn, format, + isStrict = bbcodeHandlers[bbcode].strictMatch; + + if (typeof isStrict === 'undefined') { + isStrict = base.opts.strictMatch; + } + + // Skip if the element doesn't have the attribute or the + // attribute doesn't match one of the required values + fn = isStrict ? 'every' : 'some'; + if (attrs && !attrs[fn](createAttributeMatch(isStrict))) { + return; + } + + format = bbcodeHandlers[bbcode].format; + if (isFunction(format)) { + content = format.call(base, element, content); + } else { + content = _formatString(format, content); + } + return false; + }); + } + + handleTag('*'); + handleTag(element.nodeName.toLowerCase()); + return content; + } + + /** + * Converts a HTML dom element to BBCode starting from + * the innermost element and working backwards + * + * @private + * @param {HTMLElement} element + * @param {boolean} hasCodeParent + * @return {string} BBCode + * @memberOf SCEditor.plugins.bbcode.prototype + */ + function elementToBbcode(element, hasCodeParent) { + var toBBCode = function (node, hasCodeParent, vChildren) { + var ret = ''; + + dom.traverse(node, function (node) { + var content = '', + nodeType = node.nodeType, + tag = node.nodeName.toLowerCase(), + isCodeTag = tag === 'code', + isEmoticon = tag === 'img' && + !!attr(node, EMOTICON_DATA_ATTR), + vChild = validChildren[tag], + firstChild = node.firstChild, + isValidChild = true; + + if (vChildren) { + isValidChild = vChildren.indexOf(tag) > -1; + + // Emoticons should always be converted + if (isEmoticon) { + isValidChild = true; + } + + // if this tag is one of the parents allowed children + // then set this tags allowed children to whatever it + // allows, otherwise set to what the parent allows + if (!isValidChild) { + vChild = vChildren; + } + } + + // 1 = element + if (nodeType === 1) { + // skip empty nlf elements (new lines automatically + // added after block level elements like quotes) + if (is(node, '.sceditor-nlf') && !firstChild) { + return; + } + + // don't convert iframe contents + if (tag !== 'iframe') { + content = toBBCode(node, hasCodeParent || isCodeTag, + vChild); + } + + // TODO: isValidChild is no longer needed. Should use + // valid children bbcodes instead by creating BBCode + // tokens like the parser. + if (isValidChild) { + // Emoticons should be converted if they have found + // their way into a code tag + if (!hasCodeParent || isEmoticon) { + if (!isCodeTag) { + // Parse inline codes first so they don't + // contain block level codes + content = handleTags(node, content, false); + } + + content = handleTags(node, content, true); + } + ret += handleBlockNewlines(node, content); + } else { + ret += content; + } + // 3 = text + } else if (nodeType === 3) { + ret += node.nodeValue; + } + }, false, true); + + return ret; + }; + + return toBBCode(element, hasCodeParent); + }; + + /** + * Initializer + * @private + */ + base.init = function () { + base.opts = this.opts; + base.elementToBbcode = elementToBbcode; + + // build the BBCode cache + buildBbcodeCache(); + + this.commands = extend( + true, {}, defaultCommandsOverrides, this.commands + ); + + // Add BBCode helper methods + this.toBBCode = base.toSource; + this.fromBBCode = base.toHtml; + }; + + /** + * Converts BBCode into HTML + * + * @param {boolean} asFragment + * @param {string} source + * @param {boolean} [legacyAsFragment] Used by fromBBCode() method + */ + function toHtml(asFragment, source, legacyAsFragment) { + var parser = new BBCodeParser(base.opts.parserOptions); + var toHTML = (asFragment || legacyAsFragment) ? + parser.toHTMLFragment : + parser.toHTML; + + return toHTML(base.opts.bbcodeTrim ? source.trim() : source); + } + + /** + * Converts HTML into BBCode + * + * @param {boolean} asFragment + * @param {string} html + * @param {!Document} [context] + * @param {!HTMLElement} [parent] + * @return {string} + * @private + */ + function toSource(asFragment, html, context, parent) { + context = context || document; + + var bbcode, elements; + var hasCodeParent = !!dom.closest(parent, 'code'); + var containerParent = context.createElement('div'); + var container = context.createElement('div'); + var parser = new BBCodeParser(base.opts.parserOptions); + + container.innerHTML = html; + css(containerParent, 'visibility', 'hidden'); + containerParent.appendChild(container); + context.body.appendChild(containerParent); + + if (asFragment) { + // Add text before and after so removeWhiteSpace doesn't remove + // leading and trailing whitespace + containerParent.insertBefore( + context.createTextNode('#'), + containerParent.firstChild + ); + containerParent.appendChild(context.createTextNode('#')); + } + + // Match parents white-space handling + if (parent) { + css(container, 'whiteSpace', css(parent, 'whiteSpace')); + } + + // Remove all nodes with sceditor-ignore class + elements = container.getElementsByClassName('sceditor-ignore'); + while (elements.length) { + elements[0].parentNode.removeChild(elements[0]); + } + + dom.removeWhiteSpace(containerParent); + + bbcode = elementToBbcode(container, hasCodeParent); + + context.body.removeChild(containerParent); + + bbcode = parser.toBBCode(bbcode, true); + + if (base.opts.bbcodeTrim) { + bbcode = bbcode.trim(); + } + + return bbcode; + }; + + base.toHtml = toHtml.bind(null, false); + base.fragmentToHtml = toHtml.bind(null, true); + base.toSource = toSource.bind(null, false); + base.fragmentToSource = toSource.bind(null, true); + }; + + /** + * Gets a BBCode + * + * @param {string} name + * @return {Object|null} + * @since 2.0.0 + */ + bbcodeFormat.get = function (name) { + return bbcodeHandlers[name] || null; + }; + + /** + * Adds a BBCode to the parser or updates an existing + * BBCode if a BBCode with the specified name already exists. + * + * @param {string} name + * @param {Object} bbcode + * @return {this} + * @since 2.0.0 + */ + bbcodeFormat.set = function (name, bbcode) { + if (name && bbcode) { + // merge any existing command properties + bbcode = extend(bbcodeHandlers[name] || {}, bbcode); + + bbcode.remove = function () { + delete bbcodeHandlers[name]; + }; + + bbcodeHandlers[name] = bbcode; + } + + return this; + }; + + /** + * Renames a BBCode + * + * This does not change the format or HTML handling, those must be + * changed manually. + * + * @param {string} name [description] + * @param {string} newName [description] + * @return {this|false} + * @since 2.0.0 + */ + bbcodeFormat.rename = function (name, newName) { + if (name in bbcodeHandlers) { + bbcodeHandlers[newName] = bbcodeHandlers[name]; + + delete bbcodeHandlers[name]; + } + + return this; + }; + + /** + * Removes a BBCode + * + * @param {string} name + * @return {this} + * @since 2.0.0 + */ + bbcodeFormat.remove = function (name) { + if (name in bbcodeHandlers) { + delete bbcodeHandlers[name]; + } + + return this; + }; + + bbcodeFormat.formatBBCodeString = formatBBCodeString; + + sceditor.formats.bbcode = bbcodeFormat; + sceditor.BBCodeParser = BBCodeParser; +}(sceditor)); diff --git a/client/src/sceditor/development/formats/xhtml.js b/client/src/sceditor/development/formats/xhtml.js new file mode 100644 index 0000000..2961308 --- /dev/null +++ b/client/src/sceditor/development/formats/xhtml.js @@ -0,0 +1,1269 @@ +/** + * SCEditor XHTML Plugin + * http://www.sceditor.com/ + * + * Copyright (C) 2017, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * @author Sam Clarke + */ +(function (sceditor) { + 'use strict'; + + var dom = sceditor.dom; + var utils = sceditor.utils; + + var css = dom.css; + var attr = dom.attr; + var is = dom.is; + var removeAttr = dom.removeAttr; + var convertElement = dom.convertElement; + var extend = utils.extend; + var each = utils.each; + var isEmptyObject = utils.isEmptyObject; + + var getEditorCommand = sceditor.command.get; + + var defaultCommandsOverrides = { + bold: { + txtExec: ['', ''] + }, + italic: { + txtExec: ['', ''] + }, + underline: { + txtExec: ['', ''] + }, + strike: { + txtExec: ['', ''] + }, + subscript: { + txtExec: ['', ''] + }, + superscript: { + txtExec: ['', ''] + }, + left: { + txtExec: ['
    ', '
    '] + }, + center: { + txtExec: ['
    ', '
    '] + }, + right: { + txtExec: ['
    ', '
    '] + }, + justify: { + txtExec: ['
    ', '
    '] + }, + font: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('font')._dropDown( + editor, + caller, + function (font) { + editor.insertText('', ''); + } + ); + } + }, + size: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('size')._dropDown( + editor, + caller, + function (size) { + editor.insertText('', ''); + } + ); + } + }, + color: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('color')._dropDown( + editor, + caller, + function (color) { + editor.insertText('', ''); + } + ); + } + }, + bulletlist: { + txtExec: ['
    • ', '
    '] + }, + orderedlist: { + txtExec: ['
    1. ', '
    '] + }, + table: { + txtExec: ['
    ', '
    '] + }, + horizontalrule: { + txtExec: ['
    '] + }, + code: { + txtExec: ['', ''] + }, + image: { + txtExec: function (caller, selected) { + var editor = this; + + getEditorCommand('image')._dropDown( + editor, + caller, + selected, + function (url, width, height) { + var attrs = ''; + + if (width) { + attrs += ' width="' + width + '"'; + } + + if (height) { + attrs += ' height="' + height + '"'; + } + + editor.insertText( + '' + ); + } + ); + } + }, + email: { + txtExec: function (caller, selected) { + var editor = this; + + getEditorCommand('email')._dropDown( + editor, + caller, + function (url, text) { + editor.insertText( + '' + + (text || selected || url) + + '' + ); + } + ); + } + }, + link: { + txtExec: function (caller, selected) { + var editor = this; + + getEditorCommand('link')._dropDown( + editor, + caller, + function (url, text) { + editor.insertText( + '' + + (text || selected || url) + + '' + ); + } + ); + } + }, + quote: { + txtExec: ['
    ', '
    '] + }, + youtube: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('youtube')._dropDown( + editor, + caller, + function (id, time) { + editor.insertText( + '' + ); + } + ); + } + }, + rtl: { + txtExec: ['
    ', '
    '] + }, + ltr: { + txtExec: ['
    ', '
    '] + } + }; + + /** + * XHTMLSerializer part of the XHTML plugin. + * + * @class XHTMLSerializer + * @name jQuery.sceditor.XHTMLSerializer + * @since v1.4.1 + */ + sceditor.XHTMLSerializer = function () { + var base = this; + + var opts = { + indentStr: '\t' + }; + + /** + * Array containing the output, used as it's faster + * than string concatenation in slow browsers. + * @type {Array} + * @private + */ + var outputStringBuilder = []; + + /** + * Current indention level + * @type {number} + * @private + */ + var currentIndent = 0; + + // TODO: use escape.entities + /** + * Escapes XHTML entities + * + * @param {string} str + * @return {string} + * @private + */ + function escapeEntities(str) { + var entities = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\xa0': ' ' + }; + + return !str ? '' : str.replace(/[&<>"\xa0]/g, function (entity) { + return entities[entity] || entity; + }); + }; + + /** + * Replace spaces including newlines with a single + * space except for non-breaking spaces + * + * @param {string} str + * @return {string} + * @private + */ + function trim(str) { + return str.replace(/[^\S\u00A0]+/g, ' '); + }; + + /** + * Serializes a node to XHTML + * + * @param {Node} node Node to serialize + * @param {boolean} onlyChildren If to only serialize the nodes + * children and not the node + * itself + * @return {string} The serialized node + * @name serialize + * @memberOf jQuery.sceditor.XHTMLSerializer.prototype + * @since v1.4.1 + */ + base.serialize = function (node, onlyChildren) { + outputStringBuilder = []; + + if (onlyChildren) { + node = node.firstChild; + + while (node) { + serializeNode(node); + node = node.nextSibling; + } + } else { + serializeNode(node); + } + + return outputStringBuilder.join(''); + }; + + /** + * Serializes a node to the outputStringBuilder + * + * @param {Node} node + * @return {void} + * @private + */ + function serializeNode(node, parentIsPre) { + switch (node.nodeType) { + case 1: // element + handleElement(node, parentIsPre); + break; + + case 3: // text + handleText(node, parentIsPre); + break; + + case 4: // cdata section + handleCdata(node); + break; + + case 8: // comment + handleComment(node); + break; + + case 9: // document + case 11: // document fragment + handleDoc(node); + break; + + // Ignored types + case 2: // attribute + case 5: // entity ref + case 6: // entity + case 7: // processing instruction + case 10: // document type + case 12: // notation + break; + } + }; + + /** + * Handles doc node + * @param {Node} node + * @return {void} + * @private + */ + function handleDoc(node) { + var child = node.firstChild; + + while (child) { + serializeNode(child); + child = child.nextSibling; + } + }; + + /** + * Handles element nodes + * @param {Node} node + * @return {void} + * @private + */ + function handleElement(node, parentIsPre) { + var child, attr, attrValue, + tagName = node.nodeName.toLowerCase(), + isIframe = tagName === 'iframe', + attrIdx = node.attributes.length, + firstChild = node.firstChild, + // pre || pre-wrap with any vendor prefix + isPre = parentIsPre || + /pre(?:\-wrap)?$/i.test(css(node, 'whiteSpace')), + selfClosing = !node.firstChild && !dom.canHaveChildren(node) && + !isIframe; + + if (is(node, '.sceditor-ignore')) { + return; + } + + output('<' + tagName, !parentIsPre && canIndent(node)); + while (attrIdx--) { + attr = node.attributes[attrIdx]; + + attrValue = attr.value; + + output(' ' + attr.name.toLowerCase() + '="' + + escapeEntities(attrValue) + '"', false); + } + output(selfClosing ? ' />' : '>', false); + + if (!isIframe) { + child = firstChild; + } + + while (child) { + currentIndent++; + + serializeNode(child, isPre); + child = child.nextSibling; + + currentIndent--; + } + + if (!selfClosing) { + output( + '', + !isPre && !isIframe && canIndent(node) && + firstChild && canIndent(firstChild) + ); + } + }; + + /** + * Handles CDATA nodes + * @param {Node} node + * @return {void} + * @private + */ + function handleCdata(node) { + output(''); + }; + + /** + * Handles comment nodes + * @param {Node} node + * @return {void} + * @private + */ + function handleComment(node) { + output(''); + }; + + /** + * Handles text nodes + * @param {Node} node + * @return {void} + * @private + */ + function handleText(node, parentIsPre) { + var text = node.nodeValue; + + if (!parentIsPre) { + text = trim(text); + } + + if (text) { + output(escapeEntities(text), !parentIsPre && canIndent(node)); + } + }; + + /** + * Adds a string to the outputStringBuilder. + * + * The string will be indented unless indent is set to boolean false. + * @param {string} str + * @param {boolean} indent + * @return {void} + * @private + */ + function output(str, indent) { + var i = currentIndent; + + if (indent !== false) { + // Don't add a new line if it's the first element + if (outputStringBuilder.length) { + outputStringBuilder.push('\n'); + } + + while (i--) { + outputStringBuilder.push(opts.indentStr); + } + } + + outputStringBuilder.push(str); + }; + + /** + * Checks if should indent the node or not + * @param {Node} node + * @return {boolean} + * @private + */ + function canIndent(node) { + var prev = node.previousSibling; + + if (node.nodeType !== 1 && prev) { + return !dom.isInline(prev); + } + + // first child of a block element + if (!prev && !dom.isInline(node.parentNode)) { + return true; + } + + return !dom.isInline(node); + }; + }; + + /** + * SCEditor XHTML plugin + * @class xhtml + * @name jQuery.sceditor.plugins.xhtml + * @since v1.4.1 + */ + function xhtmlFormat() { + var base = this; + + /** + * Tag converters cache + * @type {Object} + * @private + */ + var tagConvertersCache = {}; + + /** + * Attributes filter cache + * @type {Object} + * @private + */ + var attrsCache = {}; + + /** + * Init + * @return {void} + */ + base.init = function () { + if (!isEmptyObject(xhtmlFormat.converters || {})) { + each( + xhtmlFormat.converters, + function (idx, converter) { + each(converter.tags, function (tagname) { + if (!tagConvertersCache[tagname]) { + tagConvertersCache[tagname] = []; + } + + tagConvertersCache[tagname].push(converter); + }); + } + ); + } + + this.commands = extend(true, + {}, defaultCommandsOverrides, this.commands); + }; + + /** + * Converts the WYSIWYG content to XHTML + * + * @param {boolean} isFragment + * @param {string} html + * @param {Document} context + * @param {HTMLElement} [parent] + * @return {string} + * @memberOf jQuery.sceditor.plugins.xhtml.prototype + */ + function toSource(isFragment, html, context) { + var xhtml, + container = context.createElement('div'); + container.innerHTML = html; + + css(container, 'visibility', 'hidden'); + context.body.appendChild(container); + + convertTags(container); + removeTags(container); + removeAttribs(container); + + if (!isFragment) { + wrapInlines(container); + } + + xhtml = (new sceditor.XHTMLSerializer()).serialize(container, true); + + context.body.removeChild(container); + + return xhtml; + }; + + base.toSource = toSource.bind(null, false); + + base.fragmentToSource = toSource.bind(null, true);; + + /** + * Runs all converters for the specified tagName + * against the DOM node. + * @param {string} tagName + * @return {Node} node + * @private + */ + function convertNode(tagName, node) { + if (!tagConvertersCache[tagName]) { + return; + } + + tagConvertersCache[tagName].forEach(function (converter) { + if (converter.tags[tagName]) { + each(converter.tags[tagName], function (attr, values) { + if (!node.getAttributeNode) { + return; + } + + attr = node.getAttributeNode(attr); + + if (!attr || values && values.indexOf(attr.value) < 0) { + return; + } + + converter.conv.call(base, node); + }); + } else if (converter.conv) { + converter.conv.call(base, node); + } + }); + }; + + /** + * Converts any tags/attributes to their XHTML equivalents + * @param {Node} node + * @return {void} + * @private + */ + function convertTags(node) { + dom.traverse(node, function (node) { + var tagName = node.nodeName.toLowerCase(); + + convertNode('*', node); + convertNode(tagName, node); + }, true); + }; + + /** + * Tests if a node is empty and can be removed. + * + * @param {Node} node + * @return {boolean} + * @private + */ + function isEmpty(node, excludeBr) { + var rect, + childNodes = node.childNodes, + tagName = node.nodeName.toLowerCase(), + nodeValue = node.nodeValue, + childrenLength = childNodes.length, + allowedEmpty = xhtmlFormat.allowedEmptyTags || []; + + if (excludeBr && tagName === 'br') { + return true; + } + + if (is(node, '.sceditor-ignore')) { + return true; + } + + if (allowedEmpty.indexOf(tagName) > -1 || tagName === 'td' || + !dom.canHaveChildren(node)) { + + return false; + } + + // \S|\u00A0 = any non space char + if (nodeValue && /\S|\u00A0/.test(nodeValue)) { + return false; + } + + while (childrenLength--) { + if (!isEmpty(childNodes[childrenLength], + excludeBr && !node.previousSibling && !node.nextSibling)) { + return false; + } + } + + // Treat tags with a width and height from CSS as not empty + if (node.getBoundingClientRect && + (node.className || node.hasAttributes('style'))) { + rect = node.getBoundingClientRect(); + return !rect.width || !rect.height; + } + + return true; + }; + + /** + * Removes any tags that are not white listed or if no + * tags are white listed it will remove any tags that + * are black listed. + * + * @param {Node} rootNode + * @return {void} + * @private + */ + function removeTags(rootNode) { + dom.traverse(rootNode, function (node) { + var remove, + tagName = node.nodeName.toLowerCase(), + parentNode = node.parentNode, + nodeType = node.nodeType, + isBlock = !dom.isInline(node), + previousSibling = node.previousSibling, + nextSibling = node.nextSibling, + isTopLevel = parentNode === rootNode, + noSiblings = !previousSibling && !nextSibling, + empty = tagName !== 'iframe' && isEmpty(node, + isTopLevel && noSiblings && tagName !== 'br'), + document = node.ownerDocument, + allowedTags = xhtmlFormat.allowedTags, + firstChild = node.firstChild, + disallowedTags = xhtmlFormat.disallowedTags; + + // 3 = text node + if (nodeType === 3) { + return; + } + + if (nodeType === 4) { + tagName = '!cdata'; + } else if (tagName === '!' || nodeType === 8) { + tagName = '!comment'; + } + + if (nodeType === 1) { + // skip empty nlf elements (new lines automatically + // added after block level elements like quotes) + if (is(node, '.sceditor-nlf')) { + if (!firstChild || (node.childNodes.length === 1 && + /br/i.test(firstChild.nodeName))) { + // Mark as empty,it will be removed by the next code + empty = true; + } else { + node.classList.remove('sceditor-nlf'); + + if (!node.className) { + removeAttr(node, 'class'); + } + } + } + } + + if (empty) { + remove = true; + // 3 is text node which do not get filtered + } else if (allowedTags && allowedTags.length) { + remove = (allowedTags.indexOf(tagName) < 0); + } else if (disallowedTags && disallowedTags.length) { + remove = (disallowedTags.indexOf(tagName) > -1); + } + + if (remove) { + if (!empty) { + if (isBlock && previousSibling && + dom.isInline(previousSibling)) { + parentNode.insertBefore( + document.createTextNode(' '), node); + } + + // Insert all the childen after node + while (node.firstChild) { + parentNode.insertBefore(node.firstChild, + nextSibling); + } + + if (isBlock && nextSibling && + dom.isInline(nextSibling)) { + parentNode.insertBefore( + document.createTextNode(' '), nextSibling); + } + } + + parentNode.removeChild(node); + } + }, true); + }; + + /** + * Merges two sets of attribute filters into one + * + * @param {Object} filtersA + * @param {Object} filtersB + * @return {Object} + * @private + */ + function mergeAttribsFilters(filtersA, filtersB) { + var ret = {}; + + if (filtersA) { + ret = extend({}, ret, filtersA); + } + + if (!filtersB) { + return ret; + } + + each(filtersB, function (attrName, values) { + if (Array.isArray(values)) { + ret[attrName] = (ret[attrName] || []).concat(values); + } else if (!ret[attrName]) { + ret[attrName] = null; + } + }); + + return ret; + }; + + /** + * Wraps adjacent inline child nodes of root + * in paragraphs. + * + * @param {Node} root + * @private + */ + function wrapInlines(root) { + // Strip empty text nodes so they don't get wrapped. + dom.removeWhiteSpace(root); + + var wrapper; + var node = root.firstChild; + var next; + while (node) { + next = node.nextSibling; + + if (dom.isInline(node) && !is(node, '.sceditor-ignore')) { + if (!wrapper) { + wrapper = root.ownerDocument.createElement('p'); + node.parentNode.insertBefore(wrapper, node); + } + + wrapper.appendChild(node); + } else { + wrapper = null; + } + + node = next; + } + }; + + /** + * Removes any attributes that are not white listed or + * if no attributes are white listed it will remove + * any attributes that are black listed. + * @param {Node} node + * @return {void} + * @private + */ + function removeAttribs(node) { + var tagName, attr, attrName, attrsLength, validValues, remove, + allowedAttribs = xhtmlFormat.allowedAttribs, + isAllowed = allowedAttribs && + !isEmptyObject(allowedAttribs), + disallowedAttribs = xhtmlFormat.disallowedAttribs, + isDisallowed = disallowedAttribs && + !isEmptyObject(disallowedAttribs); + + attrsCache = {}; + + dom.traverse(node, function (node) { + if (!node.attributes) { + return; + } + + tagName = node.nodeName.toLowerCase(); + attrsLength = node.attributes.length; + + if (attrsLength) { + if (!attrsCache[tagName]) { + if (isAllowed) { + attrsCache[tagName] = mergeAttribsFilters( + allowedAttribs['*'], + allowedAttribs[tagName] + ); + } else { + attrsCache[tagName] = mergeAttribsFilters( + disallowedAttribs['*'], + disallowedAttribs[tagName] + ); + } + } + + while (attrsLength--) { + attr = node.attributes[attrsLength]; + attrName = attr.name; + validValues = attrsCache[tagName][attrName]; + remove = false; + + if (isAllowed) { + remove = validValues !== null && + (!Array.isArray(validValues) || + validValues.indexOf(attr.value) < 0); + } else if (isDisallowed) { + remove = validValues === null || + (Array.isArray(validValues) && + validValues.indexOf(attr.value) > -1); + } + + if (remove) { + node.removeAttribute(attrName); + } + } + } + }); + }; + }; + + /** + * Tag conveters, a converter is applied to all + * tags that match the criteria. + * @type {Array} + * @name jQuery.sceditor.plugins.xhtml.converters + * @since v1.4.1 + */ + xhtmlFormat.converters = [ + { + tags: { + '*': { + width: null + } + }, + conv: function (node) { + css(node, 'width', attr(node, 'width')); + removeAttr(node, 'width'); + } + }, + { + tags: { + '*': { + height: null + } + }, + conv: function (node) { + css(node, 'height', attr(node, 'height')); + removeAttr(node, 'height'); + } + }, + { + tags: { + 'li': { + value: null + } + }, + conv: function (node) { + removeAttr(node, 'value'); + } + }, + { + tags: { + '*': { + text: null + } + }, + conv: function (node) { + css(node, 'color', attr(node, 'text')); + removeAttr(node, 'text'); + } + }, + { + tags: { + '*': { + color: null + } + }, + conv: function (node) { + css(node, 'color', attr(node, 'color')); + removeAttr(node, 'color'); + } + }, + { + tags: { + '*': { + face: null + } + }, + conv: function (node) { + css(node, 'fontFamily', attr(node, 'face')); + removeAttr(node, 'face'); + } + }, + { + tags: { + '*': { + align: null + } + }, + conv: function (node) { + css(node, 'textAlign', attr(node, 'align')); + removeAttr(node, 'align'); + } + }, + { + tags: { + '*': { + border: null + } + }, + conv: function (node) { + css(node, 'borderWidth', attr(node, 'border')); + removeAttr(node, 'border'); + } + }, + { + tags: { + applet: { + name: null + }, + img: { + name: null + }, + layer: { + name: null + }, + map: { + name: null + }, + object: { + name: null + }, + param: { + name: null + } + }, + conv: function (node) { + if (!attr(node, 'id')) { + attr(node, 'id', attr(node, 'name')); + } + + removeAttr(node, 'name'); + } + }, + { + tags: { + '*': { + vspace: null + } + }, + conv: function (node) { + css(node, 'marginTop', attr(node, 'vspace') - 0); + css(node, 'marginBottom', attr(node, 'vspace') - 0); + removeAttr(node, 'vspace'); + } + }, + { + tags: { + '*': { + hspace: null + } + }, + conv: function (node) { + css(node, 'marginLeft', attr(node, 'hspace') - 0); + css(node, 'marginRight', attr(node, 'hspace') - 0); + removeAttr(node, 'hspace'); + } + }, + { + tags: { + 'hr': { + noshade: null + } + }, + conv: function (node) { + css(node, 'borderStyle', 'solid'); + removeAttr(node, 'noshade'); + } + }, + { + tags: { + '*': { + nowrap: null + } + }, + conv: function (node) { + css(node, 'whiteSpace', 'nowrap'); + removeAttr(node, 'nowrap'); + } + }, + { + tags: { + big: null + }, + conv: function (node) { + css(convertElement(node, 'span'), 'fontSize', 'larger'); + } + }, + { + tags: { + small: null + }, + conv: function (node) { + css(convertElement(node, 'span'), 'fontSize', 'smaller'); + } + }, + { + tags: { + b: null + }, + conv: function (node) { + convertElement(node, 'strong'); + } + }, + { + tags: { + u: null + }, + conv: function (node) { + css(convertElement(node, 'span'), 'textDecoration', + 'underline'); + } + }, + { + tags: { + s: null, + strike: null + }, + conv: function (node) { + css(convertElement(node, 'span'), 'textDecoration', + 'line-through'); + } + }, + { + tags: { + dir: null + }, + conv: function (node) { + convertElement(node, 'ul'); + } + }, + { + tags: { + center: null + }, + conv: function (node) { + css(convertElement(node, 'div'), 'textAlign', 'center'); + } + }, + { + tags: { + font: { + size: null + } + }, + conv: function (node) { + css(node, 'fontSize', css(node, 'fontSize')); + removeAttr(node, 'size'); + } + }, + { + tags: { + font: null + }, + conv: function (node) { + // All it's attributes will be converted + // by the attribute converters + convertElement(node, 'span'); + } + }, + { + tags: { + '*': { + type: ['_moz'] + } + }, + conv: function (node) { + removeAttr(node, 'type'); + } + }, + { + tags: { + '*': { + '_moz_dirty': null + } + }, + conv: function (node) { + removeAttr(node, '_moz_dirty'); + } + }, + { + tags: { + '*': { + '_moz_editor_bogus_node': null + } + }, + conv: function (node) { + node.parentNode.removeChild(node); + } + }, + { + tags: { + '*': { + 'data-sce-target': null + } + }, + conv: function (node) { + var rel = attr(node, 'rel') || ''; + var target = attr(node, 'data-sce-target'); + + // Only allow the value _blank and only on links + if (target === '_blank' && is(node, 'a')) { + if (!/(^|\s)noopener(\s|$)/.test(rel)) { + attr(node, 'rel', 'noopener' + (rel ? ' ' + rel : '')); + } + + attr(node, 'target', target); + } + + + removeAttr(node, 'data-sce-target'); + } + }, + { + tags: { + code: null + }, + conv: function (node) { + var node, nodes = node.getElementsByTagName('div'); + while ((node = nodes[0])) { + node.style.display = 'block'; + convertElement(node, 'span'); + } + } + } + ]; + + /** + * Allowed attributes map. + * + * To allow an attribute for all tags use * as the tag name. + * + * Leave empty or null to allow all attributes. (the disallow + * list will be used to filter them instead) + * @type {Object} + * @name jQuery.sceditor.plugins.xhtml.allowedAttribs + * @since v1.4.1 + */ + xhtmlFormat.allowedAttribs = {}; + + /** + * Attributes that are not allowed. + * + * Only used if allowed attributes is null or empty. + * @type {Object} + * @name jQuery.sceditor.plugins.xhtml.disallowedAttribs + * @since v1.4.1 + */ + xhtmlFormat.disallowedAttribs = {}; + + /** + * Array containing all the allowed tags. + * + * If null or empty all tags will be allowed. + * @type {Array} + * @name jQuery.sceditor.plugins.xhtml.allowedTags + * @since v1.4.1 + */ + xhtmlFormat.allowedTags = []; + + /** + * Array containing all the disallowed tags. + * + * Only used if allowed tags is null or empty. + * @type {Array} + * @name jQuery.sceditor.plugins.xhtml.disallowedTags + * @since v1.4.1 + */ + xhtmlFormat.disallowedTags = []; + + /** + * Array containing tags which should not be removed when empty. + * + * @type {Array} + * @name jQuery.sceditor.plugins.xhtml.allowedEmptyTags + * @since v2.0.0 + */ + xhtmlFormat.allowedEmptyTags = []; + + sceditor.formats.xhtml = xhtmlFormat; +}(sceditor)); diff --git a/client/src/sceditor/development/icons/material.js b/client/src/sceditor/development/icons/material.js new file mode 100644 index 0000000..acad572 --- /dev/null +++ b/client/src/sceditor/development/icons/material.js @@ -0,0 +1,132 @@ +/** + * SCEditor SVG material icons plugin + * http://www.sceditor.com/ + * + * Copyright (C) 2017, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * @author Sam Clarke + */ +(function (document, sceditor) { + 'use strict'; + + var dom = sceditor.dom; + + /** + * Material icons by Google (Apache license) + * https://github.com/google/material-design-icons/blob/master/LICENSE + * + * Extra icons by materialdesignicons.com and contributors (MIT license) + * https://github.com/Templarian/MaterialDesign-SVG/blob/master/LICENSE + */ + /* eslint max-len: off*/ + var icons = { + 'bold': '', + 'bulletlist': '', + 'center': '', + // Cody @XT3000 - https://materialdesignicons.com/ + 'code': '', + 'color': '', + 'copy': '', + 'cut': '', + 'date': '', + 'email': '', + 'emoticon': '', + // JapanYoshi @japanyoshilol - https://materialdesignicons.com/ + 'font': '', + 'format': '', + // Austin Andrews @Templarian - https://materialdesignicons.com/ + 'grip': '', + // Sam Clarke @samclarke + 'horizontalrule': '', + 'image': '', + 'indent': '', + 'italic': '', + 'justify': ' ', + 'left': ' ', + 'link': '', + 'ltr': '', + // Austin Andrews @Templarian - https://materialdesignicons.com/ + 'maximize': '', + 'orderedlist': '', + 'outdent': '', + 'paste': '', + 'pastetext': '', + 'print': '', + 'quote': '', + 'redo': '', + 'removeformat': '', + 'right': '', + 'rtl': '', + 'size': '', + 'source': '', + 'strike': '', + // Austin Andrews @Templarian - https://materialdesignicons.com/ + 'subscript': '', + // Austin Andrews @Templarian - https://materialdesignicons.com/ + 'superscript': '', + // Austin Andrews @Templarian - https://materialdesignicons.com/ + 'table': '', + 'time': '', + 'underline': '', + 'undo': '', + // Austin Andrews @Templarian - https://materialdesignicons.com/ + 'unlink': '', + 'youtube': '' + }; + + sceditor.icons.material = function () { + var nodes = {}; + + var colorPath; + + return { + create: function (command) { + if (command in icons) { + // Using viewbox="1 1 22 22" to trim off the 1 unit border + // around the SVG icons. + // Default is viewbox="0 0 24 24" + nodes[command] = sceditor.dom.parseHTML( + '' + + icons[command] + + '' + ).firstChild; + + if (command === 'color') { + colorPath = nodes[command].querySelector('.sce-color'); + } + } + + return nodes[command]; + }, + update: function (isSourceMode, currentNode) { + if (colorPath) { + var color = 'inherit'; + + if (!isSourceMode && currentNode) { + color = currentNode.ownerDocument + .queryCommandValue('forecolor'); + } + + dom.css(colorPath, 'fill', color); + } + }, + rtl: function (isRtl) { + var gripNode = nodes.grip; + + if (gripNode) { + var transform = isRtl ? 'scaleX(-1)' : ''; + + dom.css(gripNode, 'transform', transform); + dom.css(gripNode, 'msTransform', transform); + dom.css(gripNode, 'webkitTransform', transform); + } + } + }; + }; + + sceditor.icons.material.icons = icons; +})(document, sceditor); diff --git a/client/src/sceditor/development/icons/monocons.js b/client/src/sceditor/development/icons/monocons.js new file mode 100644 index 0000000..ed027d7 --- /dev/null +++ b/client/src/sceditor/development/icons/monocons.js @@ -0,0 +1,112 @@ +/** + * SCEditor SVG monocons plugin + * http://www.sceditor.com/ + * + * Copyright (C) 2017, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * @author Sam Clarke + */ +(function (document, sceditor) { + 'use strict'; + + var dom = sceditor.dom; + + /* eslint max-len: off*/ + var icons = { + 'bold': 'B', + 'bulletlist': '', + 'center': '', + 'code': '', + 'color': 'A', + 'copy': '', + 'cut': '', + 'date': '', + 'email': '', + 'emoticon': '', + 'font': '', + 'format': '', + 'grip': '', + 'horizontalrule': '', + 'image': '', + 'indent': '', + 'italic': 'i', + 'justify': '', + 'left': '', + 'link': '', + 'ltr': '', + 'maximize': '', + 'orderedlist': '', + 'outdent': '', + 'paste': '', + 'pastetext': '', + 'print': '', + 'quote': '', + 'redo': '', + 'removeformat': '', + 'right': '', + 'rtl': '', + 'size': '', + 'source': '', + 'strike': 'S', + 'subscript': '', + 'superscript': '', + 'table': '', + 'time': '', + 'underline': 'U', + 'undo': '', + 'unlink': '', + 'youtube': '' + }; + + sceditor.icons.monocons = function () { + var nodes = {}; + var colorPath; + + return { + create: function (command) { + if (command in icons) { + nodes[command] = sceditor.dom.parseHTML( + '' + + icons[command] + + '' + ).firstChild; + + if (command === 'color') { + colorPath = nodes[command].querySelector('.sce-color'); + } + } + + return nodes[command]; + }, + update: function (isSourceMode, currentNode) { + if (colorPath) { + var color = 'inherit'; + + if (!isSourceMode && currentNode) { + color = currentNode.ownerDocument + .queryCommandValue('forecolor'); + } + + dom.css(colorPath, 'fill', color); + } + }, + rtl: function (isRtl) { + var gripNode = nodes.grip; + + if (gripNode) { + var transform = isRtl ? 'scaleX(-1)' : ''; + + dom.css(gripNode, 'transform', transform); + dom.css(gripNode, 'msTransform', transform); + dom.css(gripNode, 'webkitTransform', transform); + } + } + }; + }; + + sceditor.icons.monocons.icons = icons; +})(document, sceditor); diff --git a/client/src/sceditor/development/jquery.sceditor.bbcode.js b/client/src/sceditor/development/jquery.sceditor.bbcode.js new file mode 100644 index 0000000..ee5d57d --- /dev/null +++ b/client/src/sceditor/development/jquery.sceditor.bbcode.js @@ -0,0 +1,12209 @@ +(function ($) { + 'use strict'; + + function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; } + + var $__default = /*#__PURE__*/_interopDefaultLegacy($); + + /** + * Check if the passed argument is the + * the passed type. + * + * @param {string} type + * @param {*} arg + * @returns {boolean} + */ + function isTypeof(type, arg) { + return typeof arg === type; + } + + /** + * @type {function(*): boolean} + */ + var isString = isTypeof.bind(null, 'string'); + + /** + * @type {function(*): boolean} + */ + var isUndefined = isTypeof.bind(null, 'undefined'); + + /** + * @type {function(*): boolean} + */ + var isFunction = isTypeof.bind(null, 'function'); + + /** + * @type {function(*): boolean} + */ + var isNumber = isTypeof.bind(null, 'number'); + + + /** + * Returns true if an object has no keys + * + * @param {!Object} obj + * @returns {boolean} + */ + function isEmptyObject(obj) { + return !Object.keys(obj).length; + } + + /** + * Extends the first object with any extra objects passed + * + * If the first argument is boolean and set to true + * it will extend child arrays and objects recursively. + * + * @param {!Object|boolean} targetArg + * @param {...Object} source + * @return {Object} + */ + function extend(targetArg, sourceArg) { + var isTargetBoolean = targetArg === !!targetArg; + var i = isTargetBoolean ? 2 : 1; + var target = isTargetBoolean ? sourceArg : targetArg; + var isDeep = isTargetBoolean ? targetArg : false; + + function isObject(value) { + return value !== null && typeof value === 'object' && + Object.getPrototypeOf(value) === Object.prototype; + } + + for (; i < arguments.length; i++) { + var source = arguments[i]; + + // Copy all properties for jQuery compatibility + /* eslint guard-for-in: off */ + for (var key in source) { + var targetValue = target[key]; + var value = source[key]; + + // Skip undefined values to match jQuery + if (isUndefined(value)) { + continue; + } + + // Skip special keys to prevent prototype pollution + if (key === '__proto__' || key === 'constructor') { + continue; + } + + var isValueObject = isObject(value); + var isValueArray = Array.isArray(value); + + if (isDeep && (isValueObject || isValueArray)) { + // Can only merge if target type matches otherwise create + // new target to merge into + var isSameType = isObject(targetValue) === isValueObject && + Array.isArray(targetValue) === isValueArray; + + target[key] = extend( + true, + isSameType ? targetValue : (isValueArray ? [] : {}), + value + ); + } else { + target[key] = value; + } + } + } + + return target; + } + + /** + * Removes an item from the passed array + * + * @param {!Array} arr + * @param {*} item + */ + function arrayRemove(arr, item) { + var i = arr.indexOf(item); + + if (i > -1) { + arr.splice(i, 1); + } + } + + /** + * Iterates over an array or object + * + * @param {!Object|Array} obj + * @param {function(*, *)} fn + */ + function each(obj, fn) { + if (Array.isArray(obj) || 'length' in obj && isNumber(obj.length)) { + for (var i = 0; i < obj.length; i++) { + fn(i, obj[i]); + } + } else { + Object.keys(obj).forEach(function (key) { + fn(key, obj[key]); + }); + } + } + + /** + * Cache of camelCase CSS property names + * @type {Object} + */ + var cssPropertyNameCache = {}; + + /** + * Node type constant for element nodes + * + * @type {number} + */ + var ELEMENT_NODE = 1; + + /** + * Node type constant for text nodes + * + * @type {number} + */ + var TEXT_NODE = 3; + + /** + * Node type constant for comment nodes + * + * @type {number} + */ + var COMMENT_NODE = 8; + + function toFloat(value) { + value = parseFloat(value); + + return isFinite(value) ? value : 0; + } + + /** + * Creates an element with the specified attributes + * + * Will create it in the current document unless context + * is specified. + * + * @param {!string} tag + * @param {!Object} [attributes] + * @param {!Document} [context] + * @returns {!HTMLElement} + */ + function createElement(tag, attributes, context) { + var node = (context || document).createElement(tag); + + each(attributes || {}, function (key, value) { + if (key === 'style') { + node.style.cssText = value; + } else if (key in node) { + node[key] = value; + } else { + node.setAttribute(key, value); + } + }); + + return node; + } + + /** + * Gets the first parent node that matches the selector + * + * @param {!HTMLElement} node + * @param {!string} [selector] + * @returns {HTMLElement|undefined} + */ + function parent(node, selector) { + var parent = node || {}; + + while ((parent = parent.parentNode) && !/(9|11)/.test(parent.nodeType)) { + if (!selector || is(parent, selector)) { + return parent; + } + } + } + + /** + * Checks the passed node and all parents and + * returns the first matching node if any. + * + * @param {!HTMLElement} node + * @param {!string} selector + * @returns {HTMLElement|undefined} + */ + function closest(node, selector) { + return is(node, selector) ? node : parent(node, selector); + } + + /** + * Removes the node from the DOM + * + * @param {!HTMLElement} node + */ + function remove(node) { + if (node.parentNode) { + node.parentNode.removeChild(node); + } + } + + /** + * Appends child to parent node + * + * @param {!HTMLElement} node + * @param {!HTMLElement} child + */ + function appendChild(node, child) { + node.appendChild(child); + } + + /** + * Finds any child nodes that match the selector + * + * @param {!HTMLElement} node + * @param {!string} selector + * @returns {NodeList} + */ + function find(node, selector) { + return node.querySelectorAll(selector); + } + + /** + * For on() and off() if to add/remove the event + * to the capture phase + * + * @type {boolean} + */ + var EVENT_CAPTURE = true; + + /** + * Adds an event listener for the specified events. + * + * Events should be a space separated list of events. + * + * If selector is specified the handler will only be + * called when the event target matches the selector. + * + * @param {!Node} node + * @param {string} events + * @param {string} [selector] + * @param {function(Object)} fn + * @param {boolean} [capture=false] + * @see off() + */ + // eslint-disable-next-line max-params + function on(node, events, selector, fn, capture) { + events.split(' ').forEach(function (event) { + var handler; + + if (isString(selector)) { + handler = fn['_sce-event-' + event + selector] || function (e) { + var target = e.target; + while (target && target !== node) { + if (is(target, selector)) { + fn.call(target, e); + return; + } + + target = target.parentNode; + } + }; + + fn['_sce-event-' + event + selector] = handler; + } else { + handler = selector; + capture = fn; + } + + node.addEventListener(event, handler, capture || false); + }); + } + + /** + * Removes an event listener for the specified events. + * + * @param {!Node} node + * @param {string} events + * @param {string} [selector] + * @param {function(Object)} fn + * @param {boolean} [capture=false] + * @see on() + */ + // eslint-disable-next-line max-params + function off(node, events, selector, fn, capture) { + events.split(' ').forEach(function (event) { + var handler; + + if (isString(selector)) { + handler = fn['_sce-event-' + event + selector]; + } else { + handler = selector; + capture = fn; + } + + node.removeEventListener(event, handler, capture || false); + }); + } + + /** + * If only attr param is specified it will get + * the value of the attr param. + * + * If value is specified but null the attribute + * will be removed otherwise the attr value will + * be set to the passed value. + * + * @param {!HTMLElement} node + * @param {!string} attr + * @param {?string} [value] + */ + function attr(node, attr, value) { + if (arguments.length < 3) { + return node.getAttribute(attr); + } + + // eslint-disable-next-line eqeqeq, no-eq-null + if (value == null) { + removeAttr(node, attr); + } else { + node.setAttribute(attr, value); + } + } + + /** + * Removes the specified attribute + * + * @param {!HTMLElement} node + * @param {!string} attr + */ + function removeAttr(node, attr) { + node.removeAttribute(attr); + } + + /** + * Sets the passed elements display to none + * + * @param {!HTMLElement} node + */ + function hide(node) { + css(node, 'display', 'none'); + } + + /** + * Sets the passed elements display to default + * + * @param {!HTMLElement} node + */ + function show(node) { + css(node, 'display', ''); + } + + /** + * Toggles an elements visibility + * + * @param {!HTMLElement} node + */ + function toggle(node) { + if (isVisible(node)) { + hide(node); + } else { + show(node); + } + } + + /** + * Gets a computed CSS values or sets an inline CSS value + * + * Rules should be in camelCase format and not + * hyphenated like CSS properties. + * + * @param {!HTMLElement} node + * @param {!Object|string} rule + * @param {string|number} [value] + * @return {string|number|undefined} + */ + function css(node, rule, value) { + if (arguments.length < 3) { + if (isString(rule)) { + return node.nodeType === 1 ? getComputedStyle(node)[rule] : null; + } + + each(rule, function (key, value) { + css(node, key, value); + }); + } else { + // isNaN returns false for null, false and empty strings + // so need to check it's truthy or 0 + var isNumeric = (value || value === 0) && !isNaN(value); + node.style[rule] = isNumeric ? value + 'px' : value; + } + } + + + /** + * Gets or sets the data attributes on a node + * + * Unlike the jQuery version this only stores data + * in the DOM attributes which means only strings + * can be stored. + * + * @param {Node} node + * @param {string} [key] + * @param {string} [value] + * @return {Object|undefined} + */ + function data(node, key, value) { + var argsLength = arguments.length; + var data = {}; + + if (node.nodeType === ELEMENT_NODE) { + if (argsLength === 1) { + each(node.attributes, function (_, attr) { + if (/^data\-/i.test(attr.name)) { + data[attr.name.substr(5)] = attr.value; + } + }); + + return data; + } + + if (argsLength === 2) { + return attr(node, 'data-' + key); + } + + attr(node, 'data-' + key, String(value)); + } + } + + /** + * Checks if node matches the given selector. + * + * @param {?HTMLElement} node + * @param {string} selector + * @returns {boolean} + */ + function is(node, selector) { + var result = false; + + if (node && node.nodeType === ELEMENT_NODE) { + result = (node.matches || node.msMatchesSelector || + node.webkitMatchesSelector).call(node, selector); + } + + return result; + } + + + /** + * Returns true if node contains child otherwise false. + * + * This differs from the DOM contains() method in that + * if node and child are equal this will return false. + * + * @param {!Node} node + * @param {HTMLElement} child + * @returns {boolean} + */ + function contains(node, child) { + return node !== child && node.contains && node.contains(child); + } + + /** + * @param {Node} node + * @param {string} [selector] + * @returns {?HTMLElement} + */ + function previousElementSibling(node, selector) { + var prev = node.previousElementSibling; + + if (selector && prev) { + return is(prev, selector) ? prev : null; + } + + return prev; + } + + /** + * @param {!Node} node + * @param {!Node} refNode + * @returns {Node} + */ + function insertBefore(node, refNode) { + return refNode.parentNode.insertBefore(node, refNode); + } + + /** + * @param {?HTMLElement} node + * @returns {!Array.} + */ + function classes(node) { + return node.className.trim().split(/\s+/); + } + + /** + * @param {?HTMLElement} node + * @param {string} className + * @returns {boolean} + */ + function hasClass(node, className) { + return is(node, '.' + className); + } + + /** + * @param {!HTMLElement} node + * @param {string} className + */ + function addClass(node, className) { + var classList = classes(node); + + if (classList.indexOf(className) < 0) { + classList.push(className); + } + + node.className = classList.join(' '); + } + + /** + * @param {!HTMLElement} node + * @param {string} className + */ + function removeClass(node, className) { + var classList = classes(node); + + arrayRemove(classList, className); + + node.className = classList.join(' '); + } + + /** + * Toggles a class on node. + * + * If state is specified and is truthy it will add + * the class. + * + * If state is specified and is falsey it will remove + * the class. + * + * @param {HTMLElement} node + * @param {string} className + * @param {boolean} [state] + */ + function toggleClass(node, className, state) { + state = isUndefined(state) ? !hasClass(node, className) : state; + + if (state) { + addClass(node, className); + } else { + removeClass(node, className); + } + } + + /** + * Gets or sets the width of the passed node. + * + * @param {HTMLElement} node + * @param {number|string} [value] + * @returns {number|undefined} + */ + function width(node, value) { + if (isUndefined(value)) { + var cs = getComputedStyle(node); + var padding = toFloat(cs.paddingLeft) + toFloat(cs.paddingRight); + var border = toFloat(cs.borderLeftWidth) + toFloat(cs.borderRightWidth); + + return node.offsetWidth - padding - border; + } + + css(node, 'width', value); + } + + /** + * Gets or sets the height of the passed node. + * + * @param {HTMLElement} node + * @param {number|string} [value] + * @returns {number|undefined} + */ + function height(node, value) { + if (isUndefined(value)) { + var cs = getComputedStyle(node); + var padding = toFloat(cs.paddingTop) + toFloat(cs.paddingBottom); + var border = toFloat(cs.borderTopWidth) + toFloat(cs.borderBottomWidth); + + return node.offsetHeight - padding - border; + } + + css(node, 'height', value); + } + + /** + * Triggers a custom event with the specified name and + * sets the detail property to the data object passed. + * + * @param {HTMLElement} node + * @param {string} eventName + * @param {Object} [data] + */ + function trigger(node, eventName, data) { + var event; + + if (isFunction(window.CustomEvent)) { + event = new CustomEvent(eventName, { + bubbles: true, + cancelable: true, + detail: data + }); + } else { + event = node.ownerDocument.createEvent('CustomEvent'); + event.initCustomEvent(eventName, true, true, data); + } + + node.dispatchEvent(event); + } + + /** + * Returns if a node is visible. + * + * @param {HTMLElement} + * @returns {boolean} + */ + function isVisible(node) { + return !!node.getClientRects().length; + } + + /** + * Convert CSS property names into camel case + * + * @param {string} string + * @returns {string} + */ + function camelCase(string) { + return string + .replace(/^-ms-/, 'ms-') + .replace(/-(\w)/g, function (match, char) { + return char.toUpperCase(); + }); + } + + + /** + * Loop all child nodes of the passed node + * + * The function should accept 1 parameter being the node. + * If the function returns false the loop will be exited. + * + * @param {HTMLElement} node + * @param {function} func Callback which is called with every + * child node as the first argument. + * @param {boolean} innermostFirst If the innermost node should be passed + * to the function before it's parents. + * @param {boolean} siblingsOnly If to only traverse the nodes siblings + * @param {boolean} [reverse=false] If to traverse the nodes in reverse + */ + // eslint-disable-next-line max-params + function traverse(node, func, innermostFirst, siblingsOnly, reverse) { + node = reverse ? node.lastChild : node.firstChild; + + while (node) { + var next = reverse ? node.previousSibling : node.nextSibling; + + if ( + (!innermostFirst && func(node) === false) || + (!siblingsOnly && traverse( + node, func, innermostFirst, siblingsOnly, reverse + ) === false) || + (innermostFirst && func(node) === false) + ) { + return false; + } + + node = next; + } + } + + /** + * Like traverse but loops in reverse + * @see traverse + */ + function rTraverse(node, func, innermostFirst, siblingsOnly) { + traverse(node, func, innermostFirst, siblingsOnly, true); + } + + /** + * Parses HTML into a document fragment + * + * @param {string} html + * @param {Document} [context] + * @since 1.4.4 + * @return {DocumentFragment} + */ + function parseHTML(html, context) { + context = context || document; + + var ret = context.createDocumentFragment(); + var tmp = createElement('div', {}, context); + + tmp.innerHTML = html; + + while (tmp.firstChild) { + appendChild(ret, tmp.firstChild); + } + + return ret; + } + + /** + * Checks if an element has any styling. + * + * It has styling if it is not a plain
    or

    or + * if it has a class, style attribute or data. + * + * @param {HTMLElement} elm + * @return {boolean} + * @since 1.4.4 + */ + function hasStyling(node) { + return node && (!is(node, 'p,div') || node.className || + attr(node, 'style') || !isEmptyObject(data(node))); + } + + /** + * Converts an element from one type to another. + * + * For example it can convert the element to + * + * @param {HTMLElement} element + * @param {string} toTagName + * @return {HTMLElement} + * @since 1.4.4 + */ + function convertElement(element, toTagName) { + var newElement = createElement(toTagName, {}, element.ownerDocument); + + each(element.attributes, function (_, attribute) { + // Some browsers parse invalid attributes names like + // 'size"2' which throw an exception when set, just + // ignore these. + try { + attr(newElement, attribute.name, attribute.value); + } catch (ex) {} + }); + + while (element.firstChild) { + appendChild(newElement, element.firstChild); + } + + element.parentNode.replaceChild(newElement, element); + + return newElement; + } + + /** + * List of block level elements separated by bars (|) + * + * @type {string} + */ + var blockLevelList = '|body|hr|p|div|h1|h2|h3|h4|h5|h6|address|pre|' + + 'form|table|tbody|thead|tfoot|th|tr|td|li|ol|ul|blockquote|center|' + + 'details|section|article|aside|nav|main|header|hgroup|footer|fieldset|' + + 'dl|dt|dd|figure|figcaption|'; + + /** + * List of elements that do not allow children separated by bars (|) + * + * @param {Node} node + * @return {boolean} + * @since 1.4.5 + */ + function canHaveChildren(node) { + // 1 = Element + // 9 = Document + // 11 = Document Fragment + if (!/11?|9/.test(node.nodeType)) { + return false; + } + + // List of empty HTML tags separated by bar (|) character. + // Source: http://www.w3.org/TR/html4/index/elements.html + // Source: http://www.w3.org/TR/html5/syntax.html#void-elements + return ('|iframe|area|base|basefont|br|col|frame|hr|img|input|wbr' + + '|isindex|link|meta|param|command|embed|keygen|source|track|' + + 'object|').indexOf('|' + node.nodeName.toLowerCase() + '|') < 0; + } + + /** + * Checks if an element is inline + * + * @param {HTMLElement} elm + * @param {boolean} [includeCodeAsBlock=false] + * @return {boolean} + */ + function isInline(elm, includeCodeAsBlock) { + var tagName, + nodeType = (elm || {}).nodeType || TEXT_NODE; + + if (nodeType !== ELEMENT_NODE) { + return nodeType === TEXT_NODE; + } + + tagName = elm.tagName.toLowerCase(); + + if (tagName === 'code') { + return !includeCodeAsBlock; + } + + return blockLevelList.indexOf('|' + tagName + '|') < 0; + } + + /** + * Copy the CSS from 1 node to another. + * + * Only copies CSS defined on the element e.g. style attr. + * + * @param {HTMLElement} from + * @param {HTMLElement} to + * @deprecated since v3.1.0 + */ + function copyCSS(from, to) { + if (to.style && from.style) { + to.style.cssText = from.style.cssText + to.style.cssText; + } + } + + /** + * Checks if a DOM node is empty + * + * @param {Node} node + * @returns {boolean} + */ + function isEmpty(node) { + if (node.lastChild && isEmpty(node.lastChild)) { + remove(node.lastChild); + } + + return node.nodeType === 3 ? !node.nodeValue : + (canHaveChildren(node) && !node.childNodes.length); + } + + /** + * Fixes block level elements inside in inline elements. + * + * Also fixes invalid list nesting by placing nested lists + * inside the previous li tag or wrapping them in an li tag. + * + * @param {HTMLElement} node + */ + function fixNesting(node) { + traverse(node, function (node) { + var list = 'ul,ol', + isBlock = !isInline(node, true) && node.nodeType !== COMMENT_NODE, + parent = node.parentNode; + + // Any blocklevel element inside an inline element needs fixing. + // Also

    tags that contain blocks should be fixed + if (isBlock && (isInline(parent, true) || parent.tagName === 'P')) { + // Find the last inline parent node + var lastInlineParent = node; + while (isInline(lastInlineParent.parentNode, true) || + lastInlineParent.parentNode.tagName === 'P') { + lastInlineParent = lastInlineParent.parentNode; + } + + var before = extractContents(lastInlineParent, node); + var middle = node; + + // Clone inline styling and apply it to the blocks children + while (parent && isInline(parent, true)) { + if (parent.nodeType === ELEMENT_NODE) { + var clone = parent.cloneNode(); + while (middle.firstChild) { + appendChild(clone, middle.firstChild); + } + + appendChild(middle, clone); + } + parent = parent.parentNode; + } + + insertBefore(middle, lastInlineParent); + if (!isEmpty(before)) { + insertBefore(before, middle); + } + if (isEmpty(lastInlineParent)) { + remove(lastInlineParent); + } + } + + // Fix invalid nested lists which should be wrapped in an li tag + if (isBlock && is(node, list) && is(node.parentNode, list)) { + var li = previousElementSibling(node, 'li'); + + if (!li) { + li = createElement('li'); + insertBefore(li, node); + } + + appendChild(li, node); + } + }); + } + + /** + * Finds the common parent of two nodes + * + * @param {!HTMLElement} node1 + * @param {!HTMLElement} node2 + * @return {?HTMLElement} + */ + function findCommonAncestor(node1, node2) { + while ((node1 = node1.parentNode)) { + if (contains(node1, node2)) { + return node1; + } + } + } + + /** + * @param {?Node} + * @param {boolean} [previous=false] + * @returns {?Node} + */ + function getSibling(node, previous) { + if (!node) { + return null; + } + + return (previous ? node.previousSibling : node.nextSibling) || + getSibling(node.parentNode, previous); + } + + /** + * Removes unused whitespace from the root and all it's children. + * + * @param {!HTMLElement} root + * @since 1.4.3 + */ + function removeWhiteSpace(root) { + var nodeValue, nodeType, next, previous, previousSibling, + nextNode, trimStart, + cssWhiteSpace = css(root, 'whiteSpace'), + // Preserve newlines if is pre-line + preserveNewLines = /line$/i.test(cssWhiteSpace), + node = root.firstChild; + + // Skip pre & pre-wrap with any vendor prefix + if (/pre(\-wrap)?$/i.test(cssWhiteSpace)) { + return; + } + + while (node) { + nextNode = node.nextSibling; + nodeValue = node.nodeValue; + nodeType = node.nodeType; + + if (nodeType === ELEMENT_NODE && node.firstChild) { + removeWhiteSpace(node); + } + + if (nodeType === TEXT_NODE) { + next = getSibling(node); + previous = getSibling(node, true); + trimStart = false; + + while (hasClass(previous, 'sceditor-ignore')) { + previous = getSibling(previous, true); + } + + // If previous sibling isn't inline or is a textnode that + // ends in whitespace, time the start whitespace + if (isInline(node) && previous) { + previousSibling = previous; + + while (previousSibling.lastChild) { + previousSibling = previousSibling.lastChild; + + // eslint-disable-next-line max-depth + while (hasClass(previousSibling, 'sceditor-ignore')) { + previousSibling = getSibling(previousSibling, true); + } + } + + trimStart = previousSibling.nodeType === TEXT_NODE ? + /[\t\n\r ]$/.test(previousSibling.nodeValue) : + !isInline(previousSibling); + } + + // Clear zero width spaces + nodeValue = nodeValue.replace(/\u200B/g, ''); + + // Strip leading whitespace + if (!previous || !isInline(previous) || trimStart) { + nodeValue = nodeValue.replace( + preserveNewLines ? /^[\t ]+/ : /^[\t\n\r ]+/, + '' + ); + } + + // Strip trailing whitespace + if (!next || !isInline(next)) { + nodeValue = nodeValue.replace( + preserveNewLines ? /[\t ]+$/ : /[\t\n\r ]+$/, + '' + ); + } + + // Remove empty text nodes + if (!nodeValue.length) { + remove(node); + } else { + node.nodeValue = nodeValue.replace( + preserveNewLines ? /[\t ]+/g : /[\t\n\r ]+/g, + ' ' + ); + } + } + + node = nextNode; + } + } + + /** + * Extracts all the nodes between the start and end nodes + * + * @param {HTMLElement} startNode The node to start extracting at + * @param {HTMLElement} endNode The node to stop extracting at + * @return {DocumentFragment} + */ + function extractContents(startNode, endNode) { + var range = startNode.ownerDocument.createRange(); + + range.setStartBefore(startNode); + range.setEndAfter(endNode); + + return range.extractContents(); + } + + /** + * Gets the offset position of an element + * + * @param {HTMLElement} node + * @return {Object} An object with left and top properties + */ + function getOffset(node) { + var left = 0, + top = 0; + + while (node) { + left += node.offsetLeft; + top += node.offsetTop; + node = node.offsetParent; + } + + return { + left: left, + top: top + }; + } + + /** + * Gets the value of a CSS property from the elements style attribute + * + * @param {HTMLElement} elm + * @param {string} property + * @return {string} + */ + function getStyle(elm, property) { + var styleValue, + elmStyle = elm.style; + + if (!cssPropertyNameCache[property]) { + cssPropertyNameCache[property] = camelCase(property); + } + + property = cssPropertyNameCache[property]; + styleValue = elmStyle[property]; + + // Add an exception for text-align + if ('textAlign' === property) { + styleValue = styleValue || css(elm, property); + + if (css(elm.parentNode, property) === styleValue || + css(elm, 'display') !== 'block' || is(elm, 'hr,th')) { + return ''; + } + } + + return styleValue; + } + + /** + * Tests if an element has a style. + * + * If values are specified it will check that the styles value + * matches one of the values + * + * @param {HTMLElement} elm + * @param {string} property + * @param {string|array} [values] + * @return {boolean} + */ + function hasStyle(elm, property, values) { + var styleValue = getStyle(elm, property); + + if (!styleValue) { + return false; + } + + return !values || styleValue === values || + (Array.isArray(values) && values.indexOf(styleValue) > -1); + } + + /** + * Returns true if both nodes have the same number of inline styles and all the + * inline styles have matching values + * + * @param {HTMLElement} nodeA + * @param {HTMLElement} nodeB + * @returns {boolean} + */ + function stylesMatch(nodeA, nodeB) { + var i = nodeA.style.length; + if (i !== nodeB.style.length) { + return false; + } + + while (i--) { + var prop = nodeA.style[i]; + if (nodeA.style[prop] !== nodeB.style[prop]) { + return false; + } + } + + return true; + } + + /** + * Returns true if both nodes have the same number of attributes and all the + * attribute values match + * + * @param {HTMLElement} nodeA + * @param {HTMLElement} nodeB + * @returns {boolean} + */ + function attributesMatch(nodeA, nodeB) { + var i = nodeA.attributes.length; + if (i !== nodeB.attributes.length) { + return false; + } + + while (i--) { + var prop = nodeA.attributes[i]; + var notMatches = prop.name === 'style' ? + !stylesMatch(nodeA, nodeB) : + prop.value !== attr(nodeB, prop.name); + + if (notMatches) { + return false; + } + } + + return true; + } + + /** + * Removes an element placing its children in its place + * + * @param {HTMLElement} node + */ + function removeKeepChildren(node) { + while (node.firstChild) { + insertBefore(node.firstChild, node); + } + + remove(node); + } + + /** + * Merges inline styles and tags with parents where possible + * + * @param {Node} node + * @since 3.1.0 + */ + function merge(node) { + if (node.nodeType !== ELEMENT_NODE) { + return; + } + + var parent = node.parentNode; + var tagName = node.tagName; + var mergeTags = /B|STRONG|EM|SPAN|FONT/; + + // Merge children (in reverse as children can be removed) + var i = node.childNodes.length; + while (i--) { + merge(node.childNodes[i]); + } + + // Should only merge inline tags and should not merge
    tags + if (!isInline(node) || tagName === 'BR') { + return; + } + + // Remove any inline styles that match the parent style + i = node.style.length; + while (i--) { + var prop = node.style[i]; + if (css(parent, prop) === css(node, prop)) { + node.style.removeProperty(prop); + } + } + + // Can only remove / merge tags if no inline styling left. + // If there is any inline style left then it means it at least partially + // doesn't match the parent style so must stay + if (!node.style.length) { + removeAttr(node, 'style'); + + // Remove font attributes if match parent + if (tagName === 'FONT') { + if (css(node, 'fontFamily').toLowerCase() === + css(parent, 'fontFamily').toLowerCase()) { + removeAttr(node, 'face'); + } + + if (css(node, 'color') === css(parent, 'color')) { + removeAttr(node, 'color'); + } + + if (css(node, 'fontSize') === css(parent, 'fontSize')) { + removeAttr(node, 'size'); + } + } + + // Spans and font tags with no attributes can be safely removed + if (!node.attributes.length && /SPAN|FONT/.test(tagName)) { + removeKeepChildren(node); + } else if (mergeTags.test(tagName)) { + var isBold = /B|STRONG/.test(tagName); + var isItalic = tagName === 'EM'; + + while (parent && isInline(parent) && + (!isBold || /bold|700/i.test(css(parent, 'fontWeight'))) && + (!isItalic || css(parent, 'fontStyle') === 'italic')) { + + // Remove if parent match + if ((parent.tagName === tagName || + (isBold && /B|STRONG/.test(parent.tagName))) && + attributesMatch(parent, node)) { + removeKeepChildren(node); + break; + } + + parent = parent.parentNode; + } + } + } + + // Merge siblings if attributes, including inline styles, match + var next = node.nextSibling; + if (next && next.tagName === tagName && attributesMatch(next, node)) { + appendChild(node, next); + removeKeepChildren(next); + } + } + + /** + * Default options for SCEditor + * @type {Object} + */ + var defaultOptions = { + /** @lends jQuery.sceditor.defaultOptions */ + /** + * Toolbar buttons order and groups. Should be comma separated and + * have a bar | to separate groups + * + * @type {string} + */ + toolbar: 'bold,italic,underline,strike,subscript,superscript|' + + 'left,center,right,justify|font,size,color,removeformat|' + + 'cut,copy,pastetext|bulletlist,orderedlist,indent,outdent|' + + 'table|code,quote|horizontalrule,image,email,link,unlink|' + + 'emoticon,youtube,date,time|ltr,rtl|print,maximize,source', + + /** + * Comma separated list of commands to excludes from the toolbar + * + * @type {string} + */ + toolbarExclude: null, + + /** + * Stylesheet to include in the WYSIWYG editor. This is what will style + * the WYSIWYG elements + * + * @type {string} + */ + style: 'jquery.sceditor.default.css', + + /** + * Comma separated list of fonts for the font selector + * + * @type {string} + */ + fonts: 'Arial,Arial Black,Comic Sans MS,Courier New,Georgia,Impact,' + + 'Sans-serif,Serif,Times New Roman,Trebuchet MS,Verdana', + + /** + * Colors should be comma separated and have a bar | to signal a new + * column. + * + * If null the colors will be auto generated. + * + * @type {string} + */ + colors: '#000000,#44B8FF,#1E92F7,#0074D9,#005DC2,#00369B,#b3d5f4|' + + '#444444,#C3FFFF,#9DF9FF,#7FDBFF,#68C4E8,#419DC1,#d9f4ff|' + + '#666666,#72FF84,#4CEA5E,#2ECC40,#17B529,#008E02,#c0f0c6|' + + '#888888,#FFFF44,#FFFA1E,#FFDC00,#E8C500,#C19E00,#fff5b3|' + + '#aaaaaa,#FFC95F,#FFA339,#FF851B,#E86E04,#C14700,#ffdbbb|' + + '#cccccc,#FF857A,#FF5F54,#FF4136,#E82A1F,#C10300,#ffc6c3|' + + '#eeeeee,#FF56FF,#FF30DC,#F012BE,#D900A7,#B20080,#fbb8ec|' + + '#ffffff,#F551FF,#CF2BE7,#B10DC9,#9A00B2,#9A00B2,#e8b6ef', + + /** + * The locale to use. + * @type {string} + */ + locale: attr(document.documentElement, 'lang') || 'en', + + /** + * The Charset to use + * @type {string} + */ + charset: 'utf-8', + + /** + * Compatibility mode for emoticons. + * + * Helps if you have emoticons such as :/ which would put an emoticon + * inside http:// + * + * This mode requires emoticons to be surrounded by whitespace or end of + * line chars. This mode has limited As You Type emoticon conversion + * support. It will not replace AYT for end of line chars, only + * emoticons surrounded by whitespace. They will still be replaced + * correctly when loaded just not AYT. + * + * @type {boolean} + */ + emoticonsCompat: false, + + /** + * If to enable emoticons. Can be changes at runtime using the + * emoticons() method. + * + * @type {boolean} + * @since 1.4.2 + */ + emoticonsEnabled: true, + + /** + * Emoticon root URL + * + * @type {string} + */ + emoticonsRoot: '', + emoticons: { + dropdown: { + ':)': 'emoticons/smile.png', + ':angel:': 'emoticons/angel.png', + ':angry:': 'emoticons/angry.png', + '8-)': 'emoticons/cool.png', + ':\'(': 'emoticons/cwy.png', + ':ermm:': 'emoticons/ermm.png', + ':D': 'emoticons/grin.png', + '<3': 'emoticons/heart.png', + ':(': 'emoticons/sad.png', + ':O': 'emoticons/shocked.png', + ':P': 'emoticons/tongue.png', + ';)': 'emoticons/wink.png' + }, + more: { + ':alien:': 'emoticons/alien.png', + ':blink:': 'emoticons/blink.png', + ':blush:': 'emoticons/blush.png', + ':cheerful:': 'emoticons/cheerful.png', + ':devil:': 'emoticons/devil.png', + ':dizzy:': 'emoticons/dizzy.png', + ':getlost:': 'emoticons/getlost.png', + ':happy:': 'emoticons/happy.png', + ':kissing:': 'emoticons/kissing.png', + ':ninja:': 'emoticons/ninja.png', + ':pinch:': 'emoticons/pinch.png', + ':pouty:': 'emoticons/pouty.png', + ':sick:': 'emoticons/sick.png', + ':sideways:': 'emoticons/sideways.png', + ':silly:': 'emoticons/silly.png', + ':sleeping:': 'emoticons/sleeping.png', + ':unsure:': 'emoticons/unsure.png', + ':woot:': 'emoticons/w00t.png', + ':wassat:': 'emoticons/wassat.png' + }, + hidden: { + ':whistling:': 'emoticons/whistling.png', + ':love:': 'emoticons/wub.png' + } + }, + + /** + * Width of the editor. Set to null for automatic with + * + * @type {?number} + */ + width: null, + + /** + * Height of the editor including toolbar. Set to null for automatic + * height + * + * @type {?number} + */ + height: null, + + /** + * If to allow the editor to be resized + * + * @type {boolean} + */ + resizeEnabled: true, + + /** + * Min resize to width, set to null for half textarea width or -1 for + * unlimited + * + * @type {?number} + */ + resizeMinWidth: null, + /** + * Min resize to height, set to null for half textarea height or -1 for + * unlimited + * + * @type {?number} + */ + resizeMinHeight: null, + /** + * Max resize to height, set to null for double textarea height or -1 + * for unlimited + * + * @type {?number} + */ + resizeMaxHeight: null, + /** + * Max resize to width, set to null for double textarea width or -1 for + * unlimited + * + * @type {?number} + */ + resizeMaxWidth: null, + /** + * If resizing by height is enabled + * + * @type {boolean} + */ + resizeHeight: true, + /** + * If resizing by width is enabled + * + * @type {boolean} + */ + resizeWidth: true, + + /** + * Date format, will be overridden if locale specifies one. + * + * The words year, month and day will be replaced with the users current + * year, month and day. + * + * @type {string} + */ + dateFormat: 'year-month-day', + + /** + * Element to inset the toolbar into. + * + * @type {HTMLElement} + */ + toolbarContainer: null, + + /** + * If to enable paste filtering. This is currently experimental, please + * report any issues. + * + * @type {boolean} + */ + enablePasteFiltering: false, + + /** + * If to completely disable pasting into the editor + * + * @type {boolean} + */ + disablePasting: false, + + /** + * If the editor is read only. + * + * @type {boolean} + */ + readOnly: false, + + /** + * If to set the editor to right-to-left mode. + * + * If set to null the direction will be automatically detected. + * + * @type {boolean} + */ + rtl: false, + + /** + * If to auto focus the editor on page load + * + * @type {boolean} + */ + autofocus: false, + + /** + * If to auto focus the editor to the end of the content + * + * @type {boolean} + */ + autofocusEnd: true, + + /** + * If to auto expand the editor to fix the content + * + * @type {boolean} + */ + autoExpand: false, + + /** + * If to auto update original textbox on blur + * + * @type {boolean} + */ + autoUpdate: false, + + /** + * If to enable the browsers built in spell checker + * + * @type {boolean} + */ + spellcheck: true, + + /** + * If to run the source editor when there is no WYSIWYG support. Only + * really applies to mobile OS's. + * + * @type {boolean} + */ + runWithoutWysiwygSupport: false, + + /** + * If to load the editor in source mode and still allow switching + * between WYSIWYG and source mode + * + * @type {boolean} + */ + startInSourceMode: false, + + /** + * Optional ID to give the editor. + * + * @type {string} + */ + id: null, + + /** + * Comma separated list of plugins + * + * @type {string} + */ + plugins: '', + + /** + * z-index to set the editor container to. Needed for jQuery UI dialog. + * + * @type {?number} + */ + zIndex: null, + + /** + * If to trim the BBCode. Removes any spaces at the start and end of the + * BBCode string. + * + * @type {boolean} + */ + bbcodeTrim: false, + + /** + * If to disable removing block level elements by pressing backspace at + * the start of them + * + * @type {boolean} + */ + disableBlockRemove: false, + + /** + * Array of allowed URL (should be either strings or regex) for iframes. + * + * If it's a string then iframes where the start of the src matches the + * specified string will be allowed. + * + * If it's a regex then iframes where the src matches the regex will be + * allowed. + * + * @type {Array} + */ + allowedIframeUrls: [], + + /** + * BBCode parser options, only applies if using the editor in BBCode + * mode. + * + * See SCEditor.BBCodeParser.defaults for list of valid options + * + * @type {Object} + */ + parserOptions: { }, + + /** + * CSS that will be added to the to dropdown menu (eg. z-index) + * + * @type {Object} + */ + dropDownCss: { }, + + /** + * An array of tags that are allowed in the editor content. + * If a tag is not listed here, it will be removed when the content is + * sanitized. + * + * 1 Tag is already added by default: ['iframe']. No need to add this + * further. + * + * @type {Array} + */ + allowedTags: [], + + /** + * An array of attributes that are allowed on tags in the editor content. + * If an attribute is not listed here, it will be removed when the content + * is sanitized. + * + * 3 Attributes are already added by default: + * ['allowfullscreen', 'frameborder', 'target']. + * No need to add these further. + * + * @type {Array} + */ + allowedAttributes: [] + }; + + // Must start with a valid scheme + // ^ + // Schemes that are considered safe + // (https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):| + // Relative schemes (//:) are considered safe + // (\\/\\/)| + // Image data URI's are considered safe + // data:image\\/(png|bmp|gif|p?jpe?g); + var VALID_SCHEME_REGEX = + /^(https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):|(\/\/)|data:image\/(png|bmp|gif|p?jpe?g);/i; + + /** + * Escapes a string so it's safe to use in regex + * + * @param {string} str + * @return {string} + */ + function regex(str) { + return str.replace(/([\-.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'); + } + /** + * Escapes all HTML entities in a string + * + * If noQuotes is set to false, all single and double + * quotes will also be escaped + * + * @param {string} str + * @param {boolean} [noQuotes=true] + * @return {string} + * @since 1.4.1 + */ + function entities(str, noQuotes) { + if (!str) { + return str; + } + + var replacements = { + '&': '&', + '<': '<', + '>': '>', + ' ': '  ', + '\r\n': '
    ', + '\r': '
    ', + '\n': '
    ' + }; + + if (noQuotes !== false) { + replacements['"'] = '"'; + replacements['\''] = '''; + replacements['`'] = '`'; + } + + str = str.replace(/ {2}|\r\n|[&<>\r\n'"`]/g, function (match) { + return replacements[match] || match; + }); + + return str; + } + /** + * Escape URI scheme. + * + * Appends the current URL to a url if it has a scheme that is not: + * + * http + * https + * sftp + * ftp + * mailto + * spotify + * skype + * ssh + * teamspeak + * tel + * // + * data:image/(png|jpeg|jpg|pjpeg|bmp|gif); + * + * **IMPORTANT**: This does not escape any HTML in a url, for + * that use the escape.entities() method. + * + * @param {string} url + * @return {string} + * @since 1.4.5 + */ + function uriScheme(url) { + var path, + // If there is a : before a / then it has a scheme + hasScheme = /^[^\/]*:/i, + location = window.location; + + // Has no scheme or a valid scheme + if ((!url || !hasScheme.test(url)) || VALID_SCHEME_REGEX.test(url)) { + return url; + } + + path = location.pathname.split('/'); + path.pop(); + + return location.protocol + '//' + + location.host + + path.join('/') + '/' + + url; + } + + /** + * HTML templates used by the editor and default commands + * @type {Object} + * @private + */ + var _templates = { + html: + '' + + '' + + '' + + '' + + '' + + '' + + '

    ' + + '', + + toolbarButton: '' + + '
    {dispName}
    ', + + emoticon: '', + + fontOpt: '{font}', + + sizeOpt: '{size}', + + pastetext: + '
    ' + + '
    ' + + '
    ' + + '
    ', + + table: + '
    ' + + '
    ' + + '
    ', + + image: + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ', + + email: + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ', + + link: + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ', + + youtubeMenu: + '
    ' + + '
    ' + + '
    ' + + '
    ', + + youtube: + '' + }; + + /** + * Replaces any params in a template with the passed params. + * + * If createHtml is passed it will return a DocumentFragment + * containing the parsed template. + * + * @param {string} name + * @param {Object} [params] + * @param {boolean} [createHtml] + * @returns {string|DocumentFragment} + * @private + */ + function _tmpl (name, params, createHtml) { + var template = _templates[name]; + + Object.keys(params).forEach(function (name) { + template = template.replace( + new RegExp(regex('{' + name + '}'), 'g'), params[name] + ); + }); + + if (createHtml) { + template = parseHTML(template); + } + + return template; + } + + /** + * Fixes a bug in FF where it sometimes wraps + * new lines in their own list item. + * See issue #359 + */ + function fixFirefoxListBug(editor) { + // Only apply to Firefox as will break other browsers. + if ('mozHidden' in document) { + var node = editor.getBody(); + var next; + + while (node) { + next = node; + + if (next.firstChild) { + next = next.firstChild; + } else { + + while (next && !next.nextSibling) { + next = next.parentNode; + } + + if (next) { + next = next.nextSibling; + } + } + + if (node.nodeType === 3 && /[\n\r\t]+/.test(node.nodeValue)) { + // Only remove if newlines are collapsed + if (!/^pre/.test(css(node.parentNode, 'whiteSpace'))) { + remove(node); + } + } + + node = next; + } + } + } + + + /** + * Map of all the commands for SCEditor + * @type {Object} + * @name commands + * @memberOf jQuery.sceditor + */ + var defaultCmds = { + // START_COMMAND: Bold + bold: { + exec: 'bold', + tooltip: 'Bold', + shortcut: 'Ctrl+B' + }, + // END_COMMAND + // START_COMMAND: Italic + italic: { + exec: 'italic', + tooltip: 'Italic', + shortcut: 'Ctrl+I' + }, + // END_COMMAND + // START_COMMAND: Underline + underline: { + exec: 'underline', + tooltip: 'Underline', + shortcut: 'Ctrl+U' + }, + // END_COMMAND + // START_COMMAND: Strikethrough + strike: { + exec: 'strikethrough', + tooltip: 'Strikethrough' + }, + // END_COMMAND + // START_COMMAND: Subscript + subscript: { + exec: 'subscript', + tooltip: 'Subscript' + }, + // END_COMMAND + // START_COMMAND: Superscript + superscript: { + exec: 'superscript', + tooltip: 'Superscript' + }, + // END_COMMAND + + // START_COMMAND: Left + left: { + state: function (node) { + if (node && node.nodeType === 3) { + node = node.parentNode; + } + + if (node) { + var isLtr = css(node, 'direction') === 'ltr'; + var align = css(node, 'textAlign'); + + // Can be -moz-left + return /left/.test(align) || + align === (isLtr ? 'start' : 'end'); + } + }, + exec: 'justifyleft', + tooltip: 'Align left' + }, + // END_COMMAND + // START_COMMAND: Centre + center: { + exec: 'justifycenter', + tooltip: 'Center' + }, + // END_COMMAND + // START_COMMAND: Right + right: { + state: function (node) { + if (node && node.nodeType === 3) { + node = node.parentNode; + } + + if (node) { + var isLtr = css(node, 'direction') === 'ltr'; + var align = css(node, 'textAlign'); + + // Can be -moz-right + return /right/.test(align) || + align === (isLtr ? 'end' : 'start'); + } + }, + exec: 'justifyright', + tooltip: 'Align right' + }, + // END_COMMAND + // START_COMMAND: Justify + justify: { + exec: 'justifyfull', + tooltip: 'Justify' + }, + // END_COMMAND + + // START_COMMAND: Font + font: { + _dropDown: function (editor, caller, callback) { + var content = createElement('div'); + + on(content, 'click', 'a', function (e) { + callback(data(this, 'font')); + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.opts.fonts.split(',').forEach(function (font) { + appendChild(content, _tmpl('fontOpt', { + font: font + }, true)); + }); + + editor.createDropDown(caller, 'font-picker', content); + }, + exec: function (caller) { + var editor = this; + + defaultCmds.font._dropDown(editor, caller, function (fontName) { + editor.execCommand('fontname', fontName); + }); + }, + tooltip: 'Font Name' + }, + // END_COMMAND + // START_COMMAND: Size + size: { + _dropDown: function (editor, caller, callback) { + var content = createElement('div'); + + on(content, 'click', 'a', function (e) { + callback(data(this, 'size')); + editor.closeDropDown(true); + e.preventDefault(); + }); + + for (var i = 1; i <= 7; i++) { + appendChild(content, _tmpl('sizeOpt', { + size: i + }, true)); + } + + editor.createDropDown(caller, 'fontsize-picker', content); + }, + exec: function (caller) { + var editor = this; + + defaultCmds.size._dropDown(editor, caller, function (fontSize) { + editor.execCommand('fontsize', fontSize); + }); + }, + tooltip: 'Font Size' + }, + // END_COMMAND + // START_COMMAND: Colour + color: { + _dropDown: function (editor, caller, callback) { + var content = createElement('div'), + html = '', + cmd = defaultCmds.color; + + if (!cmd._htmlCache) { + editor.opts.colors.split('|').forEach(function (column) { + html += '
    '; + + column.split(',').forEach(function (color) { + html += + ''; + }); + + html += '
    '; + }); + + cmd._htmlCache = html; + } + + appendChild(content, parseHTML(cmd._htmlCache)); + + on(content, 'click', 'a', function (e) { + callback(data(this, 'color')); + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'color-picker', content); + }, + exec: function (caller) { + var editor = this; + + defaultCmds.color._dropDown(editor, caller, function (color) { + editor.execCommand('forecolor', color); + }); + }, + tooltip: 'Font Color' + }, + // END_COMMAND + // START_COMMAND: Remove Format + removeformat: { + exec: 'removeformat', + tooltip: 'Remove Formatting' + }, + // END_COMMAND + + // START_COMMAND: Cut + cut: { + exec: 'cut', + tooltip: 'Cut', + errorMessage: 'Your browser does not allow the cut command. ' + + 'Please use the keyboard shortcut Ctrl/Cmd-X' + }, + // END_COMMAND + // START_COMMAND: Copy + copy: { + exec: 'copy', + tooltip: 'Copy', + errorMessage: 'Your browser does not allow the copy command. ' + + 'Please use the keyboard shortcut Ctrl/Cmd-C' + }, + // END_COMMAND + // START_COMMAND: Paste + paste: { + exec: 'paste', + tooltip: 'Paste', + errorMessage: 'Your browser does not allow the paste command. ' + + 'Please use the keyboard shortcut Ctrl/Cmd-V' + }, + // END_COMMAND + // START_COMMAND: Paste Text + pastetext: { + exec: function (caller) { + var val, + content = createElement('div'), + editor = this; + + appendChild(content, _tmpl('pastetext', { + label: editor._( + 'Paste your text inside the following box:' + ), + insert: editor._('Insert') + }, true)); + + on(content, 'click', '.button', function (e) { + val = find(content, '#txt')[0].value; + + if (val) { + editor.wysiwygEditorInsertText(val); + } + + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'pastetext', content); + }, + tooltip: 'Paste Text' + }, + // END_COMMAND + // START_COMMAND: Bullet List + bulletlist: { + exec: function () { + fixFirefoxListBug(this); + this.execCommand('insertunorderedlist'); + }, + tooltip: 'Bullet list' + }, + // END_COMMAND + // START_COMMAND: Ordered List + orderedlist: { + exec: function () { + fixFirefoxListBug(this); + this.execCommand('insertorderedlist'); + }, + tooltip: 'Numbered list' + }, + // END_COMMAND + // START_COMMAND: Indent + indent: { + state: function (parent, firstBlock) { + // Only works with lists, for now + var range, startParent, endParent; + + if (is(firstBlock, 'li')) { + return 0; + } + + if (is(firstBlock, 'ul,ol,menu')) { + // if the whole list is selected, then this must be + // invalidated because the browser will place a + //
    there + range = this.getRangeHelper().selectedRange(); + + startParent = range.startContainer.parentNode; + endParent = range.endContainer.parentNode; + + // TODO: could use nodeType for this? + // Maybe just check the firstBlock contains both the start + //and end containers + + // Select the tag, not the textNode + // (that's why the parentNode) + if (startParent !== + startParent.parentNode.firstElementChild || + // work around a bug in FF + (is(endParent, 'li') && endParent !== + endParent.parentNode.lastElementChild)) { + return 0; + } + } + + return -1; + }, + exec: function () { + var editor = this, + block = editor.getRangeHelper().getFirstBlockParent(); + + editor.focus(); + + // An indent system is quite complicated as there are loads + // of complications and issues around how to indent text + // As default, let's just stay with indenting the lists, + // at least, for now. + if (closest(block, 'ul,ol,menu')) { + editor.execCommand('indent'); + } + }, + tooltip: 'Add indent' + }, + // END_COMMAND + // START_COMMAND: Outdent + outdent: { + state: function (parents, firstBlock) { + return closest(firstBlock, 'ul,ol,menu') ? 0 : -1; + }, + exec: function () { + var block = this.getRangeHelper().getFirstBlockParent(); + if (closest(block, 'ul,ol,menu')) { + this.execCommand('outdent'); + } + }, + tooltip: 'Remove one indent' + }, + // END_COMMAND + + // START_COMMAND: Table + table: { + exec: function (caller) { + var editor = this, + content = createElement('div'); + + appendChild(content, _tmpl('table', { + rows: editor._('Rows:'), + cols: editor._('Cols:'), + insert: editor._('Insert') + }, true)); + + on(content, 'click', '.button', function (e) { + var rows = Number(find(content, '#rows')[0].value), + cols = Number(find(content, '#cols')[0].value), + html = ''; + + if (rows > 0 && cols > 0) { + html += Array(rows + 1).join( + '' + + Array(cols + 1).join( + '' + ) + + '' + ); + + html += '

    '; + + editor.wysiwygEditorInsertHtml(html); + editor.closeDropDown(true); + e.preventDefault(); + } + }); + + editor.createDropDown(caller, 'inserttable', content); + }, + tooltip: 'Insert a table' + }, + // END_COMMAND + + // START_COMMAND: Horizontal Rule + horizontalrule: { + exec: 'inserthorizontalrule', + tooltip: 'Insert a horizontal rule' + }, + // END_COMMAND + + // START_COMMAND: Code + code: { + exec: function () { + this.wysiwygEditorInsertHtml( + '', + '
    ' + ); + }, + tooltip: 'Code' + }, + // END_COMMAND + + // START_COMMAND: Image + image: { + _dropDown: function (editor, caller, selected, cb) { + var content = createElement('div'); + + appendChild(content, _tmpl('image', { + url: editor._('URL:'), + width: editor._('Width (optional):'), + height: editor._('Height (optional):'), + insert: editor._('Insert') + }, true)); + + + var urlInput = find(content, '#image')[0]; + + urlInput.value = selected; + + on(content, 'click', '.button', function (e) { + if (urlInput.value) { + cb( + urlInput.value, + find(content, '#width')[0].value, + find(content, '#height')[0].value + ); + } + + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'insertimage', content); + }, + exec: function (caller) { + var editor = this; + + defaultCmds.image._dropDown( + editor, + caller, + '', + function (url, width, height) { + var attrs = ''; + + if (width) { + attrs += ' width="' + parseInt(width, 10) + '"'; + } + + if (height) { + attrs += ' height="' + parseInt(height, 10) + '"'; + } + + attrs += ' src="' + entities(url) + '"'; + + editor.wysiwygEditorInsertHtml( + '' + ); + } + ); + }, + tooltip: 'Insert an image' + }, + // END_COMMAND + + // START_COMMAND: E-mail + email: { + _dropDown: function (editor, caller, cb) { + var content = createElement('div'); + + appendChild(content, _tmpl('email', { + label: editor._('E-mail:'), + desc: editor._('Description (optional):'), + insert: editor._('Insert') + }, true)); + + on(content, 'click', '.button', function (e) { + var email = find(content, '#email')[0].value; + + if (email) { + cb(email, find(content, '#des')[0].value); + } + + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'insertemail', content); + }, + exec: function (caller) { + var editor = this; + + defaultCmds.email._dropDown( + editor, + caller, + function (email, text) { + if (!editor.getRangeHelper().selectedHtml() || text) { + editor.wysiwygEditorInsertHtml( + '' + + entities((text || email)) + + '' + ); + } else { + editor.execCommand('createlink', 'mailto:' + email); + } + } + ); + }, + tooltip: 'Insert an email' + }, + // END_COMMAND + + // START_COMMAND: Link + link: { + _dropDown: function (editor, caller, cb) { + var content = createElement('div'); + + appendChild(content, _tmpl('link', { + url: editor._('URL:'), + desc: editor._('Description (optional):'), + ins: editor._('Insert') + }, true)); + + var linkInput = find(content, '#link')[0]; + + function insertUrl(e) { + if (linkInput.value) { + cb(linkInput.value, find(content, '#des')[0].value); + } + + editor.closeDropDown(true); + e.preventDefault(); + } + + on(content, 'click', '.button', insertUrl); + on(content, 'keypress', function (e) { + // 13 = enter key + if (e.which === 13 && linkInput.value) { + insertUrl(e); + } + }, EVENT_CAPTURE); + + editor.createDropDown(caller, 'insertlink', content); + }, + exec: function (caller) { + var editor = this; + + defaultCmds.link._dropDown(editor, caller, function (url, text) { + if (text || !editor.getRangeHelper().selectedHtml()) { + editor.wysiwygEditorInsertHtml( + '' + + entities(text || url) + + '' + ); + } else { + editor.execCommand('createlink', url); + } + }); + }, + tooltip: 'Insert a link' + }, + // END_COMMAND + + // START_COMMAND: Unlink + unlink: { + state: function () { + return closest(this.currentNode(), 'a') ? 0 : -1; + }, + exec: function () { + var anchor = closest(this.currentNode(), 'a'); + + if (anchor) { + while (anchor.firstChild) { + insertBefore(anchor.firstChild, anchor); + } + + remove(anchor); + } + }, + tooltip: 'Unlink' + }, + // END_COMMAND + + + // START_COMMAND: Quote + quote: { + exec: function (caller, html, author) { + var before = '
    ', + end = '
    '; + + // if there is HTML passed set end to null so any selected + // text is replaced + if (html) { + author = (author ? '' + + entities(author) + + '' : ''); + before = before + author + html + end; + end = null; + // if not add a newline to the end of the inserted quote + } else if (this.getRangeHelper().selectedHtml() === '') { + end = '
    ' + end; + } + + this.wysiwygEditorInsertHtml(before, end); + }, + tooltip: 'Insert a Quote' + }, + // END_COMMAND + + // START_COMMAND: Emoticons + emoticon: { + exec: function (caller) { + var editor = this; + + var createContent = function (includeMore) { + var moreLink, + opts = editor.opts, + emoticonsRoot = opts.emoticonsRoot || '', + emoticonsCompat = opts.emoticonsCompat, + rangeHelper = editor.getRangeHelper(), + startSpace = emoticonsCompat && + rangeHelper.getOuterText(true, 1) !== ' ' ? ' ' : '', + endSpace = emoticonsCompat && + rangeHelper.getOuterText(false, 1) !== ' ' ? ' ' : '', + content = createElement('div'), + line = createElement('div'), + perLine = 0, + emoticons = extend( + {}, + opts.emoticons.dropdown, + includeMore ? opts.emoticons.more : {} + ); + + appendChild(content, line); + + perLine = Math.sqrt(Object.keys(emoticons).length); + + on(content, 'click', 'img', function (e) { + editor.insert(startSpace + attr(this, 'alt') + endSpace, + null, false).closeDropDown(true); + + e.preventDefault(); + }); + + each(emoticons, function (code, emoticon) { + appendChild(line, createElement('img', { + src: emoticonsRoot + (emoticon.url || emoticon), + alt: code, + title: emoticon.tooltip || code + })); + + if (line.children.length >= perLine) { + line = createElement('div'); + appendChild(content, line); + } + }); + + if (!includeMore && opts.emoticons.more) { + moreLink = createElement('a', { + className: 'sceditor-more' + }); + + appendChild(moreLink, + document.createTextNode(editor._('More'))); + + on(moreLink, 'click', function (e) { + editor.createDropDown( + caller, 'more-emoticons', createContent(true) + ); + + e.preventDefault(); + }); + + appendChild(content, moreLink); + } + + return content; + }; + + editor.createDropDown(caller, 'emoticons', createContent(false)); + }, + txtExec: function (caller) { + defaultCmds.emoticon.exec.call(this, caller); + }, + tooltip: 'Insert an emoticon' + }, + // END_COMMAND + + // START_COMMAND: YouTube + youtube: { + _dropDown: function (editor, caller, callback) { + var content = createElement('div'); + + appendChild(content, _tmpl('youtubeMenu', { + label: editor._('Video URL:'), + insert: editor._('Insert') + }, true)); + + on(content, 'click', '.button', function (e) { + var val = find(content, '#link')[0].value; + var idMatch = val.match(/(?:v=|v\/|embed\/|youtu.be\/)?([a-zA-Z0-9_-]{11})/); + var timeMatch = val.match(/[&|?](?:star)?t=((\d+[hms]?){1,3})/); + var time = 0; + + if (timeMatch) { + each(timeMatch[1].split(/[hms]/), function (i, val) { + if (val !== '') { + time = (time * 60) + Number(val); + } + }); + } + + if (idMatch && /^[a-zA-Z0-9_\-]{11}$/.test(idMatch[1])) { + callback(idMatch[1], time); + } + + editor.closeDropDown(true); + e.preventDefault(); + }); + + editor.createDropDown(caller, 'insertlink', content); + }, + exec: function (btn) { + var editor = this; + + defaultCmds.youtube._dropDown(editor, btn, function (id, time) { + editor.wysiwygEditorInsertHtml(_tmpl('youtube', { + id: id, + time: time + })); + }); + }, + tooltip: 'Insert a YouTube video' + }, + // END_COMMAND + + // START_COMMAND: Date + date: { + _date: function (editor) { + var now = new Date(), + year = now.getYear(), + month = now.getMonth() + 1, + day = now.getDate(); + + if (year < 2000) { + year = 1900 + year; + } + + if (month < 10) { + month = '0' + month; + } + + if (day < 10) { + day = '0' + day; + } + + return editor.opts.dateFormat + .replace(/year/i, year) + .replace(/month/i, month) + .replace(/day/i, day); + }, + exec: function () { + this.insertText(defaultCmds.date._date(this)); + }, + txtExec: function () { + this.insertText(defaultCmds.date._date(this)); + }, + tooltip: 'Insert current date' + }, + // END_COMMAND + + // START_COMMAND: Time + time: { + _time: function () { + var now = new Date(), + hours = now.getHours(), + mins = now.getMinutes(), + secs = now.getSeconds(); + + if (hours < 10) { + hours = '0' + hours; + } + + if (mins < 10) { + mins = '0' + mins; + } + + if (secs < 10) { + secs = '0' + secs; + } + + return hours + ':' + mins + ':' + secs; + }, + exec: function () { + this.insertText(defaultCmds.time._time()); + }, + txtExec: function () { + this.insertText(defaultCmds.time._time()); + }, + tooltip: 'Insert current time' + }, + // END_COMMAND + + + // START_COMMAND: Ltr + ltr: { + state: function (parents, firstBlock) { + return firstBlock && firstBlock.style.direction === 'ltr'; + }, + exec: function () { + var editor = this, + rangeHelper = editor.getRangeHelper(), + node = rangeHelper.getFirstBlockParent(); + + editor.focus(); + + if (!node || is(node, 'body')) { + editor.execCommand('formatBlock', 'p'); + + node = rangeHelper.getFirstBlockParent(); + + if (!node || is(node, 'body')) { + return; + } + } + + var toggleValue = css(node, 'direction') === 'ltr' ? '' : 'ltr'; + css(node, 'direction', toggleValue); + }, + tooltip: 'Left-to-Right' + }, + // END_COMMAND + + // START_COMMAND: Rtl + rtl: { + state: function (parents, firstBlock) { + return firstBlock && firstBlock.style.direction === 'rtl'; + }, + exec: function () { + var editor = this, + rangeHelper = editor.getRangeHelper(), + node = rangeHelper.getFirstBlockParent(); + + editor.focus(); + + if (!node || is(node, 'body')) { + editor.execCommand('formatBlock', 'p'); + + node = rangeHelper.getFirstBlockParent(); + + if (!node || is(node, 'body')) { + return; + } + } + + var toggleValue = css(node, 'direction') === 'rtl' ? '' : 'rtl'; + css(node, 'direction', toggleValue); + }, + tooltip: 'Right-to-Left' + }, + // END_COMMAND + + + // START_COMMAND: Print + print: { + exec: 'print', + tooltip: 'Print' + }, + // END_COMMAND + + // START_COMMAND: Maximize + maximize: { + state: function () { + return this.maximize(); + }, + exec: function () { + this.maximize(!this.maximize()); + this.focus(); + }, + txtExec: function () { + this.maximize(!this.maximize()); + this.focus(); + }, + tooltip: 'Maximize', + shortcut: 'Ctrl+Shift+M' + }, + // END_COMMAND + + // START_COMMAND: Source + source: { + state: function () { + return this.sourceMode(); + }, + exec: function () { + this.toggleSourceMode(); + this.focus(); + }, + txtExec: function () { + this.toggleSourceMode(); + this.focus(); + }, + tooltip: 'View source', + shortcut: 'Ctrl+Shift+S' + }, + // END_COMMAND + + // this is here so that commands above can be removed + // without having to remove the , after the last one. + // Needed for IE. + ignore: {} + }; + + var plugins = {}; + + /** + * Plugin Manager class + * @class PluginManager + * @name PluginManager + */ + function PluginManager(thisObj) { + /** + * Alias of this + * + * @private + * @type {Object} + */ + var base = this; + + /** + * Array of all currently registered plugins + * + * @type {Array} + * @private + */ + var registeredPlugins = []; + + + /** + * Changes a signals name from "name" into "signalName". + * + * @param {string} signal + * @return {string} + * @private + */ + var formatSignalName = function (signal) { + return 'signal' + signal.charAt(0).toUpperCase() + signal.slice(1); + }; + + /** + * Calls handlers for a signal + * + * @see call() + * @see callOnlyFirst() + * @param {Array} args + * @param {boolean} returnAtFirst + * @return {*} + * @private + */ + var callHandlers = function (args, returnAtFirst) { + args = [].slice.call(args); + + var idx, ret, + signal = formatSignalName(args.shift()); + + for (idx = 0; idx < registeredPlugins.length; idx++) { + if (signal in registeredPlugins[idx]) { + ret = registeredPlugins[idx][signal].apply(thisObj, args); + + if (returnAtFirst) { + return ret; + } + } + } + }; + + /** + * Calls all handlers for the passed signal + * + * @param {string} signal + * @param {...string} args + * @function + * @name call + * @memberOf PluginManager.prototype + */ + base.call = function () { + callHandlers(arguments, false); + }; + + /** + * Calls the first handler for a signal, and returns the + * + * @param {string} signal + * @param {...string} args + * @return {*} The result of calling the handler + * @function + * @name callOnlyFirst + * @memberOf PluginManager.prototype + */ + base.callOnlyFirst = function () { + return callHandlers(arguments, true); + }; + + /** + * Checks if a signal has a handler + * + * @param {string} signal + * @return {boolean} + * @function + * @name hasHandler + * @memberOf PluginManager.prototype + */ + base.hasHandler = function (signal) { + var i = registeredPlugins.length; + signal = formatSignalName(signal); + + while (i--) { + if (signal in registeredPlugins[i]) { + return true; + } + } + + return false; + }; + + /** + * Checks if the plugin exists in plugins + * + * @param {string} plugin + * @return {boolean} + * @function + * @name exists + * @memberOf PluginManager.prototype + */ + base.exists = function (plugin) { + if (plugin in plugins) { + plugin = plugins[plugin]; + + return typeof plugin === 'function' && + typeof plugin.prototype === 'object'; + } + + return false; + }; + + /** + * Checks if the passed plugin is currently registered. + * + * @param {string} plugin + * @return {boolean} + * @function + * @name isRegistered + * @memberOf PluginManager.prototype + */ + base.isRegistered = function (plugin) { + if (base.exists(plugin)) { + var idx = registeredPlugins.length; + + while (idx--) { + if (registeredPlugins[idx] instanceof plugins[plugin]) { + return true; + } + } + } + + return false; + }; + + /** + * Registers a plugin to receive signals + * + * @param {string} plugin + * @return {boolean} + * @function + * @name register + * @memberOf PluginManager.prototype + */ + base.register = function (plugin) { + if (!base.exists(plugin) || base.isRegistered(plugin)) { + return false; + } + + plugin = new plugins[plugin](); + registeredPlugins.push(plugin); + + if ('init' in plugin) { + plugin.init.call(thisObj); + } + + return true; + }; + + /** + * Deregisters a plugin. + * + * @param {string} plugin + * @return {boolean} + * @function + * @name deregister + * @memberOf PluginManager.prototype + */ + base.deregister = function (plugin) { + var removedPlugin, + pluginIdx = registeredPlugins.length, + removed = false; + + if (!base.isRegistered(plugin)) { + return removed; + } + + while (pluginIdx--) { + if (registeredPlugins[pluginIdx] instanceof plugins[plugin]) { + removedPlugin = registeredPlugins.splice(pluginIdx, 1)[0]; + removed = true; + + if ('destroy' in removedPlugin) { + removedPlugin.destroy.call(thisObj); + } + } + } + + return removed; + }; + + /** + * Clears all plugins and removes the owner reference. + * + * Calling any functions on this object after calling + * destroy will cause a JS error. + * + * @name destroy + * @memberOf PluginManager.prototype + */ + base.destroy = function () { + var i = registeredPlugins.length; + + while (i--) { + if ('destroy' in registeredPlugins[i]) { + registeredPlugins[i].destroy.call(thisObj); + } + } + + registeredPlugins = []; + thisObj = null; + }; + } + PluginManager.plugins = plugins; + + /** + * Gets the text, start/end node and offset for + * length chars left or right of the passed node + * at the specified offset. + * + * @param {Node} node + * @param {number} offset + * @param {boolean} isLeft + * @param {number} length + * @return {Object} + * @private + */ + var outerText = function (range, isLeft, length) { + var nodeValue, remaining, start, end, node, + text = '', + next = range.startContainer, + offset = range.startOffset; + + // Handle cases where node is a paragraph and offset + // refers to the index of a text node. + // 3 = text node + if (next && next.nodeType !== 3) { + next = next.childNodes[offset]; + offset = 0; + } + + start = end = offset; + + while (length > text.length && next && next.nodeType === 3) { + nodeValue = next.nodeValue; + remaining = length - text.length; + + // If not the first node, start and end should be at their + // max values as will be updated when getting the text + if (node) { + end = nodeValue.length; + start = 0; + } + + node = next; + + if (isLeft) { + start = Math.max(end - remaining, 0); + offset = start; + + text = nodeValue.substr(start, end - start) + text; + next = node.previousSibling; + } else { + end = Math.min(remaining, nodeValue.length); + offset = start + end; + + text += nodeValue.substr(start, end); + next = node.nextSibling; + } + } + + return { + node: node || next, + offset: offset, + text: text + }; + }; + + /** + * Range helper + * + * @class RangeHelper + * @name RangeHelper + */ + function RangeHelper(win, d, sanitize) { + var _createMarker, _prepareInput, + doc = d || win.contentDocument || win.document, + startMarker = 'sceditor-start-marker', + endMarker = 'sceditor-end-marker', + base = this; + + /** + * Inserts HTML into the current range replacing any selected + * text. + * + * If endHTML is specified the selected contents will be put between + * html and endHTML. If there is nothing selected html and endHTML are + * just concatenate together. + * + * @param {string} html + * @param {string} [endHTML] + * @return False on fail + * @function + * @name insertHTML + * @memberOf RangeHelper.prototype + */ + base.insertHTML = function (html, endHTML) { + var node, div, + range = base.selectedRange(); + + if (!range) { + return false; + } + + if (endHTML) { + html += base.selectedHtml() + endHTML; + } + + div = createElement('p', {}, doc); + node = doc.createDocumentFragment(); + div.innerHTML = sanitize(html); + + while (div.firstChild) { + appendChild(node, div.firstChild); + } + + base.insertNode(node); + }; + + /** + * Prepares HTML to be inserted by adding a zero width space + * if the last child is empty and adding the range start/end + * markers to the last child. + * + * @param {Node|string} node + * @param {Node|string} [endNode] + * @param {boolean} [returnHtml] + * @return {Node|string} + * @private + */ + _prepareInput = function (node, endNode, returnHtml) { + var lastChild, + frag = doc.createDocumentFragment(); + + if (typeof node === 'string') { + if (endNode) { + node += base.selectedHtml() + endNode; + } + + frag = parseHTML(node); + } else { + appendChild(frag, node); + + if (endNode) { + appendChild(frag, base.selectedRange().extractContents()); + appendChild(frag, endNode); + } + } + + if (!(lastChild = frag.lastChild)) { + return; + } + + while (!isInline(lastChild.lastChild, true)) { + lastChild = lastChild.lastChild; + } + + if (canHaveChildren(lastChild)) { + // Webkit won't allow the cursor to be placed inside an + // empty tag, so add a zero width space to it. + if (!lastChild.lastChild) { + appendChild(lastChild, document.createTextNode('\u200B')); + } + } else { + lastChild = frag; + } + + base.removeMarkers(); + + // Append marks to last child so when restored cursor will be in + // the right place + appendChild(lastChild, _createMarker(startMarker)); + appendChild(lastChild, _createMarker(endMarker)); + + if (returnHtml) { + var div = createElement('div'); + appendChild(div, frag); + + return div.innerHTML; + } + + return frag; + }; + + /** + * The same as insertHTML except with DOM nodes instead + * + * Warning: the nodes must belong to the + * document they are being inserted into. Some browsers + * will throw exceptions if they don't. + * + * Returns boolean false on fail + * + * @param {Node} node + * @param {Node} endNode + * @return {false|undefined} + * @function + * @name insertNode + * @memberOf RangeHelper.prototype + */ + base.insertNode = function (node, endNode) { + var first, last, + input = _prepareInput(node, endNode), + range = base.selectedRange(), + parent = range.commonAncestorContainer, + emptyNodes = []; + + if (!input) { + return false; + } + + function removeIfEmpty(node) { + // Only remove empty node if it wasn't already empty + if (node && isEmpty(node) && emptyNodes.indexOf(node) < 0) { + remove(node); + } + } + + if (range.startContainer !== range.endContainer) { + each(parent.childNodes, function (_, node) { + if (isEmpty(node)) { + emptyNodes.push(node); + } + }); + + first = input.firstChild; + last = input.lastChild; + } + + range.deleteContents(); + + // FF allows
    to be selected but inserting a node + // into
    will cause it not to be displayed so must + // insert before the
    in FF. + // 3 = TextNode + if (parent && parent.nodeType !== 3 && !canHaveChildren(parent)) { + insertBefore(input, parent); + } else { + range.insertNode(input); + + // If a node was split or its contents deleted, remove any resulting + // empty tags. For example: + //

    |test

    test|
    + // When deleteContents could become: + //

    |
    + // So remove the empty ones + removeIfEmpty(first && first.previousSibling); + removeIfEmpty(last && last.nextSibling); + } + + base.restoreRange(); + }; + + /** + * Clones the selected Range + * + * @return {Range} + * @function + * @name cloneSelected + * @memberOf RangeHelper.prototype + */ + base.cloneSelected = function () { + var range = base.selectedRange(); + + if (range) { + return range.cloneRange(); + } + }; + + /** + * Gets the selected Range + * + * @return {Range} + * @function + * @name selectedRange + * @memberOf RangeHelper.prototype + */ + base.selectedRange = function () { + var range, firstChild, + sel = win.getSelection(); + + if (!sel) { + return; + } + + // When creating a new range, set the start to the first child + // element of the body element to avoid errors in FF. + if (sel.rangeCount <= 0) { + firstChild = doc.body; + while (firstChild.firstChild) { + firstChild = firstChild.firstChild; + } + + range = doc.createRange(); + // Must be setStartBefore otherwise it can cause infinite + // loops with lists in WebKit. See issue 442 + range.setStartBefore(firstChild); + + sel.addRange(range); + } + + if (sel.rangeCount > 0) { + range = sel.getRangeAt(0); + } + + return range; + }; + + /** + * Gets if there is currently a selection + * + * @return {boolean} + * @function + * @name hasSelection + * @since 1.4.4 + * @memberOf RangeHelper.prototype + */ + base.hasSelection = function () { + var sel = win.getSelection(); + + return sel && sel.rangeCount > 0; + }; + + /** + * Gets the currently selected HTML + * + * @return {string} + * @function + * @name selectedHtml + * @memberOf RangeHelper.prototype + */ + base.selectedHtml = function () { + var div, + range = base.selectedRange(); + + if (range) { + div = createElement('p', {}, doc); + appendChild(div, range.cloneContents()); + + return div.innerHTML; + } + + return ''; + }; + + /** + * Gets the parent node of the selected contents in the range + * + * @return {HTMLElement} + * @function + * @name parentNode + * @memberOf RangeHelper.prototype + */ + base.parentNode = function () { + var range = base.selectedRange(); + + if (range) { + return range.commonAncestorContainer; + } + }; + + /** + * Gets the first block level parent of the selected + * contents of the range. + * + * @return {HTMLElement} + * @function + * @name getFirstBlockParent + * @memberOf RangeHelper.prototype + */ + /** + * Gets the first block level parent of the selected + * contents of the range. + * + * @param {Node} [n] The element to get the first block level parent from + * @return {HTMLElement} + * @function + * @name getFirstBlockParent^2 + * @since 1.4.1 + * @memberOf RangeHelper.prototype + */ + base.getFirstBlockParent = function (node) { + var func = function (elm) { + if (!isInline(elm, true)) { + return elm; + } + + elm = elm ? elm.parentNode : null; + + return elm ? func(elm) : elm; + }; + + return func(node || base.parentNode()); + }; + + /** + * Inserts a node at either the start or end of the current selection + * + * @param {Bool} start + * @param {Node} node + * @function + * @name insertNodeAt + * @memberOf RangeHelper.prototype + */ + base.insertNodeAt = function (start, node) { + var currentRange = base.selectedRange(), + range = base.cloneSelected(); + + if (!range) { + return false; + } + + range.collapse(start); + range.insertNode(node); + + // Reselect the current range. + // Fixes issue with Chrome losing the selection. Issue#82 + base.selectRange(currentRange); + }; + + /** + * Creates a marker node + * + * @param {string} id + * @return {HTMLSpanElement} + * @private + */ + _createMarker = function (id) { + base.removeMarker(id); + + var marker = createElement('span', { + id: id, + className: 'sceditor-selection sceditor-ignore', + style: 'display:none;line-height:0' + }, doc); + + marker.innerHTML = ' '; + + return marker; + }; + + /** + * Inserts start/end markers for the current selection + * which can be used by restoreRange to re-select the + * range. + * + * @memberOf RangeHelper.prototype + * @function + * @name insertMarkers + */ + base.insertMarkers = function () { + var currentRange = base.selectedRange(); + var startNode = _createMarker(startMarker); + + base.removeMarkers(); + base.insertNodeAt(true, startNode); + + // Fixes issue with end marker sometimes being placed before + // the start marker when the range is collapsed. + if (currentRange && currentRange.collapsed) { + startNode.parentNode.insertBefore( + _createMarker(endMarker), startNode.nextSibling); + } else { + base.insertNodeAt(false, _createMarker(endMarker)); + } + }; + + /** + * Gets the marker with the specified ID + * + * @param {string} id + * @return {Node} + * @function + * @name getMarker + * @memberOf RangeHelper.prototype + */ + base.getMarker = function (id) { + return doc.getElementById(id); + }; + + /** + * Removes the marker with the specified ID + * + * @param {string} id + * @function + * @name removeMarker + * @memberOf RangeHelper.prototype + */ + base.removeMarker = function (id) { + var marker = base.getMarker(id); + + if (marker) { + remove(marker); + } + }; + + /** + * Removes the start/end markers + * + * @function + * @name removeMarkers + * @memberOf RangeHelper.prototype + */ + base.removeMarkers = function () { + base.removeMarker(startMarker); + base.removeMarker(endMarker); + }; + + /** + * Saves the current range location. Alias of insertMarkers() + * + * @function + * @name saveRage + * @memberOf RangeHelper.prototype + */ + base.saveRange = function () { + base.insertMarkers(); + }; + + /** + * Select the specified range + * + * @param {Range} range + * @function + * @name selectRange + * @memberOf RangeHelper.prototype + */ + base.selectRange = function (range) { + var lastChild; + var sel = win.getSelection(); + var container = range.endContainer; + + // Check if cursor is set after a BR when the BR is the only + // child of the parent. In Firefox this causes a line break + // to occur when something is typed. See issue #321 + if (range.collapsed && container && + !isInline(container, true)) { + + lastChild = container.lastChild; + while (lastChild && is(lastChild, '.sceditor-ignore')) { + lastChild = lastChild.previousSibling; + } + + if (is(lastChild, 'br')) { + var rng = doc.createRange(); + rng.setEndAfter(lastChild); + rng.collapse(false); + + if (base.compare(range, rng)) { + range.setStartBefore(lastChild); + range.collapse(true); + } + } + } + + if (sel) { + base.clear(); + sel.addRange(range); + } + }; + + /** + * Restores the last range saved by saveRange() or insertMarkers() + * + * @function + * @name restoreRange + * @memberOf RangeHelper.prototype + */ + base.restoreRange = function () { + var isCollapsed, + range = base.selectedRange(), + start = base.getMarker(startMarker), + end = base.getMarker(endMarker); + + if (!start || !end || !range) { + return false; + } + + isCollapsed = start.nextSibling === end; + + range = doc.createRange(); + range.setStartBefore(start); + range.setEndAfter(end); + + if (isCollapsed) { + range.collapse(true); + } + + base.selectRange(range); + base.removeMarkers(); + }; + + /** + * Selects the text left and right of the current selection + * + * @param {number} left + * @param {number} right + * @since 1.4.3 + * @function + * @name selectOuterText + * @memberOf RangeHelper.prototype + */ + base.selectOuterText = function (left, right) { + var start, end, + range = base.cloneSelected(); + + if (!range) { + return false; + } + + range.collapse(false); + + start = outerText(range, true, left); + end = outerText(range, false, right); + + range.setStart(start.node, start.offset); + range.setEnd(end.node, end.offset); + + base.selectRange(range); + }; + + /** + * Gets the text left or right of the current selection + * + * @param {boolean} before + * @param {number} length + * @return {string} + * @since 1.4.3 + * @function + * @name selectOuterText + * @memberOf RangeHelper.prototype + */ + base.getOuterText = function (before, length) { + var range = base.cloneSelected(); + + if (!range) { + return ''; + } + + range.collapse(!before); + + return outerText(range, before, length).text; + }; + + /** + * Replaces keywords with values based on the current caret position + * + * @param {Array} keywords + * @param {boolean} includeAfter If to include the text after the + * current caret position or just + * text before + * @param {boolean} keywordsSorted If the keywords array is pre + * sorted shortest to longest + * @param {number} longestKeyword Length of the longest keyword + * @param {boolean} requireWhitespace If the key must be surrounded + * by whitespace + * @param {string} keypressChar If this is being called from + * a keypress event, this should be + * set to the pressed character + * @return {boolean} + * @function + * @name replaceKeyword + * @memberOf RangeHelper.prototype + */ + // eslint-disable-next-line max-params + base.replaceKeyword = function ( + keywords, + includeAfter, + keywordsSorted, + longestKeyword, + requireWhitespace, + keypressChar + ) { + if (!keywordsSorted) { + keywords.sort(function (a, b) { + return a[0].length - b[0].length; + }); + } + + var outerText, match, matchPos, startIndex, + leftLen, charsLeft, keyword, keywordLen, + whitespaceRegex = '(^|[\\s\xA0\u2002\u2003\u2009])', + keywordIdx = keywords.length, + whitespaceLen = requireWhitespace ? 1 : 0, + maxKeyLen = longestKeyword || + keywords[keywordIdx - 1][0].length; + + if (requireWhitespace) { + maxKeyLen++; + } + + keypressChar = keypressChar || ''; + outerText = base.getOuterText(true, maxKeyLen); + leftLen = outerText.length; + outerText += keypressChar; + + if (includeAfter) { + outerText += base.getOuterText(false, maxKeyLen); + } + + while (keywordIdx--) { + keyword = keywords[keywordIdx][0]; + keywordLen = keyword.length; + startIndex = Math.max(0, leftLen - keywordLen - whitespaceLen); + matchPos = -1; + + if (requireWhitespace) { + match = outerText + .substr(startIndex) + .match(new RegExp(whitespaceRegex + + regex(keyword) + whitespaceRegex)); + + if (match) { + // Add the length of the text that was removed by + // substr() and also add 1 for the whitespace + matchPos = match.index + startIndex + match[1].length; + } + } else { + matchPos = outerText.indexOf(keyword, startIndex); + } + + if (matchPos > -1) { + // Make sure the match is between before and + // after, not just entirely in one side or the other + if (matchPos <= leftLen && + matchPos + keywordLen + whitespaceLen >= leftLen) { + charsLeft = leftLen - matchPos; + + // If the keypress char is white space then it should + // not be replaced, only chars that are part of the + // key should be replaced. + base.selectOuterText( + charsLeft, + keywordLen - charsLeft - + (/^\S/.test(keypressChar) ? 1 : 0) + ); + + base.insertHTML(keywords[keywordIdx][1]); + return true; + } + } + } + + return false; + }; + + /** + * Compares two ranges. + * + * If rangeB is undefined it will be set to + * the current selected range + * + * @param {Range} rngA + * @param {Range} [rngB] + * @return {boolean} + * @function + * @name compare + * @memberOf RangeHelper.prototype + */ + base.compare = function (rngA, rngB) { + if (!rngB) { + rngB = base.selectedRange(); + } + + if (!rngA || !rngB) { + return !rngA && !rngB; + } + + return rngA.compareBoundaryPoints(Range.END_TO_END, rngB) === 0 && + rngA.compareBoundaryPoints(Range.START_TO_START, rngB) === 0; + }; + + /** + * Removes any current selection + * + * @since 1.4.6 + * @function + * @name clear + * @memberOf RangeHelper.prototype + */ + base.clear = function () { + var sel = win.getSelection(); + + if (sel) { + if (sel.removeAllRanges) { + sel.removeAllRanges(); + } else if (sel.empty) { + sel.empty(); + } + } + }; + } + + var USER_AGENT = navigator.userAgent; + + /** + * Detects if the browser is iOS + * + * Needed to fix iOS specific bugs + * + * @function + * @name ios + * @memberOf jQuery.sceditor + * @type {boolean} + */ + var ios = /iPhone|iPod|iPad| wosbrowser\//i.test(USER_AGENT); + + /** + * If the browser supports WYSIWYG editing (e.g. older mobile browsers). + * + * @function + * @name isWysiwygSupported + * @return {boolean} + */ + var isWysiwygSupported = (function () { + var match, isUnsupported; + + // IE is the only browser to support documentMode + var ie = !!window.document.documentMode; + var legacyEdge = '-ms-ime-align' in document.documentElement.style; + + var div = document.createElement('div'); + div.contentEditable = true; + + // Check if the contentEditable attribute is supported + if (!('contentEditable' in document.documentElement) || + div.contentEditable !== 'true') { + return false; + } + + // I think blackberry supports contentEditable or will at least + // give a valid value for the contentEditable detection above + // so it isn't included in the below tests. + + // I hate having to do UA sniffing but some mobile browsers say they + // support contentediable when it isn't usable, i.e. you can't enter + // text. + // This is the only way I can think of to detect them which is also how + // every other editor I've seen deals with this issue. + + // Exclude Opera mobile and mini + isUnsupported = /Opera Mobi|Opera Mini/i.test(USER_AGENT); + + if (/Android/i.test(USER_AGENT)) { + isUnsupported = true; + + if (/Safari/.test(USER_AGENT)) { + // Android browser 534+ supports content editable + // This also matches Chrome which supports content editable too + match = /Safari\/(\d+)/.exec(USER_AGENT); + isUnsupported = (!match || !match[1] ? true : match[1] < 534); + } + } + + // The current version of Amazon Silk supports it, older versions didn't + // As it uses webkit like Android, assume it's the same and started + // working at versions >= 534 + if (/ Silk\//i.test(USER_AGENT)) { + match = /AppleWebKit\/(\d+)/.exec(USER_AGENT); + isUnsupported = (!match || !match[1] ? true : match[1] < 534); + } + + // iOS 5+ supports content editable + if (ios) { + // Block any version <= 4_x(_x) + isUnsupported = /OS [0-4](_\d)+ like Mac/i.test(USER_AGENT); + } + + // Firefox does support WYSIWYG on mobiles so override + // any previous value if using FF + if (/Firefox/i.test(USER_AGENT)) { + isUnsupported = false; + } + + if (/OneBrowser/i.test(USER_AGENT)) { + isUnsupported = false; + } + + // UCBrowser works but doesn't give a unique user agent + if (navigator.vendor === 'UCWEB') { + isUnsupported = false; + } + + // IE and legacy edge are not supported any more + if (ie || legacyEdge) { + isUnsupported = true; + } + + return !isUnsupported; + }()); + + /** + * Checks all emoticons are surrounded by whitespace and + * replaces any that aren't with with their emoticon code. + * + * @param {HTMLElement} node + * @param {rangeHelper} rangeHelper + * @return {void} + */ + function checkWhitespace(node, rangeHelper) { + var noneWsRegex = /[^\s\xA0\u2002\u2003\u2009]+/; + var emoticons = node && find(node, 'img[data-sceditor-emoticon]'); + + if (!node || !emoticons.length) { + return; + } + + for (var i = 0; i < emoticons.length; i++) { + var emoticon = emoticons[i]; + var parent = emoticon.parentNode; + var prev = emoticon.previousSibling; + var next = emoticon.nextSibling; + + if ((!prev || !noneWsRegex.test(prev.nodeValue.slice(-1))) && + (!next || !noneWsRegex.test((next.nodeValue || '')[0]))) { + continue; + } + + var range = rangeHelper.cloneSelected(); + var rangeStart = -1; + var rangeStartContainer = range.startContainer; + var previousText = prev.nodeValue || ''; + + previousText += data(emoticon, 'sceditor-emoticon'); + + // If the cursor is after the removed emoticon, add + // the length of the newly added text to it + if (rangeStartContainer === next) { + rangeStart = previousText.length + range.startOffset; + } + + // If the cursor is set before the next node, set it to + // the end of the new text node + if (rangeStartContainer === node && + node.childNodes[range.startOffset] === next) { + rangeStart = previousText.length; + } + + // If the cursor is set before the removed emoticon, + // just keep it at that position + if (rangeStartContainer === prev) { + rangeStart = range.startOffset; + } + + if (!next || next.nodeType !== TEXT_NODE) { + next = parent.insertBefore( + parent.ownerDocument.createTextNode(''), next + ); + } + + next.insertData(0, previousText); + remove(prev); + remove(emoticon); + + // Need to update the range starting position if it's been modified + if (rangeStart > -1) { + range.setStart(next, rangeStart); + range.collapse(true); + rangeHelper.selectRange(range); + } + } + } + /** + * Replaces any emoticons inside the root node with images. + * + * emoticons should be an object where the key is the emoticon + * code and the value is the HTML to replace it with. + * + * @param {HTMLElement} root + * @param {Object} emoticons + * @param {boolean} emoticonsCompat + * @return {void} + */ + function replace(root, emoticons, emoticonsCompat) { + var doc = root.ownerDocument; + var space = '(^|\\s|\xA0|\u2002|\u2003|\u2009|$)'; + var emoticonCodes = []; + var emoticonRegex = {}; + + // TODO: Make this tag configurable. + if (parent(root, 'code')) { + return; + } + + each(emoticons, function (key) { + emoticonRegex[key] = new RegExp(space + regex(key) + space); + emoticonCodes.push(key); + }); + + // Sort keys longest to shortest so that longer keys + // take precedence (avoids bugs with shorter keys partially + // matching longer ones) + emoticonCodes.sort(function (a, b) { + return b.length - a.length; + }); + + (function convert(node) { + node = node.firstChild; + + while (node) { + // TODO: Make this tag configurable. + if (node.nodeType === ELEMENT_NODE && !is(node, 'code')) { + convert(node); + } + + if (node.nodeType === TEXT_NODE) { + for (var i = 0; i < emoticonCodes.length; i++) { + var text = node.nodeValue; + var key = emoticonCodes[i]; + var index = emoticonsCompat ? + text.search(emoticonRegex[key]) : + text.indexOf(key); + + if (index > -1) { + // When emoticonsCompat is enabled this will be the + // position after any white space + var startIndex = text.indexOf(key, index); + var fragment = parseHTML(emoticons[key], doc); + var after = text.substr(startIndex + key.length); + + fragment.appendChild(doc.createTextNode(after)); + + node.nodeValue = text.substr(0, startIndex); + node.parentNode + .insertBefore(fragment, node.nextSibling); + } + } + } + + node = node.nextSibling; + } + }(root)); + } + + /*! @license DOMPurify 2.4.3 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.4.3/LICENSE */ + + function _typeof(obj) { + "@babel/helpers - typeof"; + + return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { + return typeof obj; + } : function (obj) { + return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }, _typeof(obj); + } + + function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { + o.__proto__ = p; + return o; + }; + + return _setPrototypeOf(o, p); + } + + function _isNativeReflectConstruct() { + if (typeof Reflect === "undefined" || !Reflect.construct) return false; + if (Reflect.construct.sham) return false; + if (typeof Proxy === "function") return true; + + try { + Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); + return true; + } catch (e) { + return false; + } + } + + function _construct(Parent, args, Class) { + if (_isNativeReflectConstruct()) { + _construct = Reflect.construct; + } else { + _construct = function _construct(Parent, args, Class) { + var a = [null]; + a.push.apply(a, args); + var Constructor = Function.bind.apply(Parent, a); + var instance = new Constructor(); + if (Class) _setPrototypeOf(instance, Class.prototype); + return instance; + }; + } + + return _construct.apply(null, arguments); + } + + function _toConsumableArray(arr) { + return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); + } + + function _arrayWithoutHoles(arr) { + if (Array.isArray(arr)) return _arrayLikeToArray(arr); + } + + function _iterableToArray(iter) { + if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); + } + + function _unsupportedIterableToArray(o, minLen) { + if (!o) return; + if (typeof o === "string") return _arrayLikeToArray(o, minLen); + var n = Object.prototype.toString.call(o).slice(8, -1); + if (n === "Object" && o.constructor) n = o.constructor.name; + if (n === "Map" || n === "Set") return Array.from(o); + if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); + } + + function _arrayLikeToArray(arr, len) { + if (len == null || len > arr.length) len = arr.length; + + for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; + + return arr2; + } + + function _nonIterableSpread() { + throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); + } + + var hasOwnProperty = Object.hasOwnProperty, + setPrototypeOf = Object.setPrototypeOf, + isFrozen = Object.isFrozen, + getPrototypeOf = Object.getPrototypeOf, + getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; + var freeze = Object.freeze, + seal = Object.seal, + create = Object.create; // eslint-disable-line import/no-mutable-exports + + var _ref = typeof Reflect !== 'undefined' && Reflect, + apply = _ref.apply, + construct = _ref.construct; + + if (!apply) { + apply = function apply(fun, thisValue, args) { + return fun.apply(thisValue, args); + }; + } + + if (!freeze) { + freeze = function freeze(x) { + return x; + }; + } + + if (!seal) { + seal = function seal(x) { + return x; + }; + } + + if (!construct) { + construct = function construct(Func, args) { + return _construct(Func, _toConsumableArray(args)); + }; + } + + var arrayForEach = unapply(Array.prototype.forEach); + var arrayPop = unapply(Array.prototype.pop); + var arrayPush = unapply(Array.prototype.push); + var stringToLowerCase = unapply(String.prototype.toLowerCase); + var stringToString = unapply(String.prototype.toString); + var stringMatch = unapply(String.prototype.match); + var stringReplace = unapply(String.prototype.replace); + var stringIndexOf = unapply(String.prototype.indexOf); + var stringTrim = unapply(String.prototype.trim); + var regExpTest = unapply(RegExp.prototype.test); + var typeErrorCreate = unconstruct(TypeError); + function unapply(func) { + return function (thisArg) { + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + return apply(func, thisArg, args); + }; + } + function unconstruct(func) { + return function () { + for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + + return construct(func, args); + }; + } + /* Add properties to a lookup table */ + + function addToSet(set, array, transformCaseFunc) { + transformCaseFunc = transformCaseFunc ? transformCaseFunc : stringToLowerCase; + + if (setPrototypeOf) { + // Make 'in' and truthy checks like Boolean(set.constructor) + // independent of any properties defined on Object.prototype. + // Prevent prototype setters from intercepting set as a this value. + setPrototypeOf(set, null); + } + + var l = array.length; + + while (l--) { + var element = array[l]; + + if (typeof element === 'string') { + var lcElement = transformCaseFunc(element); + + if (lcElement !== element) { + // Config presets (e.g. tags.js, attrs.js) are immutable. + if (!isFrozen(array)) { + array[l] = lcElement; + } + + element = lcElement; + } + } + + set[element] = true; + } + + return set; + } + /* Shallow clone an object */ + + function clone(object) { + var newObject = create(null); + var property; + + for (property in object) { + if (apply(hasOwnProperty, object, [property]) === true) { + newObject[property] = object[property]; + } + } + + return newObject; + } + /* IE10 doesn't support __lookupGetter__ so lets' + * simulate it. It also automatically checks + * if the prop is function or getter and behaves + * accordingly. */ + + function lookupGetter(object, prop) { + while (object !== null) { + var desc = getOwnPropertyDescriptor(object, prop); + + if (desc) { + if (desc.get) { + return unapply(desc.get); + } + + if (typeof desc.value === 'function') { + return unapply(desc.value); + } + } + + object = getPrototypeOf(object); + } + + function fallbackValue(element) { + console.warn('fallback value for', element); + return null; + } + + return fallbackValue; + } + + var html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'select', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']); // SVG + + var svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']); + var svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']); // List of SVG elements that are disallowed by default. + // We still need to know them so that we can do namespace + // checks properly in case one wants to add them to + // allow-list. + + var svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'fedropshadow', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', 'use']); + var mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover']); // Similarly to SVG, we want to know all MathML elements, + // even those that we disallow by default. + + var mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']); + var text = freeze(['#text']); + + var html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'playsinline', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'xmlns', 'slot']); + var svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']); + var mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']); + var xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']); + + var MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode + + var ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm); + var TMPLIT_EXPR = seal(/\${[\w\W]*}/gm); + var DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]/); // eslint-disable-line no-useless-escape + + var ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape + + var IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape + ); + var IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i); + var ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex + ); + var DOCTYPE_NAME = seal(/^html$/i); + + var getGlobal = function getGlobal() { + return typeof window === 'undefined' ? null : window; + }; + /** + * Creates a no-op policy for internal use only. + * Don't export this function outside this module! + * @param {?TrustedTypePolicyFactory} trustedTypes The policy factory. + * @param {Document} document The document object (to determine policy name suffix) + * @return {?TrustedTypePolicy} The policy created (or null, if Trusted Types + * are not supported). + */ + + + var _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, document) { + if (_typeof(trustedTypes) !== 'object' || typeof trustedTypes.createPolicy !== 'function') { + return null; + } // Allow the callers to control the unique policy name + // by adding a data-tt-policy-suffix to the script element with the DOMPurify. + // Policy creation with duplicate names throws in Trusted Types. + + + var suffix = null; + var ATTR_NAME = 'data-tt-policy-suffix'; + + if (document.currentScript && document.currentScript.hasAttribute(ATTR_NAME)) { + suffix = document.currentScript.getAttribute(ATTR_NAME); + } + + var policyName = 'dompurify' + (suffix ? '#' + suffix : ''); + + try { + return trustedTypes.createPolicy(policyName, { + createHTML: function createHTML(html) { + return html; + }, + createScriptURL: function createScriptURL(scriptUrl) { + return scriptUrl; + } + }); + } catch (_) { + // Policy creation failed (most likely another DOMPurify script has + // already run). Skip creating the policy, as this will only cause errors + // if TT are enforced. + console.warn('TrustedTypes policy ' + policyName + ' could not be created.'); + return null; + } + }; + + function createDOMPurify() { + var window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal(); + + var DOMPurify = function DOMPurify(root) { + return createDOMPurify(root); + }; + /** + * Version label, exposed for easier checks + * if DOMPurify is up to date or not + */ + + + DOMPurify.version = '2.4.3'; + /** + * Array of elements that DOMPurify removed during sanitation. + * Empty if nothing was removed. + */ + + DOMPurify.removed = []; + + if (!window || !window.document || window.document.nodeType !== 9) { + // Not running in a browser, provide a factory function + // so that you can pass your own Window + DOMPurify.isSupported = false; + return DOMPurify; + } + + var originalDocument = window.document; + var document = window.document; + var DocumentFragment = window.DocumentFragment, + HTMLTemplateElement = window.HTMLTemplateElement, + Node = window.Node, + Element = window.Element, + NodeFilter = window.NodeFilter, + _window$NamedNodeMap = window.NamedNodeMap, + NamedNodeMap = _window$NamedNodeMap === void 0 ? window.NamedNodeMap || window.MozNamedAttrMap : _window$NamedNodeMap, + HTMLFormElement = window.HTMLFormElement, + DOMParser = window.DOMParser, + trustedTypes = window.trustedTypes; + var ElementPrototype = Element.prototype; + var cloneNode = lookupGetter(ElementPrototype, 'cloneNode'); + var getNextSibling = lookupGetter(ElementPrototype, 'nextSibling'); + var getChildNodes = lookupGetter(ElementPrototype, 'childNodes'); + var getParentNode = lookupGetter(ElementPrototype, 'parentNode'); // As per issue #47, the web-components registry is inherited by a + // new document created via createHTMLDocument. As per the spec + // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries) + // a new empty registry is used when creating a template contents owner + // document, so we use that as our parent document to ensure nothing + // is inherited. + + if (typeof HTMLTemplateElement === 'function') { + var template = document.createElement('template'); + + if (template.content && template.content.ownerDocument) { + document = template.content.ownerDocument; + } + } + + var trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, originalDocument); + + var emptyHTML = trustedTypesPolicy ? trustedTypesPolicy.createHTML('') : ''; + var _document = document, + implementation = _document.implementation, + createNodeIterator = _document.createNodeIterator, + createDocumentFragment = _document.createDocumentFragment, + getElementsByTagName = _document.getElementsByTagName; + var importNode = originalDocument.importNode; + var documentMode = {}; + + try { + documentMode = clone(document).documentMode ? document.documentMode : {}; + } catch (_) {} + + var hooks = {}; + /** + * Expose whether this browser supports running the full DOMPurify. + */ + + DOMPurify.isSupported = typeof getParentNode === 'function' && implementation && typeof implementation.createHTMLDocument !== 'undefined' && documentMode !== 9; + var MUSTACHE_EXPR$1 = MUSTACHE_EXPR, + ERB_EXPR$1 = ERB_EXPR, + TMPLIT_EXPR$1 = TMPLIT_EXPR, + DATA_ATTR$1 = DATA_ATTR, + ARIA_ATTR$1 = ARIA_ATTR, + IS_SCRIPT_OR_DATA$1 = IS_SCRIPT_OR_DATA, + ATTR_WHITESPACE$1 = ATTR_WHITESPACE; + var IS_ALLOWED_URI$1 = IS_ALLOWED_URI; + /** + * We consider the elements and attributes below to be safe. Ideally + * don't add any new ones but feel free to remove unwanted ones. + */ + + /* allowed element names */ + + var ALLOWED_TAGS = null; + var DEFAULT_ALLOWED_TAGS = addToSet({}, [].concat(_toConsumableArray(html$1), _toConsumableArray(svg$1), _toConsumableArray(svgFilters), _toConsumableArray(mathMl$1), _toConsumableArray(text))); + /* Allowed attribute names */ + + var ALLOWED_ATTR = null; + var DEFAULT_ALLOWED_ATTR = addToSet({}, [].concat(_toConsumableArray(html), _toConsumableArray(svg), _toConsumableArray(mathMl), _toConsumableArray(xml))); + /* + * Configure how DOMPUrify should handle custom elements and their attributes as well as customized built-in elements. + * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements) + * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list) + * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`. + */ + + var CUSTOM_ELEMENT_HANDLING = Object.seal(Object.create(null, { + tagNameCheck: { + writable: true, + configurable: false, + enumerable: true, + value: null + }, + attributeNameCheck: { + writable: true, + configurable: false, + enumerable: true, + value: null + }, + allowCustomizedBuiltInElements: { + writable: true, + configurable: false, + enumerable: true, + value: false + } + })); + /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */ + + var FORBID_TAGS = null; + /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */ + + var FORBID_ATTR = null; + /* Decide if ARIA attributes are okay */ + + var ALLOW_ARIA_ATTR = true; + /* Decide if custom data attributes are okay */ + + var ALLOW_DATA_ATTR = true; + /* Decide if unknown protocols are okay */ + + var ALLOW_UNKNOWN_PROTOCOLS = false; + /* Output should be safe for common template engines. + * This means, DOMPurify removes data attributes, mustaches and ERB + */ + + var SAFE_FOR_TEMPLATES = false; + /* Decide if document with ... should be returned */ + + var WHOLE_DOCUMENT = false; + /* Track whether config is already set on this instance of DOMPurify. */ + + var SET_CONFIG = false; + /* Decide if all elements (e.g. style, script) must be children of + * document.body. By default, browsers might move them to document.head */ + + var FORCE_BODY = false; + /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html + * string (or a TrustedHTML object if Trusted Types are supported). + * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead + */ + + var RETURN_DOM = false; + /* Decide if a DOM `DocumentFragment` should be returned, instead of a html + * string (or a TrustedHTML object if Trusted Types are supported) */ + + var RETURN_DOM_FRAGMENT = false; + /* Try to return a Trusted Type object instead of a string, return a string in + * case Trusted Types are not supported */ + + var RETURN_TRUSTED_TYPE = false; + /* Output should be free from DOM clobbering attacks? + * This sanitizes markups named with colliding, clobberable built-in DOM APIs. + */ + + var SANITIZE_DOM = true; + /* Achieve full DOM Clobbering protection by isolating the namespace of named + * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules. + * + * HTML/DOM spec rules that enable DOM Clobbering: + * - Named Access on Window (§7.3.3) + * - DOM Tree Accessors (§3.1.5) + * - Form Element Parent-Child Relations (§4.10.3) + * - Iframe srcdoc / Nested WindowProxies (§4.8.5) + * - HTMLCollection (§4.2.10.2) + * + * Namespace isolation is implemented by prefixing `id` and `name` attributes + * with a constant string, i.e., `user-content-` + */ + + var SANITIZE_NAMED_PROPS = false; + var SANITIZE_NAMED_PROPS_PREFIX = 'user-content-'; + /* Keep element content when removing element? */ + + var KEEP_CONTENT = true; + /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead + * of importing it into a new Document and returning a sanitized copy */ + + var IN_PLACE = false; + /* Allow usage of profiles like html, svg and mathMl */ + + var USE_PROFILES = {}; + /* Tags to ignore content of when KEEP_CONTENT is true */ + + var FORBID_CONTENTS = null; + var DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']); + /* Tags that are safe for data: URIs */ + + var DATA_URI_TAGS = null; + var DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']); + /* Attributes safe for values like "javascript:" */ + + var URI_SAFE_ATTRIBUTES = null; + var DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']); + var MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML'; + var SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; + var HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; + /* Document namespace */ + + var NAMESPACE = HTML_NAMESPACE; + var IS_EMPTY_INPUT = false; + /* Allowed XHTML+XML namespaces */ + + var ALLOWED_NAMESPACES = null; + var DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString); + /* Parsing of strict XHTML documents */ + + var PARSER_MEDIA_TYPE; + var SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html']; + var DEFAULT_PARSER_MEDIA_TYPE = 'text/html'; + var transformCaseFunc; + /* Keep a reference to config to pass to hooks */ + + var CONFIG = null; + /* Ideally, do not touch anything below this line */ + + /* ______________________________________________ */ + + var formElement = document.createElement('form'); + + var isRegexOrFunction = function isRegexOrFunction(testValue) { + return testValue instanceof RegExp || testValue instanceof Function; + }; + /** + * _parseConfig + * + * @param {Object} cfg optional config literal + */ + // eslint-disable-next-line complexity + + + var _parseConfig = function _parseConfig(cfg) { + if (CONFIG && CONFIG === cfg) { + return; + } + /* Shield configuration object from tampering */ + + + if (!cfg || _typeof(cfg) !== 'object') { + cfg = {}; + } + /* Shield configuration object from prototype pollution */ + + + cfg = clone(cfg); + PARSER_MEDIA_TYPE = // eslint-disable-next-line unicorn/prefer-includes + SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? PARSER_MEDIA_TYPE = DEFAULT_PARSER_MEDIA_TYPE : PARSER_MEDIA_TYPE = cfg.PARSER_MEDIA_TYPE; // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is. + + transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase; + /* Set configuration parameters */ + + ALLOWED_TAGS = 'ALLOWED_TAGS' in cfg ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS; + ALLOWED_ATTR = 'ALLOWED_ATTR' in cfg ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR; + ALLOWED_NAMESPACES = 'ALLOWED_NAMESPACES' in cfg ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES; + URI_SAFE_ATTRIBUTES = 'ADD_URI_SAFE_ATTR' in cfg ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), // eslint-disable-line indent + cfg.ADD_URI_SAFE_ATTR, // eslint-disable-line indent + transformCaseFunc // eslint-disable-line indent + ) // eslint-disable-line indent + : DEFAULT_URI_SAFE_ATTRIBUTES; + DATA_URI_TAGS = 'ADD_DATA_URI_TAGS' in cfg ? addToSet(clone(DEFAULT_DATA_URI_TAGS), // eslint-disable-line indent + cfg.ADD_DATA_URI_TAGS, // eslint-disable-line indent + transformCaseFunc // eslint-disable-line indent + ) // eslint-disable-line indent + : DEFAULT_DATA_URI_TAGS; + FORBID_CONTENTS = 'FORBID_CONTENTS' in cfg ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS; + FORBID_TAGS = 'FORBID_TAGS' in cfg ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : {}; + FORBID_ATTR = 'FORBID_ATTR' in cfg ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : {}; + USE_PROFILES = 'USE_PROFILES' in cfg ? cfg.USE_PROFILES : false; + ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true + + ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true + + ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false + + SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false + + WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false + + RETURN_DOM = cfg.RETURN_DOM || false; // Default false + + RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false + + RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false + + FORCE_BODY = cfg.FORCE_BODY || false; // Default false + + SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true + + SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false + + KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true + + IN_PLACE = cfg.IN_PLACE || false; // Default false + + IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI$1; + NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE; + + if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) { + CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck; + } + + if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) { + CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck; + } + + if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') { + CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements; + } + + if (SAFE_FOR_TEMPLATES) { + ALLOW_DATA_ATTR = false; + } + + if (RETURN_DOM_FRAGMENT) { + RETURN_DOM = true; + } + /* Parse profile info */ + + + if (USE_PROFILES) { + ALLOWED_TAGS = addToSet({}, _toConsumableArray(text)); + ALLOWED_ATTR = []; + + if (USE_PROFILES.html === true) { + addToSet(ALLOWED_TAGS, html$1); + addToSet(ALLOWED_ATTR, html); + } + + if (USE_PROFILES.svg === true) { + addToSet(ALLOWED_TAGS, svg$1); + addToSet(ALLOWED_ATTR, svg); + addToSet(ALLOWED_ATTR, xml); + } + + if (USE_PROFILES.svgFilters === true) { + addToSet(ALLOWED_TAGS, svgFilters); + addToSet(ALLOWED_ATTR, svg); + addToSet(ALLOWED_ATTR, xml); + } + + if (USE_PROFILES.mathMl === true) { + addToSet(ALLOWED_TAGS, mathMl$1); + addToSet(ALLOWED_ATTR, mathMl); + addToSet(ALLOWED_ATTR, xml); + } + } + /* Merge configuration parameters */ + + + if (cfg.ADD_TAGS) { + if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { + ALLOWED_TAGS = clone(ALLOWED_TAGS); + } + + addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc); + } + + if (cfg.ADD_ATTR) { + if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) { + ALLOWED_ATTR = clone(ALLOWED_ATTR); + } + + addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc); + } + + if (cfg.ADD_URI_SAFE_ATTR) { + addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc); + } + + if (cfg.FORBID_CONTENTS) { + if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) { + FORBID_CONTENTS = clone(FORBID_CONTENTS); + } + + addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc); + } + /* Add #text in case KEEP_CONTENT is set to true */ + + + if (KEEP_CONTENT) { + ALLOWED_TAGS['#text'] = true; + } + /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */ + + + if (WHOLE_DOCUMENT) { + addToSet(ALLOWED_TAGS, ['html', 'head', 'body']); + } + /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */ + + + if (ALLOWED_TAGS.table) { + addToSet(ALLOWED_TAGS, ['tbody']); + delete FORBID_TAGS.tbody; + } // Prevent further manipulation of configuration. + // Not available in IE8, Safari 5, etc. + + + if (freeze) { + freeze(cfg); + } + + CONFIG = cfg; + }; + + var MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']); + var HTML_INTEGRATION_POINTS = addToSet({}, ['foreignobject', 'desc', 'title', 'annotation-xml']); // Certain elements are allowed in both SVG and HTML + // namespace. We need to specify them explicitly + // so that they don't get erroneously deleted from + // HTML namespace. + + var COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']); + /* Keep track of all possible SVG and MathML tags + * so that we can perform the namespace checks + * correctly. */ + + var ALL_SVG_TAGS = addToSet({}, svg$1); + addToSet(ALL_SVG_TAGS, svgFilters); + addToSet(ALL_SVG_TAGS, svgDisallowed); + var ALL_MATHML_TAGS = addToSet({}, mathMl$1); + addToSet(ALL_MATHML_TAGS, mathMlDisallowed); + /** + * + * + * @param {Element} element a DOM element whose namespace is being checked + * @returns {boolean} Return false if the element has a + * namespace that a spec-compliant parser would never + * return. Return true otherwise. + */ + + var _checkValidNamespace = function _checkValidNamespace(element) { + var parent = getParentNode(element); // In JSDOM, if we're inside shadow DOM, then parentNode + // can be null. We just simulate parent in this case. + + if (!parent || !parent.tagName) { + parent = { + namespaceURI: NAMESPACE, + tagName: 'template' + }; + } + + var tagName = stringToLowerCase(element.tagName); + var parentTagName = stringToLowerCase(parent.tagName); + + if (!ALLOWED_NAMESPACES[element.namespaceURI]) { + return false; + } + + if (element.namespaceURI === SVG_NAMESPACE) { + // The only way to switch from HTML namespace to SVG + // is via . If it happens via any other tag, then + // it should be killed. + if (parent.namespaceURI === HTML_NAMESPACE) { + return tagName === 'svg'; + } // The only way to switch from MathML to SVG is via` + // svg if parent is either or MathML + // text integration points. + + + if (parent.namespaceURI === MATHML_NAMESPACE) { + return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]); + } // We only allow elements that are defined in SVG + // spec. All others are disallowed in SVG namespace. + + + return Boolean(ALL_SVG_TAGS[tagName]); + } + + if (element.namespaceURI === MATHML_NAMESPACE) { + // The only way to switch from HTML namespace to MathML + // is via . If it happens via any other tag, then + // it should be killed. + if (parent.namespaceURI === HTML_NAMESPACE) { + return tagName === 'math'; + } // The only way to switch from SVG to MathML is via + // and HTML integration points + + + if (parent.namespaceURI === SVG_NAMESPACE) { + return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName]; + } // We only allow elements that are defined in MathML + // spec. All others are disallowed in MathML namespace. + + + return Boolean(ALL_MATHML_TAGS[tagName]); + } + + if (element.namespaceURI === HTML_NAMESPACE) { + // The only way to switch from SVG to HTML is via + // HTML integration points, and from MathML to HTML + // is via MathML text integration points + if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) { + return false; + } + + if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) { + return false; + } // We disallow tags that are specific for MathML + // or SVG and should never appear in HTML namespace + + + return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]); + } // For XHTML and XML documents that support custom namespaces + + + if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) { + return true; + } // The code should never reach this place (this means + // that the element somehow got namespace that is not + // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES). + // Return false just in case. + + + return false; + }; + /** + * _forceRemove + * + * @param {Node} node a DOM node + */ + + + var _forceRemove = function _forceRemove(node) { + arrayPush(DOMPurify.removed, { + element: node + }); + + try { + // eslint-disable-next-line unicorn/prefer-dom-node-remove + node.parentNode.removeChild(node); + } catch (_) { + try { + node.outerHTML = emptyHTML; + } catch (_) { + node.remove(); + } + } + }; + /** + * _removeAttribute + * + * @param {String} name an Attribute name + * @param {Node} node a DOM node + */ + + + var _removeAttribute = function _removeAttribute(name, node) { + try { + arrayPush(DOMPurify.removed, { + attribute: node.getAttributeNode(name), + from: node + }); + } catch (_) { + arrayPush(DOMPurify.removed, { + attribute: null, + from: node + }); + } + + node.removeAttribute(name); // We void attribute values for unremovable "is"" attributes + + if (name === 'is' && !ALLOWED_ATTR[name]) { + if (RETURN_DOM || RETURN_DOM_FRAGMENT) { + try { + _forceRemove(node); + } catch (_) {} + } else { + try { + node.setAttribute(name, ''); + } catch (_) {} + } + } + }; + /** + * _initDocument + * + * @param {String} dirty a string of dirty markup + * @return {Document} a DOM, filled with the dirty markup + */ + + + var _initDocument = function _initDocument(dirty) { + /* Create a HTML document */ + var doc; + var leadingWhitespace; + + if (FORCE_BODY) { + dirty = '' + dirty; + } else { + /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */ + var matches = stringMatch(dirty, /^[\r\n\t ]+/); + leadingWhitespace = matches && matches[0]; + } + + if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && NAMESPACE === HTML_NAMESPACE) { + // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict) + dirty = '' + dirty + ''; + } + + var dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty; + /* + * Use the DOMParser API by default, fallback later if needs be + * DOMParser not work for svg when has multiple root element. + */ + + if (NAMESPACE === HTML_NAMESPACE) { + try { + doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE); + } catch (_) {} + } + /* Use createHTMLDocument in case DOMParser is not available */ + + + if (!doc || !doc.documentElement) { + doc = implementation.createDocument(NAMESPACE, 'template', null); + + try { + doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload; + } catch (_) {// Syntax error if dirtyPayload is invalid xml + } + } + + var body = doc.body || doc.documentElement; + + if (dirty && leadingWhitespace) { + body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null); + } + /* Work on whole document or just its body */ + + + if (NAMESPACE === HTML_NAMESPACE) { + return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0]; + } + + return WHOLE_DOCUMENT ? doc.documentElement : body; + }; + /** + * _createIterator + * + * @param {Document} root document/fragment to create iterator for + * @return {Iterator} iterator instance + */ + + + var _createIterator = function _createIterator(root) { + return createNodeIterator.call(root.ownerDocument || root, root, // eslint-disable-next-line no-bitwise + NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT, null, false); + }; + /** + * _isClobbered + * + * @param {Node} elm element to check for clobbering attacks + * @return {Boolean} true if clobbered, false if safe + */ + + + var _isClobbered = function _isClobbered(elm) { + return elm instanceof HTMLFormElement && (typeof elm.nodeName !== 'string' || typeof elm.textContent !== 'string' || typeof elm.removeChild !== 'function' || !(elm.attributes instanceof NamedNodeMap) || typeof elm.removeAttribute !== 'function' || typeof elm.setAttribute !== 'function' || typeof elm.namespaceURI !== 'string' || typeof elm.insertBefore !== 'function' || typeof elm.hasChildNodes !== 'function'); + }; + /** + * _isNode + * + * @param {Node} obj object to check whether it's a DOM node + * @return {Boolean} true is object is a DOM node + */ + + + var _isNode = function _isNode(object) { + return _typeof(Node) === 'object' ? object instanceof Node : object && _typeof(object) === 'object' && typeof object.nodeType === 'number' && typeof object.nodeName === 'string'; + }; + /** + * _executeHook + * Execute user configurable hooks + * + * @param {String} entryPoint Name of the hook's entry point + * @param {Node} currentNode node to work on with the hook + * @param {Object} data additional hook parameters + */ + + + var _executeHook = function _executeHook(entryPoint, currentNode, data) { + if (!hooks[entryPoint]) { + return; + } + + arrayForEach(hooks[entryPoint], function (hook) { + hook.call(DOMPurify, currentNode, data, CONFIG); + }); + }; + /** + * _sanitizeElements + * + * @protect nodeName + * @protect textContent + * @protect removeChild + * + * @param {Node} currentNode to check for permission to exist + * @return {Boolean} true if node was killed, false if left alive + */ + + + var _sanitizeElements = function _sanitizeElements(currentNode) { + var content; + /* Execute a hook if present */ + + _executeHook('beforeSanitizeElements', currentNode, null); + /* Check if element is clobbered or can clobber */ + + + if (_isClobbered(currentNode)) { + _forceRemove(currentNode); + + return true; + } + /* Check if tagname contains Unicode */ + + + if (regExpTest(/[\u0080-\uFFFF]/, currentNode.nodeName)) { + _forceRemove(currentNode); + + return true; + } + /* Now let's check the element's type and name */ + + + var tagName = transformCaseFunc(currentNode.nodeName); + /* Execute a hook if present */ + + _executeHook('uponSanitizeElement', currentNode, { + tagName: tagName, + allowedTags: ALLOWED_TAGS + }); + /* Detect mXSS attempts abusing namespace confusion */ + + + if (currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && (!_isNode(currentNode.content) || !_isNode(currentNode.content.firstElementChild)) && regExpTest(/<[/\w]/g, currentNode.innerHTML) && regExpTest(/<[/\w]/g, currentNode.textContent)) { + _forceRemove(currentNode); + + return true; + } + /* Mitigate a problem with templates inside select */ + + + if (tagName === 'select' && regExpTest(/