From d4f2fcdf7908ffa176350468a85e631f0497646c Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 2 Aug 2022 20:11:02 +0100 Subject: [PATCH 01/76] Started codemirror update, In broken state --- dev/build/esbuild.js | 16 +- package-lock.json | 633 +++++++++++++++------- package.json | 13 +- resources/js/{code.mjs => code/index.mjs} | 161 ++---- resources/js/code/modes.js | 134 +++++ resources/js/code/setups.js | 32 ++ 6 files changed, 661 insertions(+), 328 deletions(-) rename resources/js/{code.mjs => code/index.mjs} (55%) create mode 100644 resources/js/code/modes.js create mode 100644 resources/js/code/setups.js diff --git a/dev/build/esbuild.js b/dev/build/esbuild.js index 46357038a..57a224876 100644 --- a/dev/build/esbuild.js +++ b/dev/build/esbuild.js @@ -1,7 +1,6 @@ #!/usr/bin/env node const esbuild = require('esbuild'); -const fs = require('fs'); const path = require('path'); // Check if we're building for production @@ -9,20 +8,19 @@ const path = require('path'); const isProd = process.argv[2] === 'production'; // Gather our input files -const jsInDir = path.join(__dirname, '../../resources/js'); -const jsInDirFiles = fs.readdirSync(jsInDir, 'utf8'); -const entryFiles = jsInDirFiles - .filter(f => f.endsWith('.js') || f.endsWith('.mjs')) - .map(f => path.join(jsInDir, f)); +const entryPoints = { + app: path.join(__dirname, '../../resources/js/app.js'), + code: path.join(__dirname, '../../resources/js/code/index.mjs'), +}; // Locate our output directory -const outDir = path.join(__dirname, '../../public/dist'); +const outdir = path.join(__dirname, '../../public/dist'); // Build via esbuild esbuild.build({ bundle: true, - entryPoints: entryFiles, - outdir: outDir, + entryPoints, + outdir, sourcemap: true, target: 'es2020', mainFields: ['module', 'main'], diff --git a/package-lock.json b/package-lock.json index 1448d592f..c141e654a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,21 +5,131 @@ "packages": { "": { "dependencies": { + "@codemirror/commands": "^6.0.1", + "@codemirror/language": "^6.2.1", + "@codemirror/legacy-modes": "^6.1.0", + "@codemirror/state": "^6.1.0", + "@codemirror/view": "^6.1.2", "clipboard": "^2.0.11", - "codemirror": "^5.65.5", + "codemirror": "^6.0.1", "dropzone": "^5.9.3", "markdown-it": "^13.0.1", "markdown-it-task-lists": "^2.1.1", - "snabbdom": "^3.5.0", + "snabbdom": "^3.5.1", "sortablejs": "^1.15.0" }, "devDependencies": { "chokidar-cli": "^3.0", - "esbuild": "0.14.42", + "esbuild": "0.14.51", "livereload": "^0.9.3", "npm-run-all": "^4.1.5", "punycode": "^2.1.1", - "sass": "^1.52.1" + "sass": "^1.54.0" + } + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.1.0.tgz", + "integrity": "sha512-wtO4O5WDyXhhCd4q4utDIDZxnQfmJ++3dGBCG9LMtI79+92OcA1DVk/n7BEupKmjIr8AzvptDz7YQ9ud6OkU+A==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.0.1.tgz", + "integrity": "sha512-iNHDByicYqQjs0Wo1MKGfqNbMYMyhS9WV6EwMVwsHXImlFemgEUC+c5X22bXKBStN3qnwg4fArNZM+gkv22baQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.2.1.tgz", + "integrity": "sha512-MC3svxuvIj0MRpFlGHxLS6vPyIdbTr2KKPEW46kCoCXw2ktb4NTkpkPBI/lSP/FoNXLCBJ0mrnUi1OoZxtpW1Q==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.1.0.tgz", + "integrity": "sha512-V/PgGpndkZeTn3Hdlg/gd8MLFdyvTCIX+iwJzjUw5iNziWiNsAY8X0jvf7m3gSfxnKkNzmid6l0g4rYSpiDaCw==", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.0.0.tgz", + "integrity": "sha512-nUUXcJW1Xp54kNs+a1ToPLK8MadO0rMTnJB8Zk4Z8gBdrN0kqV7uvUraU/T2yqg+grDNR38Vmy/MrhQN/RgwiA==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.0.1.tgz", + "integrity": "sha512-uOinkOrM+daMduCgMPomDfKLr7drGHB4jHl3Vq6xY2WRlL7MkNsBE0b+XHYa/Mee2npsJOgwvkW4n1lMFeBW2Q==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.1.0.tgz", + "integrity": "sha512-qbUr94DZTe6/V1VS7LDLz11rM/1t/nJxR1El4I6UaxDEdc0aZZvq6JCLJWiRmUf95NRAnDH6fhXn+PWp9wGCIg==" + }, + "node_modules/@codemirror/view": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.1.2.tgz", + "integrity": "sha512-puUydfKwfmOo+ixtuB+uN/ZpcteEYSnpjHmMaow1sOQhNICsKtGBup3i9ybVqyzDagARRYzSHTWjbdeHqmn31w==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@lezer/common": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz", + "integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA==" + }, + "node_modules/@lezer/highlight": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.0.0.tgz", + "integrity": "sha512-nsCnNtim90UKsB5YxoX65v3GEIw3iCHw9RM2DtdgkiqAbKh9pCdvi8AWNwkYf10Lu6fxNhXPpkpHbW6mihhvJA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.0.tgz", + "integrity": "sha512-TgEpfm9br2SX8JwtwKT8HsQZKuFkLRg6g+IRxObk9nVKQLKnkP3oMh+QGcTBL9GQsfQ2ADtKPbj2iGSMf3ytiA==", + "dependencies": { + "@lezer/common": "^1.0.0" } }, "node_modules/ansi-regex": { @@ -195,9 +305,18 @@ } }, "node_modules/codemirror": { - "version": "5.65.5", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.5.tgz", - "integrity": "sha512-HNyhvGLnYz5c+kIsB9QKVitiZUevha3ovbIYaQiGzKo7ECSL/elWD9RXt3JgNr0NdnyqE9/Rc/7uLfkJQL638w==" + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } }, "node_modules/color-convert": { "version": "1.9.3", @@ -220,6 +339,11 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "node_modules/crelt": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz", + "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==" + }, "node_modules/cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -345,9 +469,9 @@ } }, "node_modules/esbuild": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.42.tgz", - "integrity": "sha512-V0uPZotCEHokJdNqyozH6qsaQXqmZEOiZWrXnds/zaH/0SyrIayRXWRB98CENO73MIZ9T3HBIOsmds5twWtmgw==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.51.tgz", + "integrity": "sha512-+CvnDitD7Q5sT7F+FM65sWkF8wJRf+j9fPcprxYV4j+ohmzVj2W7caUqH2s5kCaCJAfcAICjSlKhDCcvDpU7nw==", "dev": true, "hasInstallScript": true, "bin": { @@ -357,32 +481,32 @@ "node": ">=12" }, "optionalDependencies": { - "esbuild-android-64": "0.14.42", - "esbuild-android-arm64": "0.14.42", - "esbuild-darwin-64": "0.14.42", - "esbuild-darwin-arm64": "0.14.42", - "esbuild-freebsd-64": "0.14.42", - "esbuild-freebsd-arm64": "0.14.42", - "esbuild-linux-32": "0.14.42", - "esbuild-linux-64": "0.14.42", - "esbuild-linux-arm": "0.14.42", - "esbuild-linux-arm64": "0.14.42", - "esbuild-linux-mips64le": "0.14.42", - "esbuild-linux-ppc64le": "0.14.42", - "esbuild-linux-riscv64": "0.14.42", - "esbuild-linux-s390x": "0.14.42", - "esbuild-netbsd-64": "0.14.42", - "esbuild-openbsd-64": "0.14.42", - "esbuild-sunos-64": "0.14.42", - "esbuild-windows-32": "0.14.42", - "esbuild-windows-64": "0.14.42", - "esbuild-windows-arm64": "0.14.42" + "esbuild-android-64": "0.14.51", + "esbuild-android-arm64": "0.14.51", + "esbuild-darwin-64": "0.14.51", + "esbuild-darwin-arm64": "0.14.51", + "esbuild-freebsd-64": "0.14.51", + "esbuild-freebsd-arm64": "0.14.51", + "esbuild-linux-32": "0.14.51", + "esbuild-linux-64": "0.14.51", + "esbuild-linux-arm": "0.14.51", + "esbuild-linux-arm64": "0.14.51", + "esbuild-linux-mips64le": "0.14.51", + "esbuild-linux-ppc64le": "0.14.51", + "esbuild-linux-riscv64": "0.14.51", + "esbuild-linux-s390x": "0.14.51", + "esbuild-netbsd-64": "0.14.51", + "esbuild-openbsd-64": "0.14.51", + "esbuild-sunos-64": "0.14.51", + "esbuild-windows-32": "0.14.51", + "esbuild-windows-64": "0.14.51", + "esbuild-windows-arm64": "0.14.51" } }, "node_modules/esbuild-android-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.42.tgz", - "integrity": "sha512-P4Y36VUtRhK/zivqGVMqhptSrFILAGlYp0Z8r9UQqHJ3iWztRCNWnlBzD9HRx0DbueXikzOiwyOri+ojAFfW6A==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.51.tgz", + "integrity": "sha512-6FOuKTHnC86dtrKDmdSj2CkcKF8PnqkaIXqvgydqfJmqBazCPdw+relrMlhGjkvVdiiGV70rpdnyFmA65ekBCQ==", "cpu": [ "x64" ], @@ -396,9 +520,9 @@ } }, "node_modules/esbuild-android-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.42.tgz", - "integrity": "sha512-0cOqCubq+RWScPqvtQdjXG3Czb3AWI2CaKw3HeXry2eoA2rrPr85HF7IpdU26UWdBXgPYtlTN1LUiuXbboROhg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.51.tgz", + "integrity": "sha512-vBtp//5VVkZWmYYvHsqBRCMMi1MzKuMIn5XDScmnykMTu9+TD9v0NMEDqQxvtFToeYmojdo5UCV2vzMQWJcJ4A==", "cpu": [ "arm64" ], @@ -412,9 +536,9 @@ } }, "node_modules/esbuild-darwin-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.42.tgz", - "integrity": "sha512-ipiBdCA3ZjYgRfRLdQwP82rTiv/YVMtW36hTvAN5ZKAIfxBOyPXY7Cejp3bMXWgzKD8B6O+zoMzh01GZsCuEIA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.51.tgz", + "integrity": "sha512-YFmXPIOvuagDcwCejMRtCDjgPfnDu+bNeh5FU2Ryi68ADDVlWEpbtpAbrtf/lvFTWPexbgyKgzppNgsmLPr8PA==", "cpu": [ "x64" ], @@ -428,9 +552,9 @@ } }, "node_modules/esbuild-darwin-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.42.tgz", - "integrity": "sha512-bU2tHRqTPOaoH/4m0zYHbFWpiYDmaA0gt90/3BMEFaM0PqVK/a6MA2V/ypV5PO0v8QxN6gH5hBPY4YJ2lopXgA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.51.tgz", + "integrity": "sha512-juYD0QnSKwAMfzwKdIF6YbueXzS6N7y4GXPDeDkApz/1RzlT42mvX9jgNmyOlWKN7YzQAYbcUEJmZJYQGdf2ow==", "cpu": [ "arm64" ], @@ -444,9 +568,9 @@ } }, "node_modules/esbuild-freebsd-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.42.tgz", - "integrity": "sha512-75h1+22Ivy07+QvxHyhVqOdekupiTZVLN1PMwCDonAqyXd8TVNJfIRFrdL8QmSJrOJJ5h8H1I9ETyl2L8LQDaw==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.51.tgz", + "integrity": "sha512-cLEI/aXjb6vo5O2Y8rvVSQ7smgLldwYY5xMxqh/dQGfWO+R1NJOFsiax3IS4Ng300SVp7Gz3czxT6d6qf2cw0g==", "cpu": [ "x64" ], @@ -460,9 +584,9 @@ } }, "node_modules/esbuild-freebsd-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.42.tgz", - "integrity": "sha512-W6Jebeu5TTDQMJUJVarEzRU9LlKpNkPBbjqSu+GUPTHDCly5zZEQq9uHkmHHl7OKm+mQ2zFySN83nmfCeZCyNA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.51.tgz", + "integrity": "sha512-TcWVw/rCL2F+jUgRkgLa3qltd5gzKjIMGhkVybkjk6PJadYInPtgtUBp1/hG+mxyigaT7ib+od1Xb84b+L+1Mg==", "cpu": [ "arm64" ], @@ -476,9 +600,9 @@ } }, "node_modules/esbuild-linux-32": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.42.tgz", - "integrity": "sha512-Ooy/Bj+mJ1z4jlWcK5Dl6SlPlCgQB9zg1UrTCeY8XagvuWZ4qGPyYEWGkT94HUsRi2hKsXvcs6ThTOjBaJSMfg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.51.tgz", + "integrity": "sha512-RFqpyC5ChyWrjx8Xj2K0EC1aN0A37H6OJfmUXIASEqJoHcntuV3j2Efr9RNmUhMfNE6yEj2VpYuDteZLGDMr0w==", "cpu": [ "ia32" ], @@ -492,9 +616,9 @@ } }, "node_modules/esbuild-linux-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.42.tgz", - "integrity": "sha512-2L0HbzQfbTuemUWfVqNIjOfaTRt9zsvjnme6lnr7/MO9toz/MJ5tZhjqrG6uDWDxhsaHI2/nsDgrv8uEEN2eoA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.51.tgz", + "integrity": "sha512-dxjhrqo5i7Rq6DXwz5v+MEHVs9VNFItJmHBe1CxROWNf4miOGoQhqSG8StStbDkQ1Mtobg6ng+4fwByOhoQoeA==", "cpu": [ "x64" ], @@ -508,9 +632,9 @@ } }, "node_modules/esbuild-linux-arm": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.42.tgz", - "integrity": "sha512-STq69yzCMhdRaWnh29UYrLSr/qaWMm/KqwaRF1pMEK7kDiagaXhSL1zQGXbYv94GuGY/zAwzK98+6idCMUOOCg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.51.tgz", + "integrity": "sha512-LsJynDxYF6Neg7ZC7748yweCDD+N8ByCv22/7IAZglIEniEkqdF4HCaa49JNDLw1UQGlYuhOB8ZT/MmcSWzcWg==", "cpu": [ "arm" ], @@ -524,9 +648,9 @@ } }, "node_modules/esbuild-linux-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.42.tgz", - "integrity": "sha512-c3Ug3e9JpVr8jAcfbhirtpBauLxzYPpycjWulD71CF6ZSY26tvzmXMJYooQ2YKqDY4e/fPu5K8bm7MiXMnyxuA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.51.tgz", + "integrity": "sha512-D9rFxGutoqQX3xJPxqd6o+kvYKeIbM0ifW2y0bgKk5HPgQQOo2k9/2Vpto3ybGYaFPCE5qTGtqQta9PoP6ZEzw==", "cpu": [ "arm64" ], @@ -540,9 +664,9 @@ } }, "node_modules/esbuild-linux-mips64le": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.42.tgz", - "integrity": "sha512-QuvpHGbYlkyXWf2cGm51LBCHx6eUakjaSrRpUqhPwjh/uvNUYvLmz2LgPTTPwCqaKt0iwL+OGVL0tXA5aDbAbg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.51.tgz", + "integrity": "sha512-vS54wQjy4IinLSlb5EIlLoln8buh1yDgliP4CuEHumrPk4PvvP4kTRIG4SzMXm6t19N0rIfT4bNdAxzJLg2k6A==", "cpu": [ "mips64el" ], @@ -556,9 +680,9 @@ } }, "node_modules/esbuild-linux-ppc64le": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.42.tgz", - "integrity": "sha512-8ohIVIWDbDT+i7lCx44YCyIRrOW1MYlks9fxTo0ME2LS/fxxdoJBwHWzaDYhjvf8kNpA+MInZvyOEAGoVDrMHg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.51.tgz", + "integrity": "sha512-xcdd62Y3VfGoyphNP/aIV9LP+RzFw5M5Z7ja+zdpQHHvokJM7d0rlDRMN+iSSwvUymQkqZO+G/xjb4/75du8BQ==", "cpu": [ "ppc64" ], @@ -572,9 +696,9 @@ } }, "node_modules/esbuild-linux-riscv64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.42.tgz", - "integrity": "sha512-DzDqK3TuoXktPyG1Lwx7vhaF49Onv3eR61KwQyxYo4y5UKTpL3NmuarHSIaSVlTFDDpcIajCDwz5/uwKLLgKiQ==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.51.tgz", + "integrity": "sha512-syXHGak9wkAnFz0gMmRBoy44JV0rp4kVCEA36P5MCeZcxFq8+fllBC2t6sKI23w3qd8Vwo9pTADCgjTSf3L3rA==", "cpu": [ "riscv64" ], @@ -588,9 +712,9 @@ } }, "node_modules/esbuild-linux-s390x": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.42.tgz", - "integrity": "sha512-YFRhPCxl8nb//Wn6SiS5pmtplBi4z9yC2gLrYoYI/tvwuB1jldir9r7JwAGy1Ck4D7sE7wBN9GFtUUX/DLdcEQ==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.51.tgz", + "integrity": "sha512-kFAJY3dv+Wq8o28K/C7xkZk/X34rgTwhknSsElIqoEo8armCOjMJ6NsMxm48KaWY2h2RUYGtQmr+RGuUPKBhyw==", "cpu": [ "s390x" ], @@ -604,9 +728,9 @@ } }, "node_modules/esbuild-netbsd-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.42.tgz", - "integrity": "sha512-QYSD2k+oT9dqB/4eEM9c+7KyNYsIPgzYOSrmfNGDIyJrbT1d+CFVKvnKahDKNJLfOYj8N4MgyFaU9/Ytc6w5Vw==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.51.tgz", + "integrity": "sha512-ZZBI7qrR1FevdPBVHz/1GSk1x5GDL/iy42Zy8+neEm/HA7ma+hH/bwPEjeHXKWUDvM36CZpSL/fn1/y9/Hb+1A==", "cpu": [ "x64" ], @@ -620,9 +744,9 @@ } }, "node_modules/esbuild-openbsd-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.42.tgz", - "integrity": "sha512-M2meNVIKWsm2HMY7+TU9AxM7ZVwI9havdsw6m/6EzdXysyCFFSoaTQ/Jg03izjCsK17FsVRHqRe26Llj6x0MNA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.51.tgz", + "integrity": "sha512-7R1/p39M+LSVQVgDVlcY1KKm6kFKjERSX1lipMG51NPcspJD1tmiZSmmBXoY5jhHIu6JL1QkFDTx94gMYK6vfA==", "cpu": [ "x64" ], @@ -636,9 +760,9 @@ } }, "node_modules/esbuild-sunos-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.42.tgz", - "integrity": "sha512-uXV8TAZEw36DkgW8Ak3MpSJs1ofBb3Smkc/6pZ29sCAN1KzCAQzsje4sUwugf+FVicrHvlamCOlFZIXgct+iqQ==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.51.tgz", + "integrity": "sha512-HoHaCswHxLEYN8eBTtyO0bFEWvA3Kdb++hSQ/lLG7TyKF69TeSG0RNoBRAs45x/oCeWaTDntEZlYwAfQlhEtJA==", "cpu": [ "x64" ], @@ -652,9 +776,9 @@ } }, "node_modules/esbuild-windows-32": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.42.tgz", - "integrity": "sha512-4iw/8qWmRICWi9ZOnJJf9sYt6wmtp3hsN4TdI5NqgjfOkBVMxNdM9Vt3626G1Rda9ya2Q0hjQRD9W1o+m6Lz6g==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.51.tgz", + "integrity": "sha512-4rtwSAM35A07CBt1/X8RWieDj3ZUHQqUOaEo5ZBs69rt5WAFjP4aqCIobdqOy4FdhYw1yF8Z0xFBTyc9lgPtEg==", "cpu": [ "ia32" ], @@ -668,9 +792,9 @@ } }, "node_modules/esbuild-windows-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.42.tgz", - "integrity": "sha512-j3cdK+Y3+a5H0wHKmLGTJcq0+/2mMBHPWkItR3vytp/aUGD/ua/t2BLdfBIzbNN9nLCRL9sywCRpOpFMx3CxzA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.51.tgz", + "integrity": "sha512-HoN/5HGRXJpWODprGCgKbdMvrC3A2gqvzewu2eECRw2sYxOUoh2TV1tS+G7bHNapPGI79woQJGV6pFH7GH7qnA==", "cpu": [ "x64" ], @@ -684,9 +808,9 @@ } }, "node_modules/esbuild-windows-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.42.tgz", - "integrity": "sha512-+lRAARnF+hf8J0mN27ujO+VbhPbDqJ8rCcJKye4y7YZLV6C4n3pTRThAb388k/zqF5uM0lS5O201u0OqoWSicw==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.51.tgz", + "integrity": "sha512-JQDqPjuOH7o+BsKMSddMfmVJXrnYZxXDHsoLHc0xgmAZkOOCflRmC43q31pk79F9xuyWY45jDBPolb5ZgGOf9g==", "cpu": [ "arm64" ], @@ -1524,9 +1648,9 @@ } }, "node_modules/sass": { - "version": "1.52.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.52.1.tgz", - "integrity": "sha512-fSzYTbr7z8oQnVJ3Acp9hV80dM1fkMN7mSD/25mpcct9F7FPBMOI8krEYALgU1aZoqGhQNhTPsuSmxjnIvAm4Q==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.54.0.tgz", + "integrity": "sha512-C4zp79GCXZfK0yoHZg+GxF818/aclhp9F48XBu/+bm9vXEVAYov9iU3FBVRMq3Hx3OA4jfKL+p2K9180mEh0xQ==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -1602,9 +1726,9 @@ } }, "node_modules/snabbdom": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.0.tgz", - "integrity": "sha512-Ff5BKG18KrrPuskHJlA9aujPHqEabItaDl96l7ZZndF4zt5AYSczz7ZjjgQAX5IBd5cd25lw9NfgX21yVUJ+9g==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.1.tgz", + "integrity": "sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==", "engines": { "node": ">=8.3.0" } @@ -1733,6 +1857,11 @@ "node": ">=4" } }, + "node_modules/style-mod": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", + "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==" + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1792,6 +1921,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/w3c-keyname": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.5.tgz", + "integrity": "sha512-WJrK7i6w+ULuZsGscCezbCH4Aev5U3xY87vnSimzzEgPQhb0Sa0a1rE3c2jtEwrFtSfi61Jefw3jI5/DD/3jbQ==" + }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -1897,6 +2031,105 @@ } }, "dependencies": { + "@codemirror/autocomplete": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.1.0.tgz", + "integrity": "sha512-wtO4O5WDyXhhCd4q4utDIDZxnQfmJ++3dGBCG9LMtI79+92OcA1DVk/n7BEupKmjIr8AzvptDz7YQ9ud6OkU+A==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "@codemirror/commands": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.0.1.tgz", + "integrity": "sha512-iNHDByicYqQjs0Wo1MKGfqNbMYMyhS9WV6EwMVwsHXImlFemgEUC+c5X22bXKBStN3qnwg4fArNZM+gkv22baQ==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "@codemirror/language": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.2.1.tgz", + "integrity": "sha512-MC3svxuvIj0MRpFlGHxLS6vPyIdbTr2KKPEW46kCoCXw2ktb4NTkpkPBI/lSP/FoNXLCBJ0mrnUi1OoZxtpW1Q==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "@codemirror/legacy-modes": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.1.0.tgz", + "integrity": "sha512-V/PgGpndkZeTn3Hdlg/gd8MLFdyvTCIX+iwJzjUw5iNziWiNsAY8X0jvf7m3gSfxnKkNzmid6l0g4rYSpiDaCw==", + "requires": { + "@codemirror/language": "^6.0.0" + } + }, + "@codemirror/lint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.0.0.tgz", + "integrity": "sha512-nUUXcJW1Xp54kNs+a1ToPLK8MadO0rMTnJB8Zk4Z8gBdrN0kqV7uvUraU/T2yqg+grDNR38Vmy/MrhQN/RgwiA==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/search": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.0.1.tgz", + "integrity": "sha512-uOinkOrM+daMduCgMPomDfKLr7drGHB4jHl3Vq6xY2WRlL7MkNsBE0b+XHYa/Mee2npsJOgwvkW4n1lMFeBW2Q==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/state": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.1.0.tgz", + "integrity": "sha512-qbUr94DZTe6/V1VS7LDLz11rM/1t/nJxR1El4I6UaxDEdc0aZZvq6JCLJWiRmUf95NRAnDH6fhXn+PWp9wGCIg==" + }, + "@codemirror/view": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.1.2.tgz", + "integrity": "sha512-puUydfKwfmOo+ixtuB+uN/ZpcteEYSnpjHmMaow1sOQhNICsKtGBup3i9ybVqyzDagARRYzSHTWjbdeHqmn31w==", + "requires": { + "@codemirror/state": "^6.0.0", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, + "@lezer/common": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz", + "integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA==" + }, + "@lezer/highlight": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.0.0.tgz", + "integrity": "sha512-nsCnNtim90UKsB5YxoX65v3GEIw3iCHw9RM2DtdgkiqAbKh9pCdvi8AWNwkYf10Lu6fxNhXPpkpHbW6mihhvJA==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, + "@lezer/lr": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.0.tgz", + "integrity": "sha512-TgEpfm9br2SX8JwtwKT8HsQZKuFkLRg6g+IRxObk9nVKQLKnkP3oMh+QGcTBL9GQsfQ2ADtKPbj2iGSMf3ytiA==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, "ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", @@ -2035,9 +2268,18 @@ } }, "codemirror": { - "version": "5.65.5", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.5.tgz", - "integrity": "sha512-HNyhvGLnYz5c+kIsB9QKVitiZUevha3ovbIYaQiGzKo7ECSL/elWD9RXt3JgNr0NdnyqE9/Rc/7uLfkJQL638w==" + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } }, "color-convert": { "version": "1.9.3", @@ -2060,6 +2302,11 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "crelt": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz", + "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==" + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -2158,170 +2405,170 @@ } }, "esbuild": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.42.tgz", - "integrity": "sha512-V0uPZotCEHokJdNqyozH6qsaQXqmZEOiZWrXnds/zaH/0SyrIayRXWRB98CENO73MIZ9T3HBIOsmds5twWtmgw==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.51.tgz", + "integrity": "sha512-+CvnDitD7Q5sT7F+FM65sWkF8wJRf+j9fPcprxYV4j+ohmzVj2W7caUqH2s5kCaCJAfcAICjSlKhDCcvDpU7nw==", "dev": true, "requires": { - "esbuild-android-64": "0.14.42", - "esbuild-android-arm64": "0.14.42", - "esbuild-darwin-64": "0.14.42", - "esbuild-darwin-arm64": "0.14.42", - "esbuild-freebsd-64": "0.14.42", - "esbuild-freebsd-arm64": "0.14.42", - "esbuild-linux-32": "0.14.42", - "esbuild-linux-64": "0.14.42", - "esbuild-linux-arm": "0.14.42", - "esbuild-linux-arm64": "0.14.42", - "esbuild-linux-mips64le": "0.14.42", - "esbuild-linux-ppc64le": "0.14.42", - "esbuild-linux-riscv64": "0.14.42", - "esbuild-linux-s390x": "0.14.42", - "esbuild-netbsd-64": "0.14.42", - "esbuild-openbsd-64": "0.14.42", - "esbuild-sunos-64": "0.14.42", - "esbuild-windows-32": "0.14.42", - "esbuild-windows-64": "0.14.42", - "esbuild-windows-arm64": "0.14.42" + "esbuild-android-64": "0.14.51", + "esbuild-android-arm64": "0.14.51", + "esbuild-darwin-64": "0.14.51", + "esbuild-darwin-arm64": "0.14.51", + "esbuild-freebsd-64": "0.14.51", + "esbuild-freebsd-arm64": "0.14.51", + "esbuild-linux-32": "0.14.51", + "esbuild-linux-64": "0.14.51", + "esbuild-linux-arm": "0.14.51", + "esbuild-linux-arm64": "0.14.51", + "esbuild-linux-mips64le": "0.14.51", + "esbuild-linux-ppc64le": "0.14.51", + "esbuild-linux-riscv64": "0.14.51", + "esbuild-linux-s390x": "0.14.51", + "esbuild-netbsd-64": "0.14.51", + "esbuild-openbsd-64": "0.14.51", + "esbuild-sunos-64": "0.14.51", + "esbuild-windows-32": "0.14.51", + "esbuild-windows-64": "0.14.51", + "esbuild-windows-arm64": "0.14.51" } }, "esbuild-android-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.42.tgz", - "integrity": "sha512-P4Y36VUtRhK/zivqGVMqhptSrFILAGlYp0Z8r9UQqHJ3iWztRCNWnlBzD9HRx0DbueXikzOiwyOri+ojAFfW6A==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.51.tgz", + "integrity": "sha512-6FOuKTHnC86dtrKDmdSj2CkcKF8PnqkaIXqvgydqfJmqBazCPdw+relrMlhGjkvVdiiGV70rpdnyFmA65ekBCQ==", "dev": true, "optional": true }, "esbuild-android-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.42.tgz", - "integrity": "sha512-0cOqCubq+RWScPqvtQdjXG3Czb3AWI2CaKw3HeXry2eoA2rrPr85HF7IpdU26UWdBXgPYtlTN1LUiuXbboROhg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.51.tgz", + "integrity": "sha512-vBtp//5VVkZWmYYvHsqBRCMMi1MzKuMIn5XDScmnykMTu9+TD9v0NMEDqQxvtFToeYmojdo5UCV2vzMQWJcJ4A==", "dev": true, "optional": true }, "esbuild-darwin-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.42.tgz", - "integrity": "sha512-ipiBdCA3ZjYgRfRLdQwP82rTiv/YVMtW36hTvAN5ZKAIfxBOyPXY7Cejp3bMXWgzKD8B6O+zoMzh01GZsCuEIA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.51.tgz", + "integrity": "sha512-YFmXPIOvuagDcwCejMRtCDjgPfnDu+bNeh5FU2Ryi68ADDVlWEpbtpAbrtf/lvFTWPexbgyKgzppNgsmLPr8PA==", "dev": true, "optional": true }, "esbuild-darwin-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.42.tgz", - "integrity": "sha512-bU2tHRqTPOaoH/4m0zYHbFWpiYDmaA0gt90/3BMEFaM0PqVK/a6MA2V/ypV5PO0v8QxN6gH5hBPY4YJ2lopXgA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.51.tgz", + "integrity": "sha512-juYD0QnSKwAMfzwKdIF6YbueXzS6N7y4GXPDeDkApz/1RzlT42mvX9jgNmyOlWKN7YzQAYbcUEJmZJYQGdf2ow==", "dev": true, "optional": true }, "esbuild-freebsd-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.42.tgz", - "integrity": "sha512-75h1+22Ivy07+QvxHyhVqOdekupiTZVLN1PMwCDonAqyXd8TVNJfIRFrdL8QmSJrOJJ5h8H1I9ETyl2L8LQDaw==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.51.tgz", + "integrity": "sha512-cLEI/aXjb6vo5O2Y8rvVSQ7smgLldwYY5xMxqh/dQGfWO+R1NJOFsiax3IS4Ng300SVp7Gz3czxT6d6qf2cw0g==", "dev": true, "optional": true }, "esbuild-freebsd-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.42.tgz", - "integrity": "sha512-W6Jebeu5TTDQMJUJVarEzRU9LlKpNkPBbjqSu+GUPTHDCly5zZEQq9uHkmHHl7OKm+mQ2zFySN83nmfCeZCyNA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.51.tgz", + "integrity": "sha512-TcWVw/rCL2F+jUgRkgLa3qltd5gzKjIMGhkVybkjk6PJadYInPtgtUBp1/hG+mxyigaT7ib+od1Xb84b+L+1Mg==", "dev": true, "optional": true }, "esbuild-linux-32": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.42.tgz", - "integrity": "sha512-Ooy/Bj+mJ1z4jlWcK5Dl6SlPlCgQB9zg1UrTCeY8XagvuWZ4qGPyYEWGkT94HUsRi2hKsXvcs6ThTOjBaJSMfg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.51.tgz", + "integrity": "sha512-RFqpyC5ChyWrjx8Xj2K0EC1aN0A37H6OJfmUXIASEqJoHcntuV3j2Efr9RNmUhMfNE6yEj2VpYuDteZLGDMr0w==", "dev": true, "optional": true }, "esbuild-linux-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.42.tgz", - "integrity": "sha512-2L0HbzQfbTuemUWfVqNIjOfaTRt9zsvjnme6lnr7/MO9toz/MJ5tZhjqrG6uDWDxhsaHI2/nsDgrv8uEEN2eoA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.51.tgz", + "integrity": "sha512-dxjhrqo5i7Rq6DXwz5v+MEHVs9VNFItJmHBe1CxROWNf4miOGoQhqSG8StStbDkQ1Mtobg6ng+4fwByOhoQoeA==", "dev": true, "optional": true }, "esbuild-linux-arm": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.42.tgz", - "integrity": "sha512-STq69yzCMhdRaWnh29UYrLSr/qaWMm/KqwaRF1pMEK7kDiagaXhSL1zQGXbYv94GuGY/zAwzK98+6idCMUOOCg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.51.tgz", + "integrity": "sha512-LsJynDxYF6Neg7ZC7748yweCDD+N8ByCv22/7IAZglIEniEkqdF4HCaa49JNDLw1UQGlYuhOB8ZT/MmcSWzcWg==", "dev": true, "optional": true }, "esbuild-linux-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.42.tgz", - "integrity": "sha512-c3Ug3e9JpVr8jAcfbhirtpBauLxzYPpycjWulD71CF6ZSY26tvzmXMJYooQ2YKqDY4e/fPu5K8bm7MiXMnyxuA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.51.tgz", + "integrity": "sha512-D9rFxGutoqQX3xJPxqd6o+kvYKeIbM0ifW2y0bgKk5HPgQQOo2k9/2Vpto3ybGYaFPCE5qTGtqQta9PoP6ZEzw==", "dev": true, "optional": true }, "esbuild-linux-mips64le": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.42.tgz", - "integrity": "sha512-QuvpHGbYlkyXWf2cGm51LBCHx6eUakjaSrRpUqhPwjh/uvNUYvLmz2LgPTTPwCqaKt0iwL+OGVL0tXA5aDbAbg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.51.tgz", + "integrity": "sha512-vS54wQjy4IinLSlb5EIlLoln8buh1yDgliP4CuEHumrPk4PvvP4kTRIG4SzMXm6t19N0rIfT4bNdAxzJLg2k6A==", "dev": true, "optional": true }, "esbuild-linux-ppc64le": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.42.tgz", - "integrity": "sha512-8ohIVIWDbDT+i7lCx44YCyIRrOW1MYlks9fxTo0ME2LS/fxxdoJBwHWzaDYhjvf8kNpA+MInZvyOEAGoVDrMHg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.51.tgz", + "integrity": "sha512-xcdd62Y3VfGoyphNP/aIV9LP+RzFw5M5Z7ja+zdpQHHvokJM7d0rlDRMN+iSSwvUymQkqZO+G/xjb4/75du8BQ==", "dev": true, "optional": true }, "esbuild-linux-riscv64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.42.tgz", - "integrity": "sha512-DzDqK3TuoXktPyG1Lwx7vhaF49Onv3eR61KwQyxYo4y5UKTpL3NmuarHSIaSVlTFDDpcIajCDwz5/uwKLLgKiQ==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.51.tgz", + "integrity": "sha512-syXHGak9wkAnFz0gMmRBoy44JV0rp4kVCEA36P5MCeZcxFq8+fllBC2t6sKI23w3qd8Vwo9pTADCgjTSf3L3rA==", "dev": true, "optional": true }, "esbuild-linux-s390x": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.42.tgz", - "integrity": "sha512-YFRhPCxl8nb//Wn6SiS5pmtplBi4z9yC2gLrYoYI/tvwuB1jldir9r7JwAGy1Ck4D7sE7wBN9GFtUUX/DLdcEQ==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.51.tgz", + "integrity": "sha512-kFAJY3dv+Wq8o28K/C7xkZk/X34rgTwhknSsElIqoEo8armCOjMJ6NsMxm48KaWY2h2RUYGtQmr+RGuUPKBhyw==", "dev": true, "optional": true }, "esbuild-netbsd-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.42.tgz", - "integrity": "sha512-QYSD2k+oT9dqB/4eEM9c+7KyNYsIPgzYOSrmfNGDIyJrbT1d+CFVKvnKahDKNJLfOYj8N4MgyFaU9/Ytc6w5Vw==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.51.tgz", + "integrity": "sha512-ZZBI7qrR1FevdPBVHz/1GSk1x5GDL/iy42Zy8+neEm/HA7ma+hH/bwPEjeHXKWUDvM36CZpSL/fn1/y9/Hb+1A==", "dev": true, "optional": true }, "esbuild-openbsd-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.42.tgz", - "integrity": "sha512-M2meNVIKWsm2HMY7+TU9AxM7ZVwI9havdsw6m/6EzdXysyCFFSoaTQ/Jg03izjCsK17FsVRHqRe26Llj6x0MNA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.51.tgz", + "integrity": "sha512-7R1/p39M+LSVQVgDVlcY1KKm6kFKjERSX1lipMG51NPcspJD1tmiZSmmBXoY5jhHIu6JL1QkFDTx94gMYK6vfA==", "dev": true, "optional": true }, "esbuild-sunos-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.42.tgz", - "integrity": "sha512-uXV8TAZEw36DkgW8Ak3MpSJs1ofBb3Smkc/6pZ29sCAN1KzCAQzsje4sUwugf+FVicrHvlamCOlFZIXgct+iqQ==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.51.tgz", + "integrity": "sha512-HoHaCswHxLEYN8eBTtyO0bFEWvA3Kdb++hSQ/lLG7TyKF69TeSG0RNoBRAs45x/oCeWaTDntEZlYwAfQlhEtJA==", "dev": true, "optional": true }, "esbuild-windows-32": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.42.tgz", - "integrity": "sha512-4iw/8qWmRICWi9ZOnJJf9sYt6wmtp3hsN4TdI5NqgjfOkBVMxNdM9Vt3626G1Rda9ya2Q0hjQRD9W1o+m6Lz6g==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.51.tgz", + "integrity": "sha512-4rtwSAM35A07CBt1/X8RWieDj3ZUHQqUOaEo5ZBs69rt5WAFjP4aqCIobdqOy4FdhYw1yF8Z0xFBTyc9lgPtEg==", "dev": true, "optional": true }, "esbuild-windows-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.42.tgz", - "integrity": "sha512-j3cdK+Y3+a5H0wHKmLGTJcq0+/2mMBHPWkItR3vytp/aUGD/ua/t2BLdfBIzbNN9nLCRL9sywCRpOpFMx3CxzA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.51.tgz", + "integrity": "sha512-HoN/5HGRXJpWODprGCgKbdMvrC3A2gqvzewu2eECRw2sYxOUoh2TV1tS+G7bHNapPGI79woQJGV6pFH7GH7qnA==", "dev": true, "optional": true }, "esbuild-windows-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.42.tgz", - "integrity": "sha512-+lRAARnF+hf8J0mN27ujO+VbhPbDqJ8rCcJKye4y7YZLV6C4n3pTRThAb388k/zqF5uM0lS5O201u0OqoWSicw==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.51.tgz", + "integrity": "sha512-JQDqPjuOH7o+BsKMSddMfmVJXrnYZxXDHsoLHc0xgmAZkOOCflRmC43q31pk79F9xuyWY45jDBPolb5ZgGOf9g==", "dev": true, "optional": true }, @@ -2922,9 +3169,9 @@ } }, "sass": { - "version": "1.52.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.52.1.tgz", - "integrity": "sha512-fSzYTbr7z8oQnVJ3Acp9hV80dM1fkMN7mSD/25mpcct9F7FPBMOI8krEYALgU1aZoqGhQNhTPsuSmxjnIvAm4Q==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.54.0.tgz", + "integrity": "sha512-C4zp79GCXZfK0yoHZg+GxF818/aclhp9F48XBu/+bm9vXEVAYov9iU3FBVRMq3Hx3OA4jfKL+p2K9180mEh0xQ==", "dev": true, "requires": { "chokidar": ">=3.0.0 <4.0.0", @@ -2982,9 +3229,9 @@ } }, "snabbdom": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.0.tgz", - "integrity": "sha512-Ff5BKG18KrrPuskHJlA9aujPHqEabItaDl96l7ZZndF4zt5AYSczz7ZjjgQAX5IBd5cd25lw9NfgX21yVUJ+9g==" + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.1.tgz", + "integrity": "sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==" }, "sortablejs": { "version": "1.15.0", @@ -3086,6 +3333,11 @@ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", "dev": true }, + "style-mod": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", + "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==" + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -3136,6 +3388,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "w3c-keyname": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.5.tgz", + "integrity": "sha512-WJrK7i6w+ULuZsGscCezbCH4Aev5U3xY87vnSimzzEgPQhb0Sa0a1rE3c2jtEwrFtSfi61Jefw3jI5/DD/3jbQ==" + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/package.json b/package.json index 9a2f66448..8407ffc77 100644 --- a/package.json +++ b/package.json @@ -16,19 +16,24 @@ }, "devDependencies": { "chokidar-cli": "^3.0", - "esbuild": "0.14.42", + "esbuild": "0.14.51", "livereload": "^0.9.3", "npm-run-all": "^4.1.5", "punycode": "^2.1.1", - "sass": "^1.52.1" + "sass": "^1.54.0" }, "dependencies": { + "@codemirror/commands": "^6.0.1", + "@codemirror/language": "^6.2.1", + "@codemirror/legacy-modes": "^6.1.0", + "@codemirror/state": "^6.1.0", + "@codemirror/view": "^6.1.2", "clipboard": "^2.0.11", - "codemirror": "^5.65.5", + "codemirror": "^6.0.1", "dropzone": "^5.9.3", "markdown-it": "^13.0.1", "markdown-it-task-lists": "^2.1.1", - "snabbdom": "^3.5.0", + "snabbdom": "^3.5.1", "sortablejs": "^1.15.0" } } diff --git a/resources/js/code.mjs b/resources/js/code/index.mjs similarity index 55% rename from resources/js/code.mjs rename to resources/js/code/index.mjs index eca941f1c..ff60cbff5 100644 --- a/resources/js/code.mjs +++ b/resources/js/code/index.mjs @@ -1,106 +1,10 @@ -import CodeMirror from "codemirror"; +import {EditorView} from "@codemirror/view" +// import {EditorState} from "@codemirror/state" import Clipboard from "clipboard/dist/clipboard.min"; // Modes -import 'codemirror/mode/css/css'; -import 'codemirror/mode/clike/clike'; -import 'codemirror/mode/diff/diff'; -import 'codemirror/mode/fortran/fortran'; -import 'codemirror/mode/go/go'; -import 'codemirror/mode/haskell/haskell'; -import 'codemirror/mode/htmlmixed/htmlmixed'; -import 'codemirror/mode/javascript/javascript'; -import 'codemirror/mode/julia/julia'; -import 'codemirror/mode/lua/lua'; -import 'codemirror/mode/markdown/markdown'; -import 'codemirror/mode/mllike/mllike'; -import 'codemirror/mode/nginx/nginx'; -import 'codemirror/mode/perl/perl'; -import 'codemirror/mode/pascal/pascal'; -import 'codemirror/mode/php/php'; -import 'codemirror/mode/powershell/powershell'; -import 'codemirror/mode/properties/properties'; -import 'codemirror/mode/python/python'; -import 'codemirror/mode/ruby/ruby'; -import 'codemirror/mode/rust/rust'; -import 'codemirror/mode/shell/shell'; -import 'codemirror/mode/sql/sql'; -import 'codemirror/mode/stex/stex'; -import 'codemirror/mode/toml/toml'; -import 'codemirror/mode/vb/vb'; -import 'codemirror/mode/vbscript/vbscript'; -import 'codemirror/mode/xml/xml'; -import 'codemirror/mode/yaml/yaml'; - -// Addons -import 'codemirror/addon/scroll/scrollpastend'; - -// Mapping of possible languages or formats from user input to their codemirror modes. -// Value can be a mode string or a function that will receive the code content & return the mode string. -// The function option is used in the event the exact mode could be dynamic depending on the code. -const modeMap = { - bash: 'shell', - css: 'css', - c: 'text/x-csrc', - java: 'text/x-java', - scala: 'text/x-scala', - kotlin: 'text/x-kotlin', - 'c++': 'text/x-c++src', - 'c#': 'text/x-csharp', - csharp: 'text/x-csharp', - diff: 'diff', - for: 'fortran', - fortran: 'fortran', - 'f#': 'text/x-fsharp', - fsharp: 'text/x-fsharp', - go: 'go', - haskell: 'haskell', - hs: 'haskell', - html: 'htmlmixed', - ini: 'properties', - javascript: 'text/javascript', - json: 'application/json', - js: 'text/javascript', - jl: 'text/x-julia', - julia: 'text/x-julia', - latex: 'text/x-stex', - lua: 'lua', - md: 'markdown', - mdown: 'markdown', - markdown: 'markdown', - ml: 'mllike', - nginx: 'nginx', - perl: 'perl', - pl: 'perl', - powershell: 'powershell', - properties: 'properties', - ocaml: 'text/x-ocaml', - pascal: 'text/x-pascal', - pas: 'text/x-pascal', - php: (content) => { - return content.includes('`; - const copyButton = document.createElement('div'); - copyButton.classList.add('CodeMirror-copy'); - copyButton.innerHTML = copyIcon; - cmInstance.display.wrapper.appendChild(copyButton); - - const clipboard = new Clipboard(copyButton, { - text: function(trigger) { - return cmInstance.getValue() - } - }); - - clipboard.on('success', event => { - copyButton.classList.add('success'); - setTimeout(() => { - copyButton.classList.remove('success'); - }, 240); - }); + // TODO + // const copyIcon = ``; + // const copyButton = document.createElement('div'); + // copyButton.classList.add('CodeMirror-copy'); + // copyButton.innerHTML = copyIcon; + // cmInstance.display.wrapper.appendChild(copyButton); + // + // const clipboard = new Clipboard(copyButton, { + // text: function(trigger) { + // return cmInstance.getValue() + // } + // }); + // + // clipboard.on('success', event => { + // copyButton.classList.add('success'); + // setTimeout(() => { + // copyButton.classList.remove('success'); + // }, 240); + // }); } /** diff --git a/resources/js/code/modes.js b/resources/js/code/modes.js new file mode 100644 index 000000000..5a89255a5 --- /dev/null +++ b/resources/js/code/modes.js @@ -0,0 +1,134 @@ +import {StreamLanguage} from "@codemirror/language" + +import {css as langCss} from '@codemirror/legacy-modes/mode/css'; +import {clike as langClike} from '@codemirror/legacy-modes/mode/clike'; +import {diff as langDiff} from '@codemirror/legacy-modes/mode/diff'; +import {fortran as langFortran} from '@codemirror/legacy-modes/mode/fortran'; +import {go as langGo} from '@codemirror/legacy-modes/mode/go'; +import {haskell as langHaskell} from '@codemirror/legacy-modes/mode/haskell'; +// import {htmlmixed as langHtmlmixed} from '@codemirror/legacy-modes/mode/htmlmixed'; +import {javascript as langJavascript} from '@codemirror/legacy-modes/mode/javascript'; +import {julia as langJulia} from '@codemirror/legacy-modes/mode/julia'; +import {lua as langLua} from '@codemirror/legacy-modes/mode/lua'; +// import {markdown as langMarkdown} from '@codemirror/legacy-modes/mode/markdown'; +import {oCaml as langMllike} from '@codemirror/legacy-modes/mode/mllike'; +import {nginx as langNginx} from '@codemirror/legacy-modes/mode/nginx'; +import {perl as langPerl} from '@codemirror/legacy-modes/mode/perl'; +import {pascal as langPascal} from '@codemirror/legacy-modes/mode/pascal'; +// import {php as langPhp} from '@codemirror/legacy-modes/mode/php'; +import {powerShell as langPowershell} from '@codemirror/legacy-modes/mode/powershell'; +import {properties as langProperties} from '@codemirror/legacy-modes/mode/properties'; +import {python as langPython} from '@codemirror/legacy-modes/mode/python'; +import {ruby as langRuby} from '@codemirror/legacy-modes/mode/ruby'; +import {rust as langRust} from '@codemirror/legacy-modes/mode/rust'; +import {shell as langShell} from '@codemirror/legacy-modes/mode/shell'; +import {sql as langSql} from '@codemirror/legacy-modes/mode/sql'; +import {stex as langStex} from '@codemirror/legacy-modes/mode/stex'; +import {toml as langToml} from '@codemirror/legacy-modes/mode/toml'; +import {vb as langVb} from '@codemirror/legacy-modes/mode/vb'; +import {vbScript as langVbscript} from '@codemirror/legacy-modes/mode/vbscript'; +import {xml as langXml} from '@codemirror/legacy-modes/mode/xml'; +import {yaml as langYaml} from '@codemirror/legacy-modes/mode/yaml'; + +export const modes = [ + langCss, + langClike, + langDiff, + langFortran, + langGo, + langHaskell, + // langHtmlmixed, + langJavascript, + langJulia, + langLua, + // langMarkdown, + langMllike, + langNginx, + langPerl, + langPascal, + // langPhp, + langPowershell, + langProperties, + langPython, + langRuby, + langRust, + langShell, + langSql, + langStex, + langToml, + langVb, + langVbscript, + langXml, + langYaml, +]; + +// Mapping of possible languages or formats from user input to their codemirror modes. +// Value can be a mode string or a function that will receive the code content & return the mode string. +// The function option is used in the event the exact mode could be dynamic depending on the code. +export const modeMap = { + bash: 'shell', + css: 'css', + c: 'text/x-csrc', + java: 'text/x-java', + scala: 'text/x-scala', + kotlin: 'text/x-kotlin', + 'c++': 'text/x-c++src', + 'c#': 'text/x-csharp', + csharp: 'text/x-csharp', + diff: 'diff', + for: 'fortran', + fortran: 'fortran', + 'f#': 'text/x-fsharp', + fsharp: 'text/x-fsharp', + go: 'go', + haskell: 'haskell', + hs: 'haskell', + html: 'htmlmixed', + ini: 'properties', + javascript: 'text/javascript', + json: 'application/json', + js: 'text/javascript', + jl: 'text/x-julia', + julia: 'text/x-julia', + latex: 'text/x-stex', + lua: 'lua', + md: 'markdown', + mdown: 'markdown', + markdown: 'markdown', + ml: 'mllike', + nginx: 'nginx', + perl: 'perl', + pl: 'perl', + powershell: 'powershell', + properties: 'properties', + ocaml: 'text/x-ocaml', + pascal: 'text/x-pascal', + pas: 'text/x-pascal', + php: (content) => { + return content.includes(' StreamLanguage.define(mode)); +} \ No newline at end of file diff --git a/resources/js/code/setups.js b/resources/js/code/setups.js new file mode 100644 index 000000000..768d3a35d --- /dev/null +++ b/resources/js/code/setups.js @@ -0,0 +1,32 @@ + +import {keymap, highlightSpecialChars, drawSelection, highlightActiveLine, dropCursor, + rectangularSelection, lineNumbers, highlightActiveLineGutter} from "@codemirror/view" +import {defaultHighlightStyle, syntaxHighlighting, bracketMatching, + foldKeymap} from "@codemirror/language" +import {defaultKeymap, history, historyKeymap} from "@codemirror/commands" +import {EditorState} from "@codemirror/state" + +import {modesAsStreamLanguages} from "./modes"; + + +export function viewer() { + return [ + lineNumbers(), + highlightActiveLineGutter(), + highlightSpecialChars(), + history(), + drawSelection(), + dropCursor(), + syntaxHighlighting(defaultHighlightStyle, {fallback: true}), + bracketMatching(), + rectangularSelection(), + highlightActiveLine(), + keymap.of([ + ...defaultKeymap, + ...historyKeymap, + ...foldKeymap, + ]), + EditorState.readOnly.of(true), + ...modesAsStreamLanguages(), + ]; +} \ No newline at end of file From 97146a63590fa193a4958c09202b40340e840a1f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 3 Aug 2022 19:40:16 +0100 Subject: [PATCH 02/76] Added handling of codemirror 6 code languages --- package-lock.json | 261 +++++++++++++++++++++++++++++++++ package.json | 5 + resources/js/code/index.mjs | 49 ++----- resources/js/code/languages.js | 120 +++++++++++++++ resources/js/code/modes.js | 134 ----------------- resources/js/code/setups.js | 4 - resources/js/code/views.js | 38 +++++ 7 files changed, 437 insertions(+), 174 deletions(-) create mode 100644 resources/js/code/languages.js delete mode 100644 resources/js/code/modes.js create mode 100644 resources/js/code/views.js diff --git a/package-lock.json b/package-lock.json index c141e654a..0eca777cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,11 @@ "": { "dependencies": { "@codemirror/commands": "^6.0.1", + "@codemirror/lang-html": "^6.1.0", + "@codemirror/lang-javascript": "^6.0.2", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-markdown": "^6.0.1", + "@codemirror/lang-php": "^6.0.0", "@codemirror/language": "^6.2.1", "@codemirror/legacy-modes": "^6.1.0", "@codemirror/state": "^6.1.0", @@ -55,6 +60,79 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@codemirror/lang-css": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.0.0.tgz", + "integrity": "sha512-jBqc+BTuwhNOTlrimFghLlSrN6iFuE44HULKWoR4qKYObhOIl9Lci1iYj6zMIte1XTQmZguNvjXMyr43LUKwSw==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/css": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.1.0.tgz", + "integrity": "sha512-gA7NmJxqvnhwza05CvR7W/39Ap9r/4Vs9uiC0IeFYo1hSlJzc/8N6Evviz6vTW1x8SpHcRYyqKOf6rpl6LfWtg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/html": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.0.2.tgz", + "integrity": "sha512-BZRJ9u/zl16hLkSpDAWm73mrfIR7HJrr0lvnhoSOCQVea5BglguWI/slxexhvUb0CB5cXgKWuo2bM+N9EhIaZw==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.0.tgz", + "integrity": "sha512-DvTcYTKLmg2viADXlTdufrT334M9jowe1qO02W28nvm+nejcvhM5vot5mE8/kPrxYw/HJHhwu1z2PyBpnMLCNQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.0.1.tgz", + "integrity": "sha512-pHPQuRwf9cUrmkmsTHRjtS9ZnGu3fA9YzAdh2++d+L9wbfnC2XbKh0Xvm/0YiUjdCnoCx9wDFEoCuAnkqKWLIw==", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-php": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.0.tgz", + "integrity": "sha512-96CEjq0xEgbzc6bdFPwILPfZ6m8917JRbh2oPszZJABlYxG4Y+eYjtYkUTDb4yuyjQKyigHoeGC6zoIOYA1NWA==", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/php": "^1.0.0" + } + }, "node_modules/@codemirror/language": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.2.1.tgz", @@ -116,6 +194,15 @@ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz", "integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA==" }, + "node_modules/@lezer/css": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.0.0.tgz", + "integrity": "sha512-616VqgDKumHmYIuxs3tnX1irEQmoDHgF/TlP4O5ICWwyHwLMErq+8iKVuzTkOdBqvYAVmObqThcDEAaaMJjAdg==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@lezer/highlight": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.0.0.tgz", @@ -124,6 +211,34 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@lezer/html": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.0.1.tgz", + "integrity": "sha512-sC00zEt3GBh3vVO6QaGX4YZCl41S9dHWN/WGBsDixy9G+sqOC7gsa4cxA/fmRVAiBvhqYkJk+5Ul4oul92CPVw==", + "dependencies": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.0.2.tgz", + "integrity": "sha512-IjOVeIRhM8IuafWNnk+UzRz7p4/JSOKBNINLYLsdSGuJS9Ju7vFdc82AlTt0jgtV5D8eBZf4g0vK4d3ttBNz7A==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.0.tgz", + "integrity": "sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@lezer/lr": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.0.tgz", @@ -132,6 +247,24 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@lezer/markdown": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.0.1.tgz", + "integrity": "sha512-LlpNWLqes3XQvd8TwpJTHf9ENl4fI6H32xQkMgltUITFMMdQpOASXQtDawWR03yS6hskh4bkhATQbgjdGMoUvA==", + "dependencies": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@lezer/php": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.0.tgz", + "integrity": "sha512-kFQu/mk/vmjpA+fjQU87d9eimqKJ9PFCa8CZCPFWGEwNnm7Ahpw32N+HYEU/YAQ0XcfmOAnW/YJCEa8WpUOMMw==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", @@ -2053,6 +2186,79 @@ "@lezer/common": "^1.0.0" } }, + "@codemirror/lang-css": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.0.0.tgz", + "integrity": "sha512-jBqc+BTuwhNOTlrimFghLlSrN6iFuE44HULKWoR4qKYObhOIl9Lci1iYj6zMIte1XTQmZguNvjXMyr43LUKwSw==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/css": "^1.0.0" + } + }, + "@codemirror/lang-html": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.1.0.tgz", + "integrity": "sha512-gA7NmJxqvnhwza05CvR7W/39Ap9r/4Vs9uiC0IeFYo1hSlJzc/8N6Evviz6vTW1x8SpHcRYyqKOf6rpl6LfWtg==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/html": "^1.0.0" + } + }, + "@codemirror/lang-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.0.2.tgz", + "integrity": "sha512-BZRJ9u/zl16hLkSpDAWm73mrfIR7HJrr0lvnhoSOCQVea5BglguWI/slxexhvUb0CB5cXgKWuo2bM+N9EhIaZw==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "@codemirror/lang-json": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.0.tgz", + "integrity": "sha512-DvTcYTKLmg2viADXlTdufrT334M9jowe1qO02W28nvm+nejcvhM5vot5mE8/kPrxYw/HJHhwu1z2PyBpnMLCNQ==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "@codemirror/lang-markdown": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.0.1.tgz", + "integrity": "sha512-pHPQuRwf9cUrmkmsTHRjtS9ZnGu3fA9YzAdh2++d+L9wbfnC2XbKh0Xvm/0YiUjdCnoCx9wDFEoCuAnkqKWLIw==", + "requires": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/markdown": "^1.0.0" + } + }, + "@codemirror/lang-php": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.0.tgz", + "integrity": "sha512-96CEjq0xEgbzc6bdFPwILPfZ6m8917JRbh2oPszZJABlYxG4Y+eYjtYkUTDb4yuyjQKyigHoeGC6zoIOYA1NWA==", + "requires": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/php": "^1.0.0" + } + }, "@codemirror/language": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.2.1.tgz", @@ -2114,6 +2320,15 @@ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz", "integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA==" }, + "@lezer/css": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.0.0.tgz", + "integrity": "sha512-616VqgDKumHmYIuxs3tnX1irEQmoDHgF/TlP4O5ICWwyHwLMErq+8iKVuzTkOdBqvYAVmObqThcDEAaaMJjAdg==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "@lezer/highlight": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.0.0.tgz", @@ -2122,6 +2337,34 @@ "@lezer/common": "^1.0.0" } }, + "@lezer/html": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.0.1.tgz", + "integrity": "sha512-sC00zEt3GBh3vVO6QaGX4YZCl41S9dHWN/WGBsDixy9G+sqOC7gsa4cxA/fmRVAiBvhqYkJk+5Ul4oul92CPVw==", + "requires": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/javascript": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.0.2.tgz", + "integrity": "sha512-IjOVeIRhM8IuafWNnk+UzRz7p4/JSOKBNINLYLsdSGuJS9Ju7vFdc82AlTt0jgtV5D8eBZf4g0vK4d3ttBNz7A==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.0.tgz", + "integrity": "sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "@lezer/lr": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.0.tgz", @@ -2130,6 +2373,24 @@ "@lezer/common": "^1.0.0" } }, + "@lezer/markdown": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.0.1.tgz", + "integrity": "sha512-LlpNWLqes3XQvd8TwpJTHf9ENl4fI6H32xQkMgltUITFMMdQpOASXQtDawWR03yS6hskh4bkhATQbgjdGMoUvA==", + "requires": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "@lezer/php": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.0.tgz", + "integrity": "sha512-kFQu/mk/vmjpA+fjQU87d9eimqKJ9PFCa8CZCPFWGEwNnm7Ahpw32N+HYEU/YAQ0XcfmOAnW/YJCEa8WpUOMMw==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", diff --git a/package.json b/package.json index 8407ffc77..4d2b70247 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,11 @@ }, "dependencies": { "@codemirror/commands": "^6.0.1", + "@codemirror/lang-html": "^6.1.0", + "@codemirror/lang-javascript": "^6.0.2", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-markdown": "^6.0.1", + "@codemirror/lang-php": "^6.0.0", "@codemirror/language": "^6.2.1", "@codemirror/legacy-modes": "^6.1.0", "@codemirror/state": "^6.1.0", diff --git a/resources/js/code/index.mjs b/resources/js/code/index.mjs index ff60cbff5..6ef659994 100644 --- a/resources/js/code/index.mjs +++ b/resources/js/code/index.mjs @@ -1,10 +1,9 @@ import {EditorView} from "@codemirror/view" -// import {EditorState} from "@codemirror/state" import Clipboard from "clipboard/dist/clipboard.min"; // Modes -import {modes, modeMap, modesAsStreamLanguages} from "./modes"; import {viewer} from "./setups.js"; +import {createView, updateViewLanguage} from "./views.js"; /** * Highlight pre elements on a page @@ -36,26 +35,24 @@ function highlightElem(elem) { elem.innerHTML = elem.innerHTML.replace(//gi ,'\n'); const content = elem.textContent.trimEnd(); - let mode = ''; + let langName = ''; if (innerCodeElem !== null) { - const langName = innerCodeElem.className.replace('language-', ''); - mode = getMode(langName, content); + langName = innerCodeElem.className.replace('language-', ''); } const wrapper = document.createElement('div'); elem.parentNode.insertBefore(wrapper, elem); - const cm = new EditorView({ + const ev = createView({ parent: wrapper, doc: content, extensions: viewer(), }); + setMode(ev, langName, content); elem.remove(); - // TODO - theme: getTheme(), - // TODO - mode, - addCopyIcon(cm); + addCopyIcon(ev); } /** @@ -84,28 +81,6 @@ function addCopyIcon(cmInstance) { // }); } -/** - * Search for a codemirror code based off a user suggestion - * @param {String} suggestion - * @param {String} content - * @returns {string} - */ -function getMode(suggestion, content) { - suggestion = suggestion.trim().replace(/^\./g, '').toLowerCase(); - - const modeMapType = typeof modeMap[suggestion]; - - if (modeMapType === 'undefined') { - return ''; - } - - if (modeMapType === 'function') { - return modeMap[suggestion](content); - } - - return modeMap[suggestion]; -} - /** * Ge the theme to use for CodeMirror instances. * @returns {*|string} @@ -172,12 +147,14 @@ export function inlineEditor(textArea, mode) { } /** - * Set the mode of a codemirror instance. - * @param cmInstance - * @param modeSuggestion + * Set the language mode of a codemirror EditorView. + * + * @param {EditorView} ev + * @param {string} modeSuggestion + * @param {string} content */ -export function setMode(cmInstance, modeSuggestion, content) { - cmInstance.setOption('mode', getMode(modeSuggestion, content)); +export function setMode(ev, modeSuggestion, content) { + updateViewLanguage(ev, modeSuggestion, content); } /** diff --git a/resources/js/code/languages.js b/resources/js/code/languages.js new file mode 100644 index 000000000..4b04bdb14 --- /dev/null +++ b/resources/js/code/languages.js @@ -0,0 +1,120 @@ +import {StreamLanguage} from "@codemirror/language" + +import {css} from '@codemirror/legacy-modes/mode/css'; +import {c, java, cpp, csharp, kotlin, scala} from '@codemirror/legacy-modes/mode/clike'; +import {diff} from '@codemirror/legacy-modes/mode/diff'; +import {fortran} from '@codemirror/legacy-modes/mode/fortran'; +import {go} from '@codemirror/legacy-modes/mode/go'; +import {haskell} from '@codemirror/legacy-modes/mode/haskell'; +import {html} from '@codemirror/lang-html'; +import {javascript} from '@codemirror/lang-javascript'; +import {json} from '@codemirror/lang-json'; +import {julia} from '@codemirror/legacy-modes/mode/julia'; +import {lua} from '@codemirror/legacy-modes/mode/lua'; +import {markdown} from '@codemirror/lang-markdown'; +import {oCaml, fSharp, sml} from '@codemirror/legacy-modes/mode/mllike'; +import {nginx} from '@codemirror/legacy-modes/mode/nginx'; +import {perl} from '@codemirror/legacy-modes/mode/perl'; +import {pascal} from '@codemirror/legacy-modes/mode/pascal'; +import {php} from '@codemirror/lang-php'; +import {powerShell} from '@codemirror/legacy-modes/mode/powershell'; +import {properties} from '@codemirror/legacy-modes/mode/properties'; +import {python} from '@codemirror/legacy-modes/mode/python'; +import {ruby} from '@codemirror/legacy-modes/mode/ruby'; +import {rust} from '@codemirror/legacy-modes/mode/rust'; +import {shell} from '@codemirror/legacy-modes/mode/shell'; +import {sql} from '@codemirror/legacy-modes/mode/sql'; +import {stex} from '@codemirror/legacy-modes/mode/stex'; +import {toml} from '@codemirror/legacy-modes/mode/toml'; +import {vb} from '@codemirror/legacy-modes/mode/vb'; +import {vbScript} from '@codemirror/legacy-modes/mode/vbscript'; +import {xml} from '@codemirror/legacy-modes/mode/xml'; +import {yaml} from '@codemirror/legacy-modes/mode/yaml'; + + +// Mapping of possible languages or formats from user input to their codemirror modes. +// Value can be a mode string or a function that will receive the code content & return the mode string. +// The function option is used in the event the exact mode could be dynamic depending on the code. +const modeMap = { + bash: () => StreamLanguage.define(shell), + css: () => StreamLanguage.define(css), + c: () => StreamLanguage.define(c), + java: () => StreamLanguage.define(java), + scala: () => StreamLanguage.define(scala), + kotlin: () => StreamLanguage.define(kotlin), + 'c++': () => StreamLanguage.define(cpp), + 'c#': () => StreamLanguage.define(csharp), + csharp: () => StreamLanguage.define(csharp), + diff: () => StreamLanguage.define(diff), + for: () => StreamLanguage.define(fortran), + fortran: () => StreamLanguage.define(fortran), + 'f#': () => StreamLanguage.define(fSharp), + fsharp: () => StreamLanguage.define(fSharp), + go: () => StreamLanguage.define(go), + haskell: () => StreamLanguage.define(haskell), + hs: () => StreamLanguage.define(haskell), + html: () => html(), + ini: () => StreamLanguage.define(properties), + javascript: () => javascript(), + json: () => json(), + js: () => javascript(), + jl: () => StreamLanguage.define(julia), + julia: () => StreamLanguage.define(julia), + latex: () => StreamLanguage.define(stex), + lua: () => StreamLanguage.define(lua), + md: () => StreamLanguage.define(markdown), + mdown: () => StreamLanguage.define(markdown), + markdown: () => StreamLanguage.define(markdown), + ml: () => StreamLanguage.define(sml), + nginx: () => StreamLanguage.define(nginx), + perl: () => StreamLanguage.define(perl), + pl: () => StreamLanguage.define(perl), + powershell: () => StreamLanguage.define(powerShell), + properties: () => StreamLanguage.define(properties), + ocaml: () => StreamLanguage.define(oCaml), + pascal: () => StreamLanguage.define(pascal), + pas: () => StreamLanguage.define(pascal), + php: (code) => { + const hasTags = code.includes(' StreamLanguage.define(python), + python: () => StreamLanguage.define(python), + ruby: () => StreamLanguage.define(ruby), + rust: () => StreamLanguage.define(rust), + rb: () => StreamLanguage.define(ruby), + rs: () => StreamLanguage.define(rust), + shell: () => StreamLanguage.define(shell), + sh: () => StreamLanguage.define(shell), + stext: () => StreamLanguage.define(stex), + toml: () => StreamLanguage.define(toml), + ts: () => javascript({typescript: true}), + typescript: () => javascript({typescript: true}), + sql: () => StreamLanguage.define(sql), + vbs: () => StreamLanguage.define(vbScript), + vbscript: () => StreamLanguage.define(vbScript), + 'vb.net': () => StreamLanguage.define(vb), + vbnet: () => StreamLanguage.define(vb), + xml: () => StreamLanguage.define(xml), + yaml: () => StreamLanguage.define(yaml), + yml: () => StreamLanguage.define(yaml), +}; + +/** + * Get the relevant codemirror language extension based upon the given language + * suggestion and content. + * @param {String} langSuggestion + * @param {String} content + * @returns {StreamLanguage} + */ +export function getLanguageExtension(langSuggestion, content) { + const suggestion = langSuggestion.trim().replace(/^\./g, '').toLowerCase(); + + const language = modeMap[suggestion]; + + if (typeof language === 'undefined') { + return undefined; + } + + return language(content); +} \ No newline at end of file diff --git a/resources/js/code/modes.js b/resources/js/code/modes.js deleted file mode 100644 index 5a89255a5..000000000 --- a/resources/js/code/modes.js +++ /dev/null @@ -1,134 +0,0 @@ -import {StreamLanguage} from "@codemirror/language" - -import {css as langCss} from '@codemirror/legacy-modes/mode/css'; -import {clike as langClike} from '@codemirror/legacy-modes/mode/clike'; -import {diff as langDiff} from '@codemirror/legacy-modes/mode/diff'; -import {fortran as langFortran} from '@codemirror/legacy-modes/mode/fortran'; -import {go as langGo} from '@codemirror/legacy-modes/mode/go'; -import {haskell as langHaskell} from '@codemirror/legacy-modes/mode/haskell'; -// import {htmlmixed as langHtmlmixed} from '@codemirror/legacy-modes/mode/htmlmixed'; -import {javascript as langJavascript} from '@codemirror/legacy-modes/mode/javascript'; -import {julia as langJulia} from '@codemirror/legacy-modes/mode/julia'; -import {lua as langLua} from '@codemirror/legacy-modes/mode/lua'; -// import {markdown as langMarkdown} from '@codemirror/legacy-modes/mode/markdown'; -import {oCaml as langMllike} from '@codemirror/legacy-modes/mode/mllike'; -import {nginx as langNginx} from '@codemirror/legacy-modes/mode/nginx'; -import {perl as langPerl} from '@codemirror/legacy-modes/mode/perl'; -import {pascal as langPascal} from '@codemirror/legacy-modes/mode/pascal'; -// import {php as langPhp} from '@codemirror/legacy-modes/mode/php'; -import {powerShell as langPowershell} from '@codemirror/legacy-modes/mode/powershell'; -import {properties as langProperties} from '@codemirror/legacy-modes/mode/properties'; -import {python as langPython} from '@codemirror/legacy-modes/mode/python'; -import {ruby as langRuby} from '@codemirror/legacy-modes/mode/ruby'; -import {rust as langRust} from '@codemirror/legacy-modes/mode/rust'; -import {shell as langShell} from '@codemirror/legacy-modes/mode/shell'; -import {sql as langSql} from '@codemirror/legacy-modes/mode/sql'; -import {stex as langStex} from '@codemirror/legacy-modes/mode/stex'; -import {toml as langToml} from '@codemirror/legacy-modes/mode/toml'; -import {vb as langVb} from '@codemirror/legacy-modes/mode/vb'; -import {vbScript as langVbscript} from '@codemirror/legacy-modes/mode/vbscript'; -import {xml as langXml} from '@codemirror/legacy-modes/mode/xml'; -import {yaml as langYaml} from '@codemirror/legacy-modes/mode/yaml'; - -export const modes = [ - langCss, - langClike, - langDiff, - langFortran, - langGo, - langHaskell, - // langHtmlmixed, - langJavascript, - langJulia, - langLua, - // langMarkdown, - langMllike, - langNginx, - langPerl, - langPascal, - // langPhp, - langPowershell, - langProperties, - langPython, - langRuby, - langRust, - langShell, - langSql, - langStex, - langToml, - langVb, - langVbscript, - langXml, - langYaml, -]; - -// Mapping of possible languages or formats from user input to their codemirror modes. -// Value can be a mode string or a function that will receive the code content & return the mode string. -// The function option is used in the event the exact mode could be dynamic depending on the code. -export const modeMap = { - bash: 'shell', - css: 'css', - c: 'text/x-csrc', - java: 'text/x-java', - scala: 'text/x-scala', - kotlin: 'text/x-kotlin', - 'c++': 'text/x-c++src', - 'c#': 'text/x-csharp', - csharp: 'text/x-csharp', - diff: 'diff', - for: 'fortran', - fortran: 'fortran', - 'f#': 'text/x-fsharp', - fsharp: 'text/x-fsharp', - go: 'go', - haskell: 'haskell', - hs: 'haskell', - html: 'htmlmixed', - ini: 'properties', - javascript: 'text/javascript', - json: 'application/json', - js: 'text/javascript', - jl: 'text/x-julia', - julia: 'text/x-julia', - latex: 'text/x-stex', - lua: 'lua', - md: 'markdown', - mdown: 'markdown', - markdown: 'markdown', - ml: 'mllike', - nginx: 'nginx', - perl: 'perl', - pl: 'perl', - powershell: 'powershell', - properties: 'properties', - ocaml: 'text/x-ocaml', - pascal: 'text/x-pascal', - pas: 'text/x-pascal', - php: (content) => { - return content.includes(' StreamLanguage.define(mode)); -} \ No newline at end of file diff --git a/resources/js/code/setups.js b/resources/js/code/setups.js index 768d3a35d..45cc9c317 100644 --- a/resources/js/code/setups.js +++ b/resources/js/code/setups.js @@ -6,9 +6,6 @@ import {defaultHighlightStyle, syntaxHighlighting, bracketMatching, import {defaultKeymap, history, historyKeymap} from "@codemirror/commands" import {EditorState} from "@codemirror/state" -import {modesAsStreamLanguages} from "./modes"; - - export function viewer() { return [ lineNumbers(), @@ -27,6 +24,5 @@ export function viewer() { ...foldKeymap, ]), EditorState.readOnly.of(true), - ...modesAsStreamLanguages(), ]; } \ No newline at end of file diff --git a/resources/js/code/views.js b/resources/js/code/views.js new file mode 100644 index 000000000..e87718939 --- /dev/null +++ b/resources/js/code/views.js @@ -0,0 +1,38 @@ +import {getLanguageExtension} from "./languages"; +import {Compartment} from "@codemirror/state" +import {EditorView} from "@codemirror/view" + +const viewLangCompartments = new WeakMap(); + +/** + * Create a new editor view. + * + * @param {Object} config + * @returns {EditorView} + */ +export function createView(config) { + const langCompartment = new Compartment(); + config.extensions.push(langCompartment.of([])); + + const ev = new EditorView(config); + + viewLangCompartments.set(ev, langCompartment); + + return ev; +} + +/** + * Set the language mode of an EditorView. + * + * @param {EditorView} ev + * @param {string} modeSuggestion + * @param {string} content + */ +export function updateViewLanguage(ev, modeSuggestion, content) { + const compartment = viewLangCompartments.get(ev); + const language = getLanguageExtension(modeSuggestion, content); + + ev.dispatch({ + effects: compartment.reconfigure(language ? language : []) + }) +} \ No newline at end of file From 4757ed9453c0cfd8dd229e34ee3a64f74dfe0f3a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 4 Aug 2022 13:33:51 +0100 Subject: [PATCH 03/76] Converted codemirror languges to new packages where available Does increase bundle size massively though, Will need to think about solutions for this. --- package-lock.json | 217 +++++++++++++++++++++++++++++++++ package.json | 7 ++ resources/js/code/languages.js | 56 +++++---- 3 files changed, 253 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0eca777cb..a29be3324 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,11 +6,18 @@ "": { "dependencies": { "@codemirror/commands": "^6.0.1", + "@codemirror/lang-cpp": "^6.0.1", + "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-html": "^6.1.0", + "@codemirror/lang-java": "^6.0.0", "@codemirror/lang-javascript": "^6.0.2", "@codemirror/lang-json": "^6.0.0", "@codemirror/lang-markdown": "^6.0.1", "@codemirror/lang-php": "^6.0.0", + "@codemirror/lang-python": "^6.0.1", + "@codemirror/lang-rust": "^6.0.0", + "@codemirror/lang-sql": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", "@codemirror/language": "^6.2.1", "@codemirror/legacy-modes": "^6.1.0", "@codemirror/state": "^6.1.0", @@ -60,6 +67,15 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@codemirror/lang-cpp": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.1.tgz", + "integrity": "sha512-46p3ohfhjzkLWJ3VwvzX0aqlXh8UkEqX1xo2Eds9l6Ql3uDoxI2IZEjR9cgJaGOZTXCkDzQuQH7sfYAxMvzLjA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/cpp": "^1.0.0" + } + }, "node_modules/@codemirror/lang-css": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.0.0.tgz", @@ -85,6 +101,15 @@ "@lezer/html": "^1.0.0" } }, + "node_modules/@codemirror/lang-java": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.0.tgz", + "integrity": "sha512-aeWq+ikUS6Eubk6RBbiMgxuBIT4Ih8Asb1qc2pSiMcstrwr4ODbazPXsBHbLBYg3aObvFyOm2bNQncbQJjZ3sQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/java": "^1.0.0" + } + }, "node_modules/@codemirror/lang-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.0.2.tgz", @@ -133,6 +158,48 @@ "@lezer/php": "^1.0.0" } }, + "node_modules/@codemirror/lang-python": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.0.1.tgz", + "integrity": "sha512-w2jTSY+LgXnK7iIBLgMxk6xtJhZHkcxcGGveuq9zYmncURmOTFXKnDvBaBClNIHKgjkHXZqGK8ZduCMK23hZPA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/python": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-rust": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.0.tgz", + "integrity": "sha512-VQql3Qk1BwoXb3SUkeWll/EEwhsgQWc1bpia7CFqqp2PhQBb5A6r4Vj2JCkU/nE6A7TDPSGHTOoqJSG5s/VXtQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/rust": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-sql": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.0.0.tgz", + "integrity": "sha512-mq4NwTDbbo7QZktfgPsS+ms0FmAceH4WM2jLbgf+N28FoKUy0JzGe3XJymgnTewXnNUwujKBxArQzibxSDdVyQ==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-xml": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.0.0.tgz", + "integrity": "sha512-M/HLWxIiP956xGjtrxkeHkCmDGVQGKu782x8pOH5CLJIMkWtiB1DWfDoDHqpFjdEE9dkfcqPWvYfVi6GbhuXEg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, "node_modules/@codemirror/language": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.2.1.tgz", @@ -194,6 +261,15 @@ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz", "integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA==" }, + "node_modules/@lezer/cpp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.0.0.tgz", + "integrity": "sha512-Klk3/AIEKoptmm6cNm7xTulNXjdTKkD+hVOEcz/NeRg8tIestP5hsGHJeFDR/XtyDTxsjoPjKZRIGohht7zbKw==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@lezer/css": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.0.0.tgz", @@ -221,6 +297,15 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@lezer/java": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.0.0.tgz", + "integrity": "sha512-z2EA0JHq2WoiKfQy5uOOd4t17PJtq8guh58gPkSzOnNcQ7DNbkrU+Axak+jL8+Noinwyz2tRNOseQFj+Tg+P0A==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@lezer/javascript": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.0.2.tgz", @@ -265,6 +350,33 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@lezer/python": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.0.tgz", + "integrity": "sha512-FVPp2usfj3zZuc+2RidXAY94WAcsHQ3dbKDbXuZgoAwUungAcXwd3EWXiWQvwNqbae+ek51bWi8dwbiQqweWCg==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/rust": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.0.tgz", + "integrity": "sha512-IpGAxIjNxYmX9ra6GfQTSPegdCAWNeq23WNmrsMMQI7YNSvKtYxO4TX5rgZUmbhEucWn0KTBMeDEPXg99YKtTA==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/xml": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.0.tgz", + "integrity": "sha512-73iI9UK8iqSvWtLlOEl/g+50ivwQn8Ge6foHVN66AXUS1RccFnAoc7BYU8b3c8/rP6dfCOGqAGaWLxBzhj60MA==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", @@ -2186,6 +2298,15 @@ "@lezer/common": "^1.0.0" } }, + "@codemirror/lang-cpp": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.1.tgz", + "integrity": "sha512-46p3ohfhjzkLWJ3VwvzX0aqlXh8UkEqX1xo2Eds9l6Ql3uDoxI2IZEjR9cgJaGOZTXCkDzQuQH7sfYAxMvzLjA==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/cpp": "^1.0.0" + } + }, "@codemirror/lang-css": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.0.0.tgz", @@ -2211,6 +2332,15 @@ "@lezer/html": "^1.0.0" } }, + "@codemirror/lang-java": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.0.tgz", + "integrity": "sha512-aeWq+ikUS6Eubk6RBbiMgxuBIT4Ih8Asb1qc2pSiMcstrwr4ODbazPXsBHbLBYg3aObvFyOm2bNQncbQJjZ3sQ==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/java": "^1.0.0" + } + }, "@codemirror/lang-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.0.2.tgz", @@ -2259,6 +2389,48 @@ "@lezer/php": "^1.0.0" } }, + "@codemirror/lang-python": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.0.1.tgz", + "integrity": "sha512-w2jTSY+LgXnK7iIBLgMxk6xtJhZHkcxcGGveuq9zYmncURmOTFXKnDvBaBClNIHKgjkHXZqGK8ZduCMK23hZPA==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/python": "^1.0.0" + } + }, + "@codemirror/lang-rust": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.0.tgz", + "integrity": "sha512-VQql3Qk1BwoXb3SUkeWll/EEwhsgQWc1bpia7CFqqp2PhQBb5A6r4Vj2JCkU/nE6A7TDPSGHTOoqJSG5s/VXtQ==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/rust": "^1.0.0" + } + }, + "@codemirror/lang-sql": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.0.0.tgz", + "integrity": "sha512-mq4NwTDbbo7QZktfgPsS+ms0FmAceH4WM2jLbgf+N28FoKUy0JzGe3XJymgnTewXnNUwujKBxArQzibxSDdVyQ==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@codemirror/lang-xml": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.0.0.tgz", + "integrity": "sha512-M/HLWxIiP956xGjtrxkeHkCmDGVQGKu782x8pOH5CLJIMkWtiB1DWfDoDHqpFjdEE9dkfcqPWvYfVi6GbhuXEg==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, "@codemirror/language": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.2.1.tgz", @@ -2320,6 +2492,15 @@ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz", "integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA==" }, + "@lezer/cpp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.0.0.tgz", + "integrity": "sha512-Klk3/AIEKoptmm6cNm7xTulNXjdTKkD+hVOEcz/NeRg8tIestP5hsGHJeFDR/XtyDTxsjoPjKZRIGohht7zbKw==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "@lezer/css": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.0.0.tgz", @@ -2347,6 +2528,15 @@ "@lezer/lr": "^1.0.0" } }, + "@lezer/java": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.0.0.tgz", + "integrity": "sha512-z2EA0JHq2WoiKfQy5uOOd4t17PJtq8guh58gPkSzOnNcQ7DNbkrU+Axak+jL8+Noinwyz2tRNOseQFj+Tg+P0A==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "@lezer/javascript": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.0.2.tgz", @@ -2391,6 +2581,33 @@ "@lezer/lr": "^1.0.0" } }, + "@lezer/python": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.0.tgz", + "integrity": "sha512-FVPp2usfj3zZuc+2RidXAY94WAcsHQ3dbKDbXuZgoAwUungAcXwd3EWXiWQvwNqbae+ek51bWi8dwbiQqweWCg==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/rust": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.0.tgz", + "integrity": "sha512-IpGAxIjNxYmX9ra6GfQTSPegdCAWNeq23WNmrsMMQI7YNSvKtYxO4TX5rgZUmbhEucWn0KTBMeDEPXg99YKtTA==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/xml": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.0.tgz", + "integrity": "sha512-73iI9UK8iqSvWtLlOEl/g+50ivwQn8Ge6foHVN66AXUS1RccFnAoc7BYU8b3c8/rP6dfCOGqAGaWLxBzhj60MA==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", diff --git a/package.json b/package.json index 4d2b70247..11e69962e 100644 --- a/package.json +++ b/package.json @@ -24,11 +24,18 @@ }, "dependencies": { "@codemirror/commands": "^6.0.1", + "@codemirror/lang-cpp": "^6.0.1", + "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-html": "^6.1.0", + "@codemirror/lang-java": "^6.0.0", "@codemirror/lang-javascript": "^6.0.2", "@codemirror/lang-json": "^6.0.0", "@codemirror/lang-markdown": "^6.0.1", "@codemirror/lang-php": "^6.0.0", + "@codemirror/lang-python": "^6.0.1", + "@codemirror/lang-rust": "^6.0.0", + "@codemirror/lang-sql": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", "@codemirror/language": "^6.2.1", "@codemirror/legacy-modes": "^6.1.0", "@codemirror/state": "^6.1.0", diff --git a/resources/js/code/languages.js b/resources/js/code/languages.js index 4b04bdb14..b43b1c397 100644 --- a/resources/js/code/languages.js +++ b/resources/js/code/languages.js @@ -1,12 +1,14 @@ import {StreamLanguage} from "@codemirror/language" -import {css} from '@codemirror/legacy-modes/mode/css'; -import {c, java, cpp, csharp, kotlin, scala} from '@codemirror/legacy-modes/mode/clike'; +import {css} from '@codemirror/lang-css'; +import {c, csharp, kotlin, scala} from '@codemirror/legacy-modes/mode/clike'; +import {cpp} from '@codemirror/lang-cpp'; import {diff} from '@codemirror/legacy-modes/mode/diff'; import {fortran} from '@codemirror/legacy-modes/mode/fortran'; import {go} from '@codemirror/legacy-modes/mode/go'; import {haskell} from '@codemirror/legacy-modes/mode/haskell'; import {html} from '@codemirror/lang-html'; +import {java} from '@codemirror/lang-java'; import {javascript} from '@codemirror/lang-javascript'; import {json} from '@codemirror/lang-json'; import {julia} from '@codemirror/legacy-modes/mode/julia'; @@ -19,16 +21,16 @@ import {pascal} from '@codemirror/legacy-modes/mode/pascal'; import {php} from '@codemirror/lang-php'; import {powerShell} from '@codemirror/legacy-modes/mode/powershell'; import {properties} from '@codemirror/legacy-modes/mode/properties'; -import {python} from '@codemirror/legacy-modes/mode/python'; +import {python} from '@codemirror/lang-python'; import {ruby} from '@codemirror/legacy-modes/mode/ruby'; -import {rust} from '@codemirror/legacy-modes/mode/rust'; +import {rust} from '@codemirror/lang-rust'; import {shell} from '@codemirror/legacy-modes/mode/shell'; -import {sql} from '@codemirror/legacy-modes/mode/sql'; +import {sql} from '@codemirror/lang-sql'; import {stex} from '@codemirror/legacy-modes/mode/stex'; import {toml} from '@codemirror/legacy-modes/mode/toml'; import {vb} from '@codemirror/legacy-modes/mode/vb'; import {vbScript} from '@codemirror/legacy-modes/mode/vbscript'; -import {xml} from '@codemirror/legacy-modes/mode/xml'; +import {xml} from '@codemirror/lang-xml'; import {yaml} from '@codemirror/legacy-modes/mode/yaml'; @@ -37,12 +39,9 @@ import {yaml} from '@codemirror/legacy-modes/mode/yaml'; // The function option is used in the event the exact mode could be dynamic depending on the code. const modeMap = { bash: () => StreamLanguage.define(shell), - css: () => StreamLanguage.define(css), c: () => StreamLanguage.define(c), - java: () => StreamLanguage.define(java), - scala: () => StreamLanguage.define(scala), - kotlin: () => StreamLanguage.define(kotlin), - 'c++': () => StreamLanguage.define(cpp), + css: () => css(), + 'c++': () => cpp(), 'c#': () => StreamLanguage.define(csharp), csharp: () => StreamLanguage.define(csharp), diff: () => StreamLanguage.define(diff), @@ -55,47 +54,50 @@ const modeMap = { hs: () => StreamLanguage.define(haskell), html: () => html(), ini: () => StreamLanguage.define(properties), + java: () => java(), javascript: () => javascript(), json: () => json(), js: () => javascript(), jl: () => StreamLanguage.define(julia), julia: () => StreamLanguage.define(julia), + kotlin: () => StreamLanguage.define(kotlin), latex: () => StreamLanguage.define(stex), lua: () => StreamLanguage.define(lua), - md: () => StreamLanguage.define(markdown), - mdown: () => StreamLanguage.define(markdown), - markdown: () => StreamLanguage.define(markdown), + markdown: () => markdown(), + md: () => markdown(), + mdown: () => markdown(), ml: () => StreamLanguage.define(sml), nginx: () => StreamLanguage.define(nginx), - perl: () => StreamLanguage.define(perl), - pl: () => StreamLanguage.define(perl), - powershell: () => StreamLanguage.define(powerShell), - properties: () => StreamLanguage.define(properties), - ocaml: () => StreamLanguage.define(oCaml), - pascal: () => StreamLanguage.define(pascal), pas: () => StreamLanguage.define(pascal), + pascal: () => StreamLanguage.define(pascal), + perl: () => StreamLanguage.define(perl), php: (code) => { const hasTags = code.includes(' StreamLanguage.define(python), - python: () => StreamLanguage.define(python), - ruby: () => StreamLanguage.define(ruby), - rust: () => StreamLanguage.define(rust), + pl: () => StreamLanguage.define(perl), + powershell: () => StreamLanguage.define(powerShell), + properties: () => StreamLanguage.define(properties), + ocaml: () => StreamLanguage.define(oCaml), + py: () => python(), + python: () => python(), rb: () => StreamLanguage.define(ruby), - rs: () => StreamLanguage.define(rust), + rs: () => rust(), + ruby: () => StreamLanguage.define(ruby), + rust: () => rust(), + scala: () => StreamLanguage.define(scala), shell: () => StreamLanguage.define(shell), sh: () => StreamLanguage.define(shell), stext: () => StreamLanguage.define(stex), toml: () => StreamLanguage.define(toml), ts: () => javascript({typescript: true}), typescript: () => javascript({typescript: true}), - sql: () => StreamLanguage.define(sql), + sql: () => sql(), vbs: () => StreamLanguage.define(vbScript), vbscript: () => StreamLanguage.define(vbScript), 'vb.net': () => StreamLanguage.define(vb), vbnet: () => StreamLanguage.define(vb), - xml: () => StreamLanguage.define(xml), + xml: () => xml(), yaml: () => StreamLanguage.define(yaml), yml: () => StreamLanguage.define(yaml), }; From 9fd7a6abedaf69efc449c7ea00724eb890fc71f7 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 4 Aug 2022 14:19:04 +0100 Subject: [PATCH 04/76] Added dark theme handling --- package-lock.json | 23 +++++++++++++++++++++++ package.json | 1 + resources/js/code/views.js | 23 +++++++++++++++++++++-- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index a29be3324..8327421b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@codemirror/language": "^6.2.1", "@codemirror/legacy-modes": "^6.1.0", "@codemirror/state": "^6.1.0", + "@codemirror/theme-one-dark": "^6.0.0", "@codemirror/view": "^6.1.2", "clipboard": "^2.0.11", "codemirror": "^6.0.1", @@ -246,6 +247,17 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.1.0.tgz", "integrity": "sha512-qbUr94DZTe6/V1VS7LDLz11rM/1t/nJxR1El4I6UaxDEdc0aZZvq6JCLJWiRmUf95NRAnDH6fhXn+PWp9wGCIg==" }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.0.0.tgz", + "integrity": "sha512-jTCfi1I8QT++3m21Ui6sU8qwu3F/hLv161KLxfvkV1cYWSBwyUanmQFs89ChobQjBHi2x7s2k71wF9WYvE8fdw==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, "node_modules/@codemirror/view": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.1.2.tgz", @@ -2477,6 +2489,17 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.1.0.tgz", "integrity": "sha512-qbUr94DZTe6/V1VS7LDLz11rM/1t/nJxR1El4I6UaxDEdc0aZZvq6JCLJWiRmUf95NRAnDH6fhXn+PWp9wGCIg==" }, + "@codemirror/theme-one-dark": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.0.0.tgz", + "integrity": "sha512-jTCfi1I8QT++3m21Ui6sU8qwu3F/hLv161KLxfvkV1cYWSBwyUanmQFs89ChobQjBHi2x7s2k71wF9WYvE8fdw==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, "@codemirror/view": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.1.2.tgz", diff --git a/package.json b/package.json index 11e69962e..3a655c87a 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@codemirror/language": "^6.2.1", "@codemirror/legacy-modes": "^6.1.0", "@codemirror/state": "^6.1.0", + "@codemirror/theme-one-dark": "^6.0.0", "@codemirror/view": "^6.1.2", "clipboard": "^2.0.11", "codemirror": "^6.0.1", diff --git a/resources/js/code/views.js b/resources/js/code/views.js index e87718939..cada9a1d6 100644 --- a/resources/js/code/views.js +++ b/resources/js/code/views.js @@ -1,18 +1,20 @@ -import {getLanguageExtension} from "./languages"; +import {getLanguageExtension} from "./languages" import {Compartment} from "@codemirror/state" import {EditorView} from "@codemirror/view" +import {oneDark} from "@codemirror/theme-one-dark" const viewLangCompartments = new WeakMap(); /** * Create a new editor view. * - * @param {Object} config + * @param {{parent: Element, doc: String, extensions: Array}} config * @returns {EditorView} */ export function createView(config) { const langCompartment = new Compartment(); config.extensions.push(langCompartment.of([])); + config.extensions.push(getTheme(config.parent)); const ev = new EditorView(config); @@ -21,6 +23,23 @@ export function createView(config) { return ev; } +/** + * Ge the theme extension to use for editor view instance. + * @returns {Extension} + */ +function getTheme(viewParentEl) { + const darkMode = document.documentElement.classList.contains('dark-mode'); + + const eventData = { + darkMode: darkMode, + theme: null, + }; + + window.$events.emitPublic(viewParentEl, 'library-cm6::configure-theme', eventData); + + return eventData.theme || (darkMode ? oneDark : []); +} + /** * Set the language mode of an EditorView. * From f51036b203936a3814286df37a03f6e200d9bb79 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 17 Feb 2023 22:14:34 +0000 Subject: [PATCH 05/76] Added newer languages where possible Cannot find existing option for twig/smarty, need to look other methods. --- package-lock.json | 134 --------------------------------- package.json | 10 --- resources/js/code.mjs | 0 resources/js/code/languages.js | 63 +++++++++------- 4 files changed, 37 insertions(+), 170 deletions(-) delete mode 100644 resources/js/code.mjs diff --git a/package-lock.json b/package-lock.json index 6cdb7e43b..e8cd4f6db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,18 +6,8 @@ "": { "dependencies": { "@codemirror/commands": "^6.0.1", - "@codemirror/lang-cpp": "^6.0.1", - "@codemirror/lang-css": "^6.0.0", - "@codemirror/lang-html": "^6.1.0", - "@codemirror/lang-java": "^6.0.0", - "@codemirror/lang-javascript": "^6.0.2", - "@codemirror/lang-json": "^6.0.0", "@codemirror/lang-markdown": "^6.0.1", "@codemirror/lang-php": "^6.0.0", - "@codemirror/lang-python": "^6.0.1", - "@codemirror/lang-rust": "^6.0.0", - "@codemirror/lang-sql": "^6.0.0", - "@codemirror/lang-xml": "^6.0.0", "@codemirror/language": "^6.2.1", "@codemirror/legacy-modes": "^6.1.0", "@codemirror/state": "^6.1.0", @@ -68,15 +58,6 @@ "@lezer/common": "^1.0.0" } }, - "node_modules/@codemirror/lang-cpp": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.2.tgz", - "integrity": "sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@lezer/cpp": "^1.0.0" - } - }, "node_modules/@codemirror/lang-css": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.0.2.tgz", @@ -104,15 +85,6 @@ "@lezer/html": "^1.3.0" } }, - "node_modules/@codemirror/lang-java": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.1.tgz", - "integrity": "sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@lezer/java": "^1.0.0" - } - }, "node_modules/@codemirror/lang-javascript": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.1.4.tgz", @@ -127,15 +99,6 @@ "@lezer/javascript": "^1.0.0" } }, - "node_modules/@codemirror/lang-json": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", - "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@lezer/json": "^1.0.0" - } - }, "node_modules/@codemirror/lang-markdown": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.1.0.tgz", @@ -161,49 +124,6 @@ "@lezer/php": "^1.0.0" } }, - "node_modules/@codemirror/lang-python": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.1.tgz", - "integrity": "sha512-AddGMIKUssUAqaDKoxKWA5GAzy/CVE0eSY7/ANgNzdS1GYBkp6N49XKEyMElkuN04UsZ+bTIQdj+tVV75NMwJw==", - "dependencies": { - "@codemirror/autocomplete": "^6.3.2", - "@codemirror/language": "^6.0.0", - "@lezer/python": "^1.0.0" - } - }, - "node_modules/@codemirror/lang-rust": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.1.tgz", - "integrity": "sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@lezer/rust": "^1.0.0" - } - }, - "node_modules/@codemirror/lang-sql": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.4.0.tgz", - "integrity": "sha512-UWGK1+zc9+JtkiT+XxHByp4N6VLgLvC2x0tIudrJG26gyNtn0hWOVoB0A8kh/NABPWkKl3tLWDYf2qOBJS9Zdw==", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@codemirror/lang-xml": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.0.2.tgz", - "integrity": "sha512-JQYZjHL2LAfpiZI2/qZ/qzDuSqmGKMwyApYmEUUCTxLM4MWS7sATUEfIguZQr9Zjx/7gcdnewb039smF6nC2zw==", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/language": "^6.4.0", - "@codemirror/state": "^6.0.0", - "@lezer/common": "^1.0.0", - "@lezer/xml": "^1.0.0" - } - }, "node_modules/@codemirror/language": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.6.0.tgz", @@ -628,15 +548,6 @@ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.2.tgz", "integrity": "sha512-SVgiGtMnMnW3ActR8SXgsDhw7a0w0ChHSYAyAUxxrOiJ1OqYWEKk/xJd84tTSPo1mo6DXLObAJALNnd0Hrv7Ng==" }, - "node_modules/@lezer/cpp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.0.tgz", - "integrity": "sha512-zUHrjNFuY/DOZCkOBJ6qItQIkcopHM/Zv/QOE0a4XNG3HDNahxTNu5fQYl8dIuKCpxCqRdMl5cEwl5zekFc7BA==", - "dependencies": { - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, "node_modules/@lezer/css": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.1.tgz", @@ -664,15 +575,6 @@ "@lezer/lr": "^1.0.0" } }, - "node_modules/@lezer/java": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.0.3.tgz", - "integrity": "sha512-kKN17wmgP1cgHb8juR4pwVSPMKkDMzY/lAPbBsZ1fpXwbk2sg3N1kIrf0q+LefxgrANaQb/eNO7+m2QPruTFng==", - "dependencies": { - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, "node_modules/@lezer/javascript": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.1.tgz", @@ -682,15 +584,6 @@ "@lezer/lr": "^1.3.0" } }, - "node_modules/@lezer/json": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.0.tgz", - "integrity": "sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw==", - "dependencies": { - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, "node_modules/@lezer/lr": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.3.3.tgz", @@ -717,33 +610,6 @@ "@lezer/lr": "^1.1.0" } }, - "node_modules/@lezer/python": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.2.tgz", - "integrity": "sha512-ukm4VhDasFX7/9BUYHTyUNXH0xQ5B7/QBlZD8P51+dh6GtXRSCQqNxloez5d+MxVb2Sg+31S8E/33qoFREfkpA==", - "dependencies": { - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@lezer/rust": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.0.tgz", - "integrity": "sha512-IpGAxIjNxYmX9ra6GfQTSPegdCAWNeq23WNmrsMMQI7YNSvKtYxO4TX5rgZUmbhEucWn0KTBMeDEPXg99YKtTA==", - "dependencies": { - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@lezer/xml": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.1.tgz", - "integrity": "sha512-jMDXrV953sDAUEMI25VNrI9dz94Ai96FfeglytFINhhwQ867HKlCE2jt3AwZTCT7M528WxdDWv/Ty8e9wizwmQ==", - "dependencies": { - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, "node_modules/ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", diff --git a/package.json b/package.json index 1ab0fe482..24f451a9c 100644 --- a/package.json +++ b/package.json @@ -24,18 +24,8 @@ }, "dependencies": { "@codemirror/commands": "^6.0.1", - "@codemirror/lang-cpp": "^6.0.1", - "@codemirror/lang-css": "^6.0.0", - "@codemirror/lang-html": "^6.1.0", - "@codemirror/lang-java": "^6.0.0", - "@codemirror/lang-javascript": "^6.0.2", - "@codemirror/lang-json": "^6.0.0", "@codemirror/lang-markdown": "^6.0.1", "@codemirror/lang-php": "^6.0.0", - "@codemirror/lang-python": "^6.0.1", - "@codemirror/lang-rust": "^6.0.0", - "@codemirror/lang-sql": "^6.0.0", - "@codemirror/lang-xml": "^6.0.0", "@codemirror/language": "^6.2.1", "@codemirror/legacy-modes": "^6.1.0", "@codemirror/state": "^6.1.0", diff --git a/resources/js/code.mjs b/resources/js/code.mjs deleted file mode 100644 index e69de29bb..000000000 diff --git a/resources/js/code/languages.js b/resources/js/code/languages.js index b43b1c397..61aaeeefe 100644 --- a/resources/js/code/languages.js +++ b/resources/js/code/languages.js @@ -1,37 +1,37 @@ import {StreamLanguage} from "@codemirror/language" -import {css} from '@codemirror/lang-css'; -import {c, csharp, kotlin, scala} from '@codemirror/legacy-modes/mode/clike'; -import {cpp} from '@codemirror/lang-cpp'; +import {css} from '@codemirror/legacy-modes/mode/css'; +import {c, cpp, csharp, java, kotlin, scala, dart} from '@codemirror/legacy-modes/mode/clike'; import {diff} from '@codemirror/legacy-modes/mode/diff'; import {fortran} from '@codemirror/legacy-modes/mode/fortran'; import {go} from '@codemirror/legacy-modes/mode/go'; import {haskell} from '@codemirror/legacy-modes/mode/haskell'; -import {html} from '@codemirror/lang-html'; -import {java} from '@codemirror/lang-java'; -import {javascript} from '@codemirror/lang-javascript'; -import {json} from '@codemirror/lang-json'; +import {javascript, json, typescript} from '@codemirror/legacy-modes/mode/javascript'; import {julia} from '@codemirror/legacy-modes/mode/julia'; import {lua} from '@codemirror/legacy-modes/mode/lua'; import {markdown} from '@codemirror/lang-markdown'; import {oCaml, fSharp, sml} from '@codemirror/legacy-modes/mode/mllike'; import {nginx} from '@codemirror/legacy-modes/mode/nginx'; +import {octave} from '@codemirror/legacy-modes/mode/octave'; import {perl} from '@codemirror/legacy-modes/mode/perl'; import {pascal} from '@codemirror/legacy-modes/mode/pascal'; import {php} from '@codemirror/lang-php'; import {powerShell} from '@codemirror/legacy-modes/mode/powershell'; import {properties} from '@codemirror/legacy-modes/mode/properties'; -import {python} from '@codemirror/lang-python'; +import {python} from '@codemirror/legacy-modes/mode/python'; import {ruby} from '@codemirror/legacy-modes/mode/ruby'; -import {rust} from '@codemirror/lang-rust'; +import {rust} from '@codemirror/legacy-modes/mode/rust'; +import {scheme} from '@codemirror/legacy-modes/mode/scheme'; import {shell} from '@codemirror/legacy-modes/mode/shell'; -import {sql} from '@codemirror/lang-sql'; +import {standardSQL, pgSQL, msSQL, mySQL, sqlite, plSQL} from '@codemirror/legacy-modes/mode/sql'; import {stex} from '@codemirror/legacy-modes/mode/stex'; import {toml} from '@codemirror/legacy-modes/mode/toml'; +// import {twig, smarty} from '@codemirror/legacy-modes/mode/php'; // TODO import {vb} from '@codemirror/legacy-modes/mode/vb'; import {vbScript} from '@codemirror/legacy-modes/mode/vbscript'; -import {xml} from '@codemirror/lang-xml'; +import {xml, html} from '@codemirror/legacy-modes/mode/xml'; import {yaml} from '@codemirror/legacy-modes/mode/yaml'; +import {swift} from "@codemirror/legacy-modes/mode/swift"; // Mapping of possible languages or formats from user input to their codemirror modes. @@ -40,10 +40,11 @@ import {yaml} from '@codemirror/legacy-modes/mode/yaml'; const modeMap = { bash: () => StreamLanguage.define(shell), c: () => StreamLanguage.define(c), - css: () => css(), - 'c++': () => cpp(), + css: () => StreamLanguage.define(css), + 'c++': () => StreamLanguage.define(cpp), 'c#': () => StreamLanguage.define(csharp), csharp: () => StreamLanguage.define(csharp), + dart: () => StreamLanguage.define(dart), diff: () => StreamLanguage.define(diff), for: () => StreamLanguage.define(fortran), fortran: () => StreamLanguage.define(fortran), @@ -52,52 +53,62 @@ const modeMap = { go: () => StreamLanguage.define(go), haskell: () => StreamLanguage.define(haskell), hs: () => StreamLanguage.define(haskell), - html: () => html(), + html: () => StreamLanguage.define(html), ini: () => StreamLanguage.define(properties), - java: () => java(), - javascript: () => javascript(), - json: () => json(), - js: () => javascript(), + java: () => StreamLanguage.define(java), + javascript: () => StreamLanguage.define(javascript), + json: () => StreamLanguage.define(json), + js: () => StreamLanguage.define(javascript), jl: () => StreamLanguage.define(julia), julia: () => StreamLanguage.define(julia), kotlin: () => StreamLanguage.define(kotlin), latex: () => StreamLanguage.define(stex), lua: () => StreamLanguage.define(lua), markdown: () => markdown(), + matlab: () => StreamLanguage.define(octave), md: () => markdown(), mdown: () => markdown(), ml: () => StreamLanguage.define(sml), + mssql: () => StreamLanguage.define(msSQL), + mysql: () => StreamLanguage.define(mySQL), nginx: () => StreamLanguage.define(nginx), + octave: () => StreamLanguage.define(octave), pas: () => StreamLanguage.define(pascal), pascal: () => StreamLanguage.define(pascal), perl: () => StreamLanguage.define(perl), + pgsql: () => StreamLanguage.define(pgSQL), php: (code) => { const hasTags = code.includes(' StreamLanguage.define(perl), + 'pl/sql': () => StreamLanguage.define(plSQL), + postgresql: () => StreamLanguage.define(pgSQL), powershell: () => StreamLanguage.define(powerShell), properties: () => StreamLanguage.define(properties), ocaml: () => StreamLanguage.define(oCaml), - py: () => python(), - python: () => python(), + py: () => StreamLanguage.define(python), + python: () => StreamLanguage.define(python), rb: () => StreamLanguage.define(ruby), - rs: () => rust(), + rs: () => StreamLanguage.define(rust), ruby: () => StreamLanguage.define(ruby), - rust: () => rust(), + rust: () => StreamLanguage.define(rust), scala: () => StreamLanguage.define(scala), + scheme: () => StreamLanguage.define(scheme), shell: () => StreamLanguage.define(shell), sh: () => StreamLanguage.define(shell), stext: () => StreamLanguage.define(stex), + swift: () => StreamLanguage.define(swift), toml: () => StreamLanguage.define(toml), - ts: () => javascript({typescript: true}), - typescript: () => javascript({typescript: true}), - sql: () => sql(), + ts: () => StreamLanguage.define(typescript), + typescript: () => StreamLanguage.define(typescript), + sql: () => StreamLanguage.define(standardSQL), + sqlite: () => StreamLanguage.define(sqlite), vbs: () => StreamLanguage.define(vbScript), vbscript: () => StreamLanguage.define(vbScript), 'vb.net': () => StreamLanguage.define(vb), vbnet: () => StreamLanguage.define(vb), - xml: () => xml(), + xml: () => StreamLanguage.define(xml), yaml: () => StreamLanguage.define(yaml), yml: () => StreamLanguage.define(yaml), }; From c148e2f3d96f3e62fef9ee63c0cd52943b3160ab Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 17 Feb 2023 22:37:13 +0000 Subject: [PATCH 06/76] Added esbuild bundle inspection metafile --- .gitignore | 4 +++- dev/build/esbuild.js | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 90b80e7b8..5f3aa6600 100644 --- a/.gitignore +++ b/.gitignore @@ -21,8 +21,10 @@ yarn.lock nbproject .buildpath .project +.nvmrc .settings/ webpack-stats.json .phpunit.result.cache .DS_Store -phpstan.neon \ No newline at end of file +phpstan.neon +esbuild-meta.json \ No newline at end of file diff --git a/dev/build/esbuild.js b/dev/build/esbuild.js index 57a224876..2ff7ac1f4 100644 --- a/dev/build/esbuild.js +++ b/dev/build/esbuild.js @@ -2,6 +2,7 @@ const esbuild = require('esbuild'); const path = require('path'); +const fs = require('fs'); // Check if we're building for production // (Set via passing `production` as first argument) @@ -19,6 +20,7 @@ const outdir = path.join(__dirname, '../../public/dist'); // Build via esbuild esbuild.build({ bundle: true, + metafile: true, entryPoints, outdir, sourcemap: true, @@ -27,4 +29,6 @@ esbuild.build({ format: 'esm', minify: isProd, logLevel: "info", +}).then(result => { + fs.writeFileSync('esbuild-meta.json', JSON.stringify(result.metafile)); }).catch(() => process.exit(1)); \ No newline at end of file From c42956bcafc7c43275457887c119476af8f72b36 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 13 Mar 2023 13:18:33 +0000 Subject: [PATCH 07/76] Started build of content-permissions API endpoints --- app/Auth/Permissions/EntityPermission.php | 17 ++-- app/Entities/EntityProvider.php | 38 +++----- app/Entities/Tools/PermissionsUpdater.php | 74 +++++++++++++++- .../Api/ContentPermissionsController.php | 87 +++++++++++++++++++ routes/api.php | 4 + 5 files changed, 181 insertions(+), 39 deletions(-) create mode 100644 app/Http/Controllers/Api/ContentPermissionsController.php diff --git a/app/Auth/Permissions/EntityPermission.php b/app/Auth/Permissions/EntityPermission.php index 32ebc440d..603cf61ad 100644 --- a/app/Auth/Permissions/EntityPermission.php +++ b/app/Auth/Permissions/EntityPermission.php @@ -5,7 +5,6 @@ namespace BookStack\Auth\Permissions; use BookStack\Auth\Role; use BookStack\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Database\Eloquent\Relations\MorphTo; /** * @property int $id @@ -23,14 +22,14 @@ class EntityPermission extends Model protected $fillable = ['role_id', 'view', 'create', 'update', 'delete']; public $timestamps = false; - - /** - * Get this restriction's attached entity. - */ - public function restrictable(): MorphTo - { - return $this->morphTo('restrictable'); - } + protected $hidden = ['entity_id', 'entity_type', 'id']; + protected $casts = [ + 'view' => 'boolean', + 'create' => 'boolean', + 'read' => 'boolean', + 'update' => 'boolean', + 'delete' => 'boolean', + ]; /** * Get the role assigned to this entity permission. diff --git a/app/Entities/EntityProvider.php b/app/Entities/EntityProvider.php index aaf392c7b..365daf7eb 100644 --- a/app/Entities/EntityProvider.php +++ b/app/Entities/EntityProvider.php @@ -18,30 +18,11 @@ use BookStack\Entities\Models\PageRevision; */ class EntityProvider { - /** - * @var Bookshelf - */ - public $bookshelf; - - /** - * @var Book - */ - public $book; - - /** - * @var Chapter - */ - public $chapter; - - /** - * @var Page - */ - public $page; - - /** - * @var PageRevision - */ - public $pageRevision; + public Bookshelf $bookshelf; + public Book $book; + public Chapter $chapter; + public Page $page; + public PageRevision $pageRevision; public function __construct() { @@ -69,13 +50,18 @@ class EntityProvider } /** - * Get an entity instance by it's basic name. + * Get an entity instance by its basic name. */ public function get(string $type): Entity { $type = strtolower($type); + $instance = $this->all()[$type] ?? null; - return $this->all()[$type]; + if (is_null($instance)) { + throw new \InvalidArgumentException("Provided type \"{$type}\" is not a valid entity type"); + } + + return $instance; } /** diff --git a/app/Entities/Tools/PermissionsUpdater.php b/app/Entities/Tools/PermissionsUpdater.php index eb4eb6b48..0d1d307af 100644 --- a/app/Entities/Tools/PermissionsUpdater.php +++ b/app/Entities/Tools/PermissionsUpdater.php @@ -4,20 +4,20 @@ namespace BookStack\Entities\Tools; use BookStack\Actions\ActivityType; use BookStack\Auth\Permissions\EntityPermission; +use BookStack\Auth\Role; use BookStack\Auth\User; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Entity; use BookStack\Facades\Activity; use Illuminate\Http\Request; -use Illuminate\Support\Collection; class PermissionsUpdater { /** * Update an entities permissions from a permission form submit request. */ - public function updateFromPermissionsForm(Entity $entity, Request $request) + public function updateFromPermissionsForm(Entity $entity, Request $request): void { $permissions = $request->get('permissions', null); $ownerId = $request->get('owned_by', null); @@ -39,12 +39,44 @@ class PermissionsUpdater Activity::add(ActivityType::PERMISSIONS_UPDATE, $entity); } + /** + * Update permissions from API request data. + */ + public function updateFromApiRequestData(Entity $entity, array $data): void + { + if (isset($data['override_role_permissions'])) { + $entity->permissions()->where('role_id', '!=', 0)->delete(); + $rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions($data['override_role_permissions'] ?? [], false); + $entity->permissions()->createMany($rolePermissionData); + } + + if (array_key_exists('override_fallback_permissions', $data)) { + $entity->permissions()->where('role_id', '=', 0)->delete(); + } + + if (isset($data['override_fallback_permissions'])) { + $data = $data['override_fallback_permissions']; + $data['role_id'] = 0; + $rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions([$data], true); + $entity->permissions()->createMany($rolePermissionData); + } + + if (isset($data['owner_id'])) { + $this->updateOwnerFromId($entity, intval($data['owner_id'])); + } + + $entity->save(); + $entity->rebuildPermissions(); + + Activity::add(ActivityType::PERMISSIONS_UPDATE, $entity); + } + /** * Update the owner of the given entity. * Checks the user exists in the system first. * Does not save the model, just updates it. */ - protected function updateOwnerFromId(Entity $entity, int $newOwnerId) + protected function updateOwnerFromId(Entity $entity, int $newOwnerId): void { $newOwner = User::query()->find($newOwnerId); if (!is_null($newOwner)) { @@ -67,7 +99,41 @@ class PermissionsUpdater $formatted[] = $entityPermissionData; } - return $formatted; + return $this->filterEntityPermissionDataUponRole($formatted, true); + } + + protected function formatPermissionsFromApiRequestToEntityPermissions(array $permissions, bool $allowFallback): array + { + $formatted = []; + + foreach ($permissions as $requestPermissionData) { + $entityPermissionData = ['role_id' => $requestPermissionData['role_id']]; + foreach (EntityPermission::PERMISSIONS as $permission) { + $entityPermissionData[$permission] = boolval($requestPermissionData[$permission] ?? false); + } + $formatted[] = $entityPermissionData; + } + + return $this->filterEntityPermissionDataUponRole($formatted, $allowFallback); + } + + protected function filterEntityPermissionDataUponRole(array $entityPermissionData, bool $allowFallback): array + { + $roleIds = []; + foreach ($entityPermissionData as $permissionEntry) { + $roleIds[] = intval($permissionEntry['role_id']); + } + + $actualRoleIds = array_unique(array_values(array_filter($roleIds))); + $rolesById = Role::query()->whereIn('id', $actualRoleIds)->get('id')->keyBy('id'); + + return array_values(array_filter($entityPermissionData, function ($data) use ($rolesById, $allowFallback) { + if (intval($data['role_id']) === 0) { + return $allowFallback; + } + + return $rolesById->has($data['role_id']); + })); } /** diff --git a/app/Http/Controllers/Api/ContentPermissionsController.php b/app/Http/Controllers/Api/ContentPermissionsController.php new file mode 100644 index 000000000..16fb68a0f --- /dev/null +++ b/app/Http/Controllers/Api/ContentPermissionsController.php @@ -0,0 +1,87 @@ + [ + 'owner_id' => ['int'], + + 'override_role_permissions' => ['array'], + 'override_role_permissions.*.role_id' => ['required', 'int'], + 'override_role_permissions.*.view' => ['required', 'boolean'], + 'override_role_permissions.*.create' => ['required', 'boolean'], + 'override_role_permissions.*.update' => ['required', 'boolean'], + 'override_role_permissions.*.delete' => ['required', 'boolean'], + + 'override_fallback_permissions' => ['nullable'], + 'override_fallback_permissions.view' => ['required', 'boolean'], + 'override_fallback_permissions.create' => ['required', 'boolean'], + 'override_fallback_permissions.update' => ['required', 'boolean'], + 'override_fallback_permissions.delete' => ['required', 'boolean'], + ] + ]; + + /** + * Read the configured content-level permissions for the item of the given type and ID. + * 'contentType' should be one of: page, book, chapter, bookshelf. + * 'contentId' should be the relevant ID of that item type you'd like to handle permissions for. + */ + public function read(string $contentType, string $contentId) + { + $entity = $this->entities->get($contentType) + ->newQuery()->scopes(['visible'])->findOrFail($contentId); + + $this->checkOwnablePermission('restrictions-manage', $entity); + + return response()->json($this->formattedPermissionDataForEntity($entity)); + } + + /** + * Update the configured content-level permissions for the item of the given type and ID. + * 'contentType' should be one of: page, book, chapter, bookshelf. + * 'contentId' should be the relevant ID of that item type you'd like to handle permissions for. + */ + public function update(Request $request, string $contentType, string $contentId) + { + $entity = $this->entities->get($contentType) + ->newQuery()->scopes(['visible'])->findOrFail($contentId); + + $this->checkOwnablePermission('restrictions-manage', $entity); + + $data = $this->validate($request, $this->rules()['update']); + $this->permissionsUpdater->updateFromApiRequestData($entity, $data); + + return response()->json($this->formattedPermissionDataForEntity($entity)); + } + + protected function formattedPermissionDataForEntity(Entity $entity): array + { + $rolePermissions = $entity->permissions() + ->where('role_id', '!=', 0) + ->with(['role:id,display_name']) + ->get(); + + $fallback = $entity->permissions()->where('role_id', '=', 0)->first(); + $fallback?->makeHidden('role_id'); + + return [ + 'owner' => $entity->ownedBy()->first(), + 'override_role_permissions' => $rolePermissions, + 'override_fallback_permissions' => $fallback, + 'inheriting' => is_null($fallback), + ]; + } +} diff --git a/routes/api.php b/routes/api.php index d1b64d455..1b852fed7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -13,6 +13,7 @@ use BookStack\Http\Controllers\Api\BookExportApiController; use BookStack\Http\Controllers\Api\BookshelfApiController; use BookStack\Http\Controllers\Api\ChapterApiController; use BookStack\Http\Controllers\Api\ChapterExportApiController; +use BookStack\Http\Controllers\Api\ContentPermissionsController; use BookStack\Http\Controllers\Api\PageApiController; use BookStack\Http\Controllers\Api\PageExportApiController; use BookStack\Http\Controllers\Api\RecycleBinApiController; @@ -85,3 +86,6 @@ Route::delete('roles/{id}', [RoleApiController::class, 'delete']); Route::get('recycle-bin', [RecycleBinApiController::class, 'list']); Route::put('recycle-bin/{deletionId}', [RecycleBinApiController::class, 'restore']); Route::delete('recycle-bin/{deletionId}', [RecycleBinApiController::class, 'destroy']); + +Route::get('content-permissions/{contentType}/{contentId}', [ContentPermissionsController::class, 'read']); +Route::put('content-permissions/{contentType}/{contentId}', [ContentPermissionsController::class, 'update']); From 0de75300591873ece8af60152fdb51172e41f3a5 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 13 Mar 2023 20:06:52 +0000 Subject: [PATCH 08/76] Tweaked content permission endpoints, covered with tests --- app/Entities/Tools/PermissionsUpdater.php | 10 +- .../Api/ContentPermissionsController.php | 43 ++- tests/Api/ContentPermissionsApiTest.php | 262 ++++++++++++++++++ 3 files changed, 295 insertions(+), 20 deletions(-) create mode 100644 tests/Api/ContentPermissionsApiTest.php diff --git a/app/Entities/Tools/PermissionsUpdater.php b/app/Entities/Tools/PermissionsUpdater.php index 0d1d307af..36ed7ccde 100644 --- a/app/Entities/Tools/PermissionsUpdater.php +++ b/app/Entities/Tools/PermissionsUpdater.php @@ -44,18 +44,18 @@ class PermissionsUpdater */ public function updateFromApiRequestData(Entity $entity, array $data): void { - if (isset($data['override_role_permissions'])) { + if (isset($data['role_permissions'])) { $entity->permissions()->where('role_id', '!=', 0)->delete(); - $rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions($data['override_role_permissions'] ?? [], false); + $rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions($data['role_permissions'] ?? [], false); $entity->permissions()->createMany($rolePermissionData); } - if (array_key_exists('override_fallback_permissions', $data)) { + if (array_key_exists('fallback_permissions', $data)) { $entity->permissions()->where('role_id', '=', 0)->delete(); } - if (isset($data['override_fallback_permissions'])) { - $data = $data['override_fallback_permissions']; + if (isset($data['fallback_permissions']['inheriting']) && $data['fallback_permissions']['inheriting'] !== true) { + $data = $data['fallback_permissions']; $data['role_id'] = 0; $rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions([$data], true); $entity->permissions()->createMany($rolePermissionData); diff --git a/app/Http/Controllers/Api/ContentPermissionsController.php b/app/Http/Controllers/Api/ContentPermissionsController.php index 16fb68a0f..1db90f97a 100644 --- a/app/Http/Controllers/Api/ContentPermissionsController.php +++ b/app/Http/Controllers/Api/ContentPermissionsController.php @@ -19,18 +19,19 @@ class ContentPermissionsController extends ApiController 'update' => [ 'owner_id' => ['int'], - 'override_role_permissions' => ['array'], - 'override_role_permissions.*.role_id' => ['required', 'int'], - 'override_role_permissions.*.view' => ['required', 'boolean'], - 'override_role_permissions.*.create' => ['required', 'boolean'], - 'override_role_permissions.*.update' => ['required', 'boolean'], - 'override_role_permissions.*.delete' => ['required', 'boolean'], + 'role_permissions' => ['array'], + 'role_permissions.*.role_id' => ['required', 'int', 'exists:roles,id'], + 'role_permissions.*.view' => ['required', 'boolean'], + 'role_permissions.*.create' => ['required', 'boolean'], + 'role_permissions.*.update' => ['required', 'boolean'], + 'role_permissions.*.delete' => ['required', 'boolean'], - 'override_fallback_permissions' => ['nullable'], - 'override_fallback_permissions.view' => ['required', 'boolean'], - 'override_fallback_permissions.create' => ['required', 'boolean'], - 'override_fallback_permissions.update' => ['required', 'boolean'], - 'override_fallback_permissions.delete' => ['required', 'boolean'], + 'fallback_permissions' => ['nullable'], + 'fallback_permissions.inheriting' => ['required_with:fallback_permissions', 'boolean'], + 'fallback_permissions.view' => ['required_if:fallback_permissions.inheriting,false', 'boolean'], + 'fallback_permissions.create' => ['required_if:fallback_permissions.inheriting,false', 'boolean'], + 'fallback_permissions.update' => ['required_if:fallback_permissions.inheriting,false', 'boolean'], + 'fallback_permissions.delete' => ['required_if:fallback_permissions.inheriting,false', 'boolean'], ] ]; @@ -38,6 +39,9 @@ class ContentPermissionsController extends ApiController * Read the configured content-level permissions for the item of the given type and ID. * 'contentType' should be one of: page, book, chapter, bookshelf. * 'contentId' should be the relevant ID of that item type you'd like to handle permissions for. + * The permissions shown are those that override the default for just the specified item, they do not show the + * full evaluated permission for a role, nor do they reflect permissions inherited from other items in the hierarchy. + * Fallback permission values may be `null` when inheriting is active. */ public function read(string $contentType, string $contentId) { @@ -53,6 +57,10 @@ class ContentPermissionsController extends ApiController * Update the configured content-level permissions for the item of the given type and ID. * 'contentType' should be one of: page, book, chapter, bookshelf. * 'contentId' should be the relevant ID of that item type you'd like to handle permissions for. + * Providing an empty `role_permissions` array will remove any existing configured role permissions, + * so you may want to fetch existing permissions beforehand if just adding/removing a single item. + * You should completely omit the `owner_id`, `role_permissions` and/or the `fallback_permissions` properties + * if you don't wish to update details within those categories. */ public function update(Request $request, string $contentType, string $contentId) { @@ -75,13 +83,18 @@ class ContentPermissionsController extends ApiController ->get(); $fallback = $entity->permissions()->where('role_id', '=', 0)->first(); - $fallback?->makeHidden('role_id'); + $fallbackData = [ + 'inheriting' => is_null($fallback), + 'view' => $fallback->view ?? null, + 'create' => $fallback->create ?? null, + 'update' => $fallback->update ?? null, + 'delete' => $fallback->delete ?? null, + ]; return [ 'owner' => $entity->ownedBy()->first(), - 'override_role_permissions' => $rolePermissions, - 'override_fallback_permissions' => $fallback, - 'inheriting' => is_null($fallback), + 'role_permissions' => $rolePermissions, + 'fallback_permissions' => $fallbackData, ]; } } diff --git a/tests/Api/ContentPermissionsApiTest.php b/tests/Api/ContentPermissionsApiTest.php new file mode 100644 index 000000000..50b82e5c4 --- /dev/null +++ b/tests/Api/ContentPermissionsApiTest.php @@ -0,0 +1,262 @@ +entities->page(); + $endpointMap = [ + ['get', "/api/content-permissions/page/{$page->id}"], + ['put', "/api/content-permissions/page/{$page->id}"], + ]; + $editor = $this->users->editor(); + + $this->actingAs($editor, 'api'); + foreach ($endpointMap as [$method, $uri]) { + $resp = $this->json($method, $uri); + $resp->assertStatus(403); + $resp->assertJson($this->permissionErrorResponse()); + } + + $this->permissions->grantUserRolePermissions($editor, ['restrictions-manage-all']); + + foreach ($endpointMap as [$method, $uri]) { + $resp = $this->json($method, $uri); + $this->assertNotEquals(403, $resp->getStatusCode()); + } + } + + public function test_read_endpoint_shows_expected_detail() + { + $page = $this->entities->page(); + $owner = $this->users->newUser(); + $role = $this->users->createRole(); + $this->permissions->addEntityPermission($page, ['view', 'delete'], $role); + $this->permissions->changeEntityOwner($page, $owner); + $this->permissions->setFallbackPermissions($page, ['update', 'create']); + + $this->actingAsApiAdmin(); + $resp = $this->getJson($this->baseEndpoint . "/page/{$page->id}"); + + $resp->assertOk(); + $resp->assertExactJson([ + 'owner' => [ + 'id' => $owner->id, 'name' => $owner->name, 'slug' => $owner->slug, + ], + 'role_permissions' => [ + [ + 'role_id' => $role->id, + 'view' => true, + 'create' => false, + 'update' => false, + 'delete' => true, + 'role' => [ + 'id' => $role->id, + 'display_name' => $role->display_name, + ] + ] + ], + 'fallback_permissions' => [ + 'inheriting' => false, + 'view' => false, + 'create' => true, + 'update' => true, + 'delete' => false, + ], + ]); + } + + public function test_read_endpoint_shows_expected_detail_when_items_are_empty() + { + $page = $this->entities->page(); + $page->permissions()->delete(); + $page->owned_by = null; + $page->save(); + + $this->actingAsApiAdmin(); + $resp = $this->getJson($this->baseEndpoint . "/page/{$page->id}"); + + $resp->assertOk(); + $resp->assertExactJson([ + 'owner' => null, + 'role_permissions' => [], + 'fallback_permissions' => [ + 'inheriting' => true, + 'view' => null, + 'create' => null, + 'update' => null, + 'delete' => null, + ], + ]); + } + + public function test_update_endpoint_can_change_owner() + { + $page = $this->entities->page(); + $newOwner = $this->users->newUser(); + + $this->actingAsApiAdmin(); + $resp = $this->putJson($this->baseEndpoint . "/page/{$page->id}", [ + 'owner_id' => $newOwner->id, + ]); + + $resp->assertOk(); + $resp->assertExactJson([ + 'owner' => ['id' => $newOwner->id, 'name' => $newOwner->name, 'slug' => $newOwner->slug], + 'role_permissions' => [], + 'fallback_permissions' => [ + 'inheriting' => true, + 'view' => null, + 'create' => null, + 'update' => null, + 'delete' => null, + ], + ]); + } + + public function test_update_can_set_role_permissions() + { + $page = $this->entities->page(); + $page->owned_by = null; + $page->save(); + $newRoleA = $this->users->createRole(); + $newRoleB = $this->users->createRole(); + + $this->actingAsApiAdmin(); + $resp = $this->putJson($this->baseEndpoint . "/page/{$page->id}", [ + 'role_permissions' => [ + ['role_id' => $newRoleA->id, 'view' => true, 'create' => false, 'update' => false, 'delete' => false], + ['role_id' => $newRoleB->id, 'view' => true, 'create' => false, 'update' => true, 'delete' => true], + ], + ]); + + $resp->assertOk(); + $resp->assertExactJson([ + 'owner' => null, + 'role_permissions' => [ + [ + 'role_id' => $newRoleA->id, + 'view' => true, + 'create' => false, + 'update' => false, + 'delete' => false, + 'role' => [ + 'id' => $newRoleA->id, + 'display_name' => $newRoleA->display_name, + ] + ], + [ + 'role_id' => $newRoleB->id, + 'view' => true, + 'create' => false, + 'update' => true, + 'delete' => true, + 'role' => [ + 'id' => $newRoleB->id, + 'display_name' => $newRoleB->display_name, + ] + ] + ], + 'fallback_permissions' => [ + 'inheriting' => true, + 'view' => null, + 'create' => null, + 'update' => null, + 'delete' => null, + ], + ]); + } + + public function test_update_can_set_fallback_permissions() + { + $page = $this->entities->page(); + $page->owned_by = null; + $page->save(); + + $this->actingAsApiAdmin(); + $resp = $this->putJson($this->baseEndpoint . "/page/{$page->id}", [ + 'fallback_permissions' => [ + 'inheriting' => false, + 'view' => true, + 'create' => true, + 'update' => true, + 'delete' => false, + ], + ]); + + $resp->assertOk(); + $resp->assertExactJson([ + 'owner' => null, + 'role_permissions' => [], + 'fallback_permissions' => [ + 'inheriting' => false, + 'view' => true, + 'create' => true, + 'update' => true, + 'delete' => false, + ], + ]); + } + + public function test_update_can_clear_roles_permissions() + { + $page = $this->entities->page(); + $this->permissions->addEntityPermission($page, ['view'], $this->users->createRole()); + $page->owned_by = null; + $page->save(); + + $this->actingAsApiAdmin(); + $resp = $this->putJson($this->baseEndpoint . "/page/{$page->id}", [ + 'role_permissions' => [], + ]); + + $resp->assertOk(); + $resp->assertExactJson([ + 'owner' => null, + 'role_permissions' => [], + 'fallback_permissions' => [ + 'inheriting' => true, + 'view' => null, + 'create' => null, + 'update' => null, + 'delete' => null, + ], + ]); + } + + public function test_update_can_clear_fallback_permissions() + { + $page = $this->entities->page(); + $this->permissions->setFallbackPermissions($page, ['view', 'update']); + $page->owned_by = null; + $page->save(); + + $this->actingAsApiAdmin(); + $resp = $this->putJson($this->baseEndpoint . "/page/{$page->id}", [ + 'fallback_permissions' => [ + 'inheriting' => true, + ], + ]); + + $resp->assertOk(); + $resp->assertExactJson([ + 'owner' => null, + 'role_permissions' => [], + 'fallback_permissions' => [ + 'inheriting' => true, + 'view' => null, + 'create' => null, + 'update' => null, + 'delete' => null, + ], + ]); + } +} From 190392482992801e4bed18828ad328b07f7e572a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 13 Mar 2023 20:41:32 +0000 Subject: [PATCH 09/76] Added content-perms API examples and docs tweaks --- .../Api/ContentPermissionsController.php | 4 +- .../requests/content-permissions-update.json | 26 +++++++++++++ .../responses/content-permissions-read.json | 38 +++++++++++++++++++ .../responses/content-permissions-update.json | 38 +++++++++++++++++++ 4 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 dev/api/requests/content-permissions-update.json create mode 100644 dev/api/responses/content-permissions-read.json create mode 100644 dev/api/responses/content-permissions-update.json diff --git a/app/Http/Controllers/Api/ContentPermissionsController.php b/app/Http/Controllers/Api/ContentPermissionsController.php index 1db90f97a..ef17af8ad 100644 --- a/app/Http/Controllers/Api/ContentPermissionsController.php +++ b/app/Http/Controllers/Api/ContentPermissionsController.php @@ -54,13 +54,13 @@ class ContentPermissionsController extends ApiController } /** - * Update the configured content-level permissions for the item of the given type and ID. + * Update the configured content-level permission overrides for the item of the given type and ID. * 'contentType' should be one of: page, book, chapter, bookshelf. * 'contentId' should be the relevant ID of that item type you'd like to handle permissions for. * Providing an empty `role_permissions` array will remove any existing configured role permissions, * so you may want to fetch existing permissions beforehand if just adding/removing a single item. * You should completely omit the `owner_id`, `role_permissions` and/or the `fallback_permissions` properties - * if you don't wish to update details within those categories. + * from your request data if you don't wish to update details within those categories. */ public function update(Request $request, string $contentType, string $contentId) { diff --git a/dev/api/requests/content-permissions-update.json b/dev/api/requests/content-permissions-update.json new file mode 100644 index 000000000..124bb8bff --- /dev/null +++ b/dev/api/requests/content-permissions-update.json @@ -0,0 +1,26 @@ +{ + "owner_id": 1, + "role_permissions": [ + { + "role_id": 2, + "view": true, + "create": true, + "update": true, + "delete": false + }, + { + "role_id": 3, + "view": false, + "create": false, + "update": false, + "delete": false + } + ], + "fallback_permissions": { + "inheriting": false, + "view": true, + "create": true, + "update": false, + "delete": false + } +} \ No newline at end of file diff --git a/dev/api/responses/content-permissions-read.json b/dev/api/responses/content-permissions-read.json new file mode 100644 index 000000000..591fc5c86 --- /dev/null +++ b/dev/api/responses/content-permissions-read.json @@ -0,0 +1,38 @@ +{ + "owner": { + "id": 1, + "name": "Admin", + "slug": "admin" + }, + "role_permissions": [ + { + "role_id": 2, + "view": true, + "create": false, + "update": true, + "delete": false, + "role": { + "id": 2, + "display_name": "Editor" + } + }, + { + "role_id": 10, + "view": true, + "create": true, + "update": false, + "delete": false, + "role": { + "id": 10, + "display_name": "Wizards of the west" + } + } + ], + "fallback_permissions": { + "inheriting": false, + "view": true, + "create": false, + "update": false, + "delete": false + } +} \ No newline at end of file diff --git a/dev/api/responses/content-permissions-update.json b/dev/api/responses/content-permissions-update.json new file mode 100644 index 000000000..67fa40bf0 --- /dev/null +++ b/dev/api/responses/content-permissions-update.json @@ -0,0 +1,38 @@ +{ + "owner": { + "id": 1, + "name": "Admin", + "slug": "admin" + }, + "role_permissions": [ + { + "role_id": 2, + "view": true, + "create": true, + "update": true, + "delete": false, + "role": { + "id": 2, + "display_name": "Editor" + } + }, + { + "role_id": 3, + "view": false, + "create": false, + "update": false, + "delete": false, + "role": { + "id": 3, + "display_name": "Viewer" + } + } + ], + "fallback_permissions": { + "inheriting": false, + "view": true, + "create": true, + "update": false, + "delete": false + } +} \ No newline at end of file From 6357056d7b4e5e4a738bba2c416b86e54ab00533 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 13 Mar 2023 21:03:00 +0000 Subject: [PATCH 10/76] Updated php deps --- composer.json | 2 +- composer.lock | 247 ++++++++++++++++++++++++-------------------------- 2 files changed, 120 insertions(+), 129 deletions(-) diff --git a/composer.json b/composer.json index b1ac1789b..44143e042 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,7 @@ "nunomaduro/larastan": "^2.4", "phpunit/phpunit": "^9.5", "squizlabs/php_codesniffer": "^3.7", - "ssddanbrown/asserthtml": "^1.0" + "ssddanbrown/asserthtml": "^2.0" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 040e9ce04..bbdf4efa9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "64276cbeb1f79f4c94992cc739807d72", + "content-hash": "5a066407dfbd1809ffd39114a873333d", "packages": [ { "name": "aws/aws-crt-php", @@ -58,16 +58,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.260.3", + "version": "3.261.10", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "547b8047b2f9a551a7100b22e1abe1a3cc1b0ff0" + "reference": "4889eff2b3fe35e878fbcaf8374d73f043609170" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/547b8047b2f9a551a7100b22e1abe1a3cc1b0ff0", - "reference": "547b8047b2f9a551a7100b22e1abe1a3cc1b0ff0", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/4889eff2b3fe35e878fbcaf8374d73f043609170", + "reference": "4889eff2b3fe35e878fbcaf8374d73f043609170", "shasum": "" }, "require": { @@ -146,9 +146,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.260.3" + "source": "https://github.com/aws/aws-sdk-php/tree/3.261.10" }, - "time": "2023-02-24T19:25:34+00:00" + "time": "2023-03-13T18:19:14+00:00" }, { "name": "bacon/bacon-qr-code", @@ -417,21 +417,24 @@ }, { "name": "dasprid/enum", - "version": "1.0.3", + "version": "1.0.4", "source": { "type": "git", "url": "https://github.com/DASPRiD/Enum.git", - "reference": "5abf82f213618696dda8e3bf6f64dd042d8542b2" + "reference": "8e6b6ea76eabbf19ea2bf5b67b98e1860474012f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/5abf82f213618696dda8e3bf6f64dd042d8542b2", - "reference": "5abf82f213618696dda8e3bf6f64dd042d8542b2", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8e6b6ea76eabbf19ea2bf5b67b98e1860474012f", + "reference": "8e6b6ea76eabbf19ea2bf5b67b98e1860474012f", "shasum": "" }, + "require": { + "php": ">=7.1 <9.0" + }, "require-dev": { "phpunit/phpunit": "^7 | ^8 | ^9", - "squizlabs/php_codesniffer": "^3.4" + "squizlabs/php_codesniffer": "*" }, "type": "library", "autoload": { @@ -458,9 +461,9 @@ ], "support": { "issues": "https://github.com/DASPRiD/Enum/issues", - "source": "https://github.com/DASPRiD/Enum/tree/1.0.3" + "source": "https://github.com/DASPRiD/Enum/tree/1.0.4" }, - "time": "2020-10-02T16:03:48+00:00" + "time": "2023-03-01T18:44:03+00:00" }, { "name": "dflydev/dot-access-data", @@ -632,16 +635,16 @@ }, { "name": "doctrine/dbal", - "version": "3.6.0", + "version": "3.6.1", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "85b98cb23c8af471a67abfe14485da696bcabc2e" + "reference": "57815c7bbcda3cd18871d253c1dd8cbe56f8526e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/85b98cb23c8af471a67abfe14485da696bcabc2e", - "reference": "85b98cb23c8af471a67abfe14485da696bcabc2e", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/57815c7bbcda3cd18871d253c1dd8cbe56f8526e", + "reference": "57815c7bbcda3cd18871d253c1dd8cbe56f8526e", "shasum": "" }, "require": { @@ -657,11 +660,11 @@ "doctrine/coding-standard": "11.1.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2022.3", - "phpstan/phpstan": "1.9.14", - "phpstan/phpstan-strict-rules": "^1.4", - "phpunit/phpunit": "9.6.3", + "phpstan/phpstan": "1.10.3", + "phpstan/phpstan-strict-rules": "^1.5", + "phpunit/phpunit": "9.6.4", "psalm/plugin-phpunit": "0.18.4", - "squizlabs/php_codesniffer": "3.7.1", + "squizlabs/php_codesniffer": "3.7.2", "symfony/cache": "^5.4|^6.0", "symfony/console": "^4.4|^5.4|^6.0", "vimeo/psalm": "4.30.0" @@ -724,7 +727,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.6.0" + "source": "https://github.com/doctrine/dbal/tree/3.6.1" }, "funding": [ { @@ -740,7 +743,7 @@ "type": "tidelift" } ], - "time": "2023-02-07T22:52:03+00:00" + "time": "2023-03-02T19:26:24+00:00" }, { "name": "doctrine/deprecations", @@ -1309,24 +1312,24 @@ }, { "name": "graham-campbell/result-type", - "version": "v1.1.0", + "version": "v1.1.1", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "a878d45c1914464426dc94da61c9e1d36ae262a8" + "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/a878d45c1914464426dc94da61c9e1d36ae262a8", - "reference": "a878d45c1914464426dc94da61c9e1d36ae262a8", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", + "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9" + "phpoption/phpoption": "^1.9.1" }, "require-dev": { - "phpunit/phpunit": "^8.5.28 || ^9.5.21" + "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" }, "type": "library", "autoload": { @@ -1355,7 +1358,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.0" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.1" }, "funding": [ { @@ -1367,7 +1370,7 @@ "type": "tidelift" } ], - "time": "2022-07-30T15:56:11+00:00" + "time": "2023-02-25T20:23:15+00:00" }, { "name": "guzzlehttp/guzzle", @@ -1583,16 +1586,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.4.3", + "version": "2.4.4", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "67c26b443f348a51926030c83481b85718457d3d" + "reference": "3cf1b6d4f0c820a2cf8bcaec39fc698f3443b5cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/67c26b443f348a51926030c83481b85718457d3d", - "reference": "67c26b443f348a51926030c83481b85718457d3d", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/3cf1b6d4f0c820a2cf8bcaec39fc698f3443b5cf", + "reference": "3cf1b6d4f0c820a2cf8bcaec39fc698f3443b5cf", "shasum": "" }, "require": { @@ -1682,7 +1685,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.4.3" + "source": "https://github.com/guzzle/psr7/tree/2.4.4" }, "funding": [ { @@ -1698,7 +1701,7 @@ "type": "tidelift" } ], - "time": "2022-10-26T14:07:24+00:00" + "time": "2023-03-09T13:19:02+00:00" }, { "name": "guzzlehttp/uri-template", @@ -3462,16 +3465,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.15.3", + "version": "v4.15.4", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039" + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/570e980a201d8ed0236b0a62ddf2c9cbb2034039", - "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290", "shasum": "" }, "require": { @@ -3512,9 +3515,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.3" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4" }, - "time": "2023-01-16T22:05:37+00:00" + "time": "2023-03-05T19:49:14+00:00" }, { "name": "nunomaduro/termwind", @@ -3867,24 +3870,24 @@ }, { "name": "phpoption/phpoption", - "version": "1.9.0", + "version": "1.9.1", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "dc5ff11e274a90cc1c743f66c9ad700ce50db9ab" + "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/dc5ff11e274a90cc1c743f66c9ad700ce50db9ab", - "reference": "dc5ff11e274a90cc1c743f66c9ad700ce50db9ab", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/dd3a383e599f49777d8b628dadbb90cae435b87e", + "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8", - "phpunit/phpunit": "^8.5.28 || ^9.5.21" + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" }, "type": "library", "extra": { @@ -3926,7 +3929,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.0" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.1" }, "funding": [ { @@ -3938,20 +3941,20 @@ "type": "tidelift" } ], - "time": "2022-07-30T15:51:26+00:00" + "time": "2023-02-25T19:38:58+00:00" }, { "name": "phpseclib/phpseclib", - "version": "3.0.18", + "version": "3.0.19", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "f28693d38ba21bb0d9f0c411ee5dae2b178201da" + "reference": "cc181005cf548bfd8a4896383bb825d859259f95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/f28693d38ba21bb0d9f0c411ee5dae2b178201da", - "reference": "f28693d38ba21bb0d9f0c411ee5dae2b178201da", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/cc181005cf548bfd8a4896383bb825d859259f95", + "reference": "cc181005cf548bfd8a4896383bb825d859259f95", "shasum": "" }, "require": { @@ -4032,7 +4035,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.18" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.19" }, "funding": [ { @@ -4048,7 +4051,7 @@ "type": "tidelift" } ], - "time": "2022-12-17T18:26:50+00:00" + "time": "2023-03-05T17:13:09+00:00" }, { "name": "pragmarx/google2fa", @@ -4104,33 +4107,27 @@ }, { "name": "predis/predis", - "version": "v2.1.1", + "version": "v2.1.2", "source": { "type": "git", "url": "https://github.com/predis/predis.git", - "reference": "c5b60884e89630f9518a7919f0566db438f0fc9a" + "reference": "a77a43913a74f9331f637bb12867eb8e274814e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/predis/predis/zipball/c5b60884e89630f9518a7919f0566db438f0fc9a", - "reference": "c5b60884e89630f9518a7919f0566db438f0fc9a", + "url": "https://api.github.com/repos/predis/predis/zipball/a77a43913a74f9331f637bb12867eb8e274814e5", + "reference": "a77a43913a74f9331f637bb12867eb8e274814e5", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { + "friendsofphp/php-cs-fixer": "^3.3", + "phpstan/phpstan": "^1.9", "phpunit/phpunit": "^8.0 || ~9.4.4" }, - "suggest": { - "ext-curl": "Allows access to Webdis when paired with phpiredis" - }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.0-dev" - } - }, "autoload": { "psr-4": { "Predis\\": "src/" @@ -4145,12 +4142,6 @@ "name": "Till Krüss", "homepage": "https://till.im", "role": "Maintainer" - }, - { - "name": "Daniele Alessandri", - "email": "suppakilla@gmail.com", - "homepage": "http://clorophilla.net", - "role": "Creator" } ], "description": "A flexible and feature-complete Redis client for PHP.", @@ -4162,7 +4153,7 @@ ], "support": { "issues": "https://github.com/predis/predis/issues", - "source": "https://github.com/predis/predis/tree/v2.1.1" + "source": "https://github.com/predis/predis/tree/v2.1.2" }, "funding": [ { @@ -4170,7 +4161,7 @@ "type": "github" } ], - "time": "2023-01-17T20:57:35+00:00" + "time": "2023-03-02T18:32:04+00:00" }, { "name": "psr/cache", @@ -8043,16 +8034,16 @@ }, { "name": "filp/whoops", - "version": "2.14.6", + "version": "2.15.1", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "f7948baaa0330277c729714910336383286305da" + "reference": "e864ac957acd66e1565f25efda61e37791a5db0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/f7948baaa0330277c729714910336383286305da", - "reference": "f7948baaa0330277c729714910336383286305da", + "url": "https://api.github.com/repos/filp/whoops/zipball/e864ac957acd66e1565f25efda61e37791a5db0b", + "reference": "e864ac957acd66e1565f25efda61e37791a5db0b", "shasum": "" }, "require": { @@ -8102,7 +8093,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.14.6" + "source": "https://github.com/filp/whoops/tree/2.15.1" }, "funding": [ { @@ -8110,7 +8101,7 @@ "type": "github" } ], - "time": "2022-11-02T16:23:29+00:00" + "time": "2023-03-06T18:09:13+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -8305,16 +8296,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.11.0", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", "shasum": "" }, "require": { @@ -8352,7 +8343,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" }, "funding": [ { @@ -8360,7 +8351,7 @@ "type": "tidelift" } ], - "time": "2022-03-03T13:19:32+00:00" + "time": "2023-03-08T13:26:56+00:00" }, { "name": "nunomaduro/collision", @@ -8452,16 +8443,16 @@ }, { "name": "nunomaduro/larastan", - "version": "2.4.1", + "version": "2.5.1", "source": { "type": "git", "url": "https://github.com/nunomaduro/larastan.git", - "reference": "238fdbfba3aae133cdec73e99826c9b0232141f7" + "reference": "072e2c9566ae000bf66c92384fc933b81885244b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/larastan/zipball/238fdbfba3aae133cdec73e99826c9b0232141f7", - "reference": "238fdbfba3aae133cdec73e99826c9b0232141f7", + "url": "https://api.github.com/repos/nunomaduro/larastan/zipball/072e2c9566ae000bf66c92384fc933b81885244b", + "reference": "072e2c9566ae000bf66c92384fc933b81885244b", "shasum": "" }, "require": { @@ -8475,11 +8466,11 @@ "illuminate/support": "^9.47.0 || ^10.0.0", "php": "^8.0.2", "phpmyadmin/sql-parser": "^5.6.0", - "phpstan/phpstan": "^1.9.8" + "phpstan/phpstan": "~1.10.3" }, "require-dev": { "nikic/php-parser": "^4.15.2", - "orchestra/testbench": "^7.19.0|^8.0.0", + "orchestra/testbench": "^7.19.0 || ^8.0.0", "phpunit/phpunit": "^9.5.27" }, "suggest": { @@ -8524,7 +8515,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/larastan/issues", - "source": "https://github.com/nunomaduro/larastan/tree/2.4.1" + "source": "https://github.com/nunomaduro/larastan/tree/2.5.1" }, "funding": [ { @@ -8544,7 +8535,7 @@ "type": "patreon" } ], - "time": "2023-02-05T12:19:17+00:00" + "time": "2023-03-04T23:46:40+00:00" }, { "name": "phar-io/manifest", @@ -8746,16 +8737,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.3", + "version": "1.10.6", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "5419375b5891add97dc74be71e6c1c34baaddf64" + "reference": "50d089a3e0904b0fe7e2cf2d4fd37d427d64235a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/5419375b5891add97dc74be71e6c1c34baaddf64", - "reference": "5419375b5891add97dc74be71e6c1c34baaddf64", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/50d089a3e0904b0fe7e2cf2d4fd37d427d64235a", + "reference": "50d089a3e0904b0fe7e2cf2d4fd37d427d64235a", "shasum": "" }, "require": { @@ -8785,7 +8776,7 @@ ], "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.10.3" + "source": "https://github.com/phpstan/phpstan/tree/1.10.6" }, "funding": [ { @@ -8801,20 +8792,20 @@ "type": "tidelift" } ], - "time": "2023-02-25T14:47:13+00:00" + "time": "2023-03-09T16:55:12+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.25", + "version": "9.2.26", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "0e2b40518197a8c0d4b08bc34dfff1c99c508954" + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/0e2b40518197a8c0d4b08bc34dfff1c99c508954", - "reference": "0e2b40518197a8c0d4b08bc34dfff1c99c508954", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", "shasum": "" }, "require": { @@ -8836,8 +8827,8 @@ "phpunit/phpunit": "^9.3" }, "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", "extra": { @@ -8870,7 +8861,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.25" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26" }, "funding": [ { @@ -8878,7 +8869,7 @@ "type": "github" } ], - "time": "2023-02-25T05:32:00+00:00" + "time": "2023-03-06T12:58:08+00:00" }, { "name": "phpunit/php-file-iterator", @@ -9123,16 +9114,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.3", + "version": "9.6.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "e7b1615e3e887d6c719121c6d4a44b0ab9645555" + "reference": "86e761949019ae83f49240b2f2123fb5ab3b2fc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e7b1615e3e887d6c719121c6d4a44b0ab9645555", - "reference": "e7b1615e3e887d6c719121c6d4a44b0ab9645555", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/86e761949019ae83f49240b2f2123fb5ab3b2fc5", + "reference": "86e761949019ae83f49240b2f2123fb5ab3b2fc5", "shasum": "" }, "require": { @@ -9165,8 +9156,8 @@ "sebastian/version": "^3.0.2" }, "suggest": { - "ext-soap": "*", - "ext-xdebug": "*" + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "bin": [ "phpunit" @@ -9205,7 +9196,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.3" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.5" }, "funding": [ { @@ -9221,7 +9212,7 @@ "type": "tidelift" } ], - "time": "2023-02-04T13:37:15+00:00" + "time": "2023-03-09T06:34:10+00:00" }, { "name": "sebastian/cli-parser", @@ -10246,16 +10237,16 @@ }, { "name": "ssddanbrown/asserthtml", - "version": "v1.0.1", + "version": "v2.0.0", "source": { "type": "git", - "url": "https://github.com/ssddanbrown/htmlassert.git", - "reference": "f7d4352bb3d69347097b2841fd71934182821928" + "url": "https://github.com/ssddanbrown/asserthtml.git", + "reference": "6baf3ef2087f5928ae34f0d41db27aefcdf60414" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ssddanbrown/htmlassert/zipball/f7d4352bb3d69347097b2841fd71934182821928", - "reference": "f7d4352bb3d69347097b2841fd71934182821928", + "url": "https://api.github.com/repos/ssddanbrown/asserthtml/zipball/6baf3ef2087f5928ae34f0d41db27aefcdf60414", + "reference": "6baf3ef2087f5928ae34f0d41db27aefcdf60414", "shasum": "" }, "require": { @@ -10289,8 +10280,8 @@ "description": "HTML Content Assertions for PHPUnit", "homepage": "https://github.com/ssddanbrown/asserthtml", "support": { - "issues": "https://github.com/ssddanbrown/htmlassert/issues", - "source": "https://github.com/ssddanbrown/htmlassert/tree/v1.0.1" + "issues": "https://github.com/ssddanbrown/asserthtml/issues", + "source": "https://github.com/ssddanbrown/asserthtml/tree/v2.0.0" }, "funding": [ { @@ -10298,7 +10289,7 @@ "type": "github" } ], - "time": "2022-04-09T13:31:13+00:00" + "time": "2023-03-01T16:48:08+00:00" }, { "name": "symfony/dom-crawler", From d9eec6d82caf2c63c8535f6842612fc6939d5d0e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 14 Mar 2023 12:19:19 +0000 Subject: [PATCH 11/76] Started Image API build --- ...php => ContentPermissionApiController.php} | 2 +- .../Api/ImageGalleryApiController.php | 127 ++++++++++++++++++ .../Controllers/Api/RoleApiController.php | 6 +- .../Images/GalleryImageController.php | 11 +- app/Uploads/Image.php | 11 ++ routes/api.php | 13 +- 6 files changed, 155 insertions(+), 15 deletions(-) rename app/Http/Controllers/Api/{ContentPermissionsController.php => ContentPermissionApiController.php} (98%) create mode 100644 app/Http/Controllers/Api/ImageGalleryApiController.php diff --git a/app/Http/Controllers/Api/ContentPermissionsController.php b/app/Http/Controllers/Api/ContentPermissionApiController.php similarity index 98% rename from app/Http/Controllers/Api/ContentPermissionsController.php rename to app/Http/Controllers/Api/ContentPermissionApiController.php index ef17af8ad..47a0d3782 100644 --- a/app/Http/Controllers/Api/ContentPermissionsController.php +++ b/app/Http/Controllers/Api/ContentPermissionApiController.php @@ -7,7 +7,7 @@ use BookStack\Entities\Models\Entity; use BookStack\Entities\Tools\PermissionsUpdater; use Illuminate\Http\Request; -class ContentPermissionsController extends ApiController +class ContentPermissionApiController extends ApiController { public function __construct( protected PermissionsUpdater $permissionsUpdater, diff --git a/app/Http/Controllers/Api/ImageGalleryApiController.php b/app/Http/Controllers/Api/ImageGalleryApiController.php new file mode 100644 index 000000000..85c0c3cef --- /dev/null +++ b/app/Http/Controllers/Api/ImageGalleryApiController.php @@ -0,0 +1,127 @@ + [ + 'type' => ['required', 'string', 'in:gallery,drawio'], + 'uploaded_to' => ['required', 'integer', 'exists:pages,id'], + 'image' => ['required', 'file', ...$this->getImageValidationRules()], + 'name' => ['string', 'max:180'], + ], + 'update' => [ + 'name' => ['string', 'max:180'], + ] + ]; + } + + /** + * Get a listing of gallery images and drawings in the system. + * Requires visibility of the content they're originally uploaded to. + */ + public function list() + { + $images = Image::query()->scopes(['visible']) + ->select($this->fieldsToExpose) + ->whereIn('type', ['gallery', 'drawio']); + + return $this->apiListingResponse($images, [ + ...$this->fieldsToExpose + ]); + } + + /** + * Create a new image in the system. + */ + public function create(Request $request) + { + $data = $this->validate($request, $this->rules()['create']); + + $image = $this->imageRepo->saveNew($data['image'], $data['type'], $data['uploaded_to']); + + return response()->json($this->formatForSingleResponse($image)); + } + + /** + * View the details of a single image. + */ + public function read(string $id) + { + $image = $this->imageRepo->getById($id); + $this->checkOwnablePermission('page-view', $image->getPage()); + + return response()->json($this->formatForSingleResponse($image)); + } + + /** + * Update an existing image in the system. + */ + public function update(Request $request, string $id) + { + $data = $this->validate($request, $this->rules()['update']); + $image = $this->imageRepo->getById($id); + $this->checkOwnablePermission('page-view', $image->getPage()); + $this->checkOwnablePermission('image-update', $image); + + $this->imageRepo->updateImageDetails($image, $data); + + return response()->json($this->formatForSingleResponse($image)); + } + + /** + * Delete an image from the system. + */ + public function delete(string $id) + { + $image = $this->imageRepo->getById($id); + $this->checkOwnablePermission('page-view', $image->getPage()); + $this->checkOwnablePermission('image-delete', $image); + $this->imageRepo->destroyImage($image); + + return response('', 204); + } + + /** + * Format the given image model for single-result display. + */ + protected function formatForSingleResponse(Image $image): array + { + $this->imageRepo->loadThumbs($image); + $data = $image->getAttributes(); + $data['created_by'] = $image->createdBy; + $data['updated_by'] = $image->updatedBy; + $data['content'] = []; + + $escapedUrl = htmlentities($image->url); + $escapedName = htmlentities($image->name); + if ($image->type === 'drawio') { + $data['content']['html'] = "
id}\">
"; + $data['content']['markdown'] = $data['content']['html']; + } else { + $escapedDisplayThumb = htmlentities($image->thumbs['display']); + $data['content']['html'] = "\"{$escapedName}\""; + $mdEscapedName = str_replace(']', '', str_replace('[', '', $image->name)); + $mdEscapedThumb = str_replace(']', '', str_replace('[', '', $image->thumbs['display'])); + $data['content']['markdown'] = "![{$mdEscapedName}]({$mdEscapedThumb})"; + } + + return $data; + } +} diff --git a/app/Http/Controllers/Api/RoleApiController.php b/app/Http/Controllers/Api/RoleApiController.php index 4f78455e0..6986c73f7 100644 --- a/app/Http/Controllers/Api/RoleApiController.php +++ b/app/Http/Controllers/Api/RoleApiController.php @@ -88,10 +88,10 @@ class RoleApiController extends ApiController */ public function read(string $id) { - $user = $this->permissionsRepo->getRoleById($id); - $this->singleFormatter($user); + $role = $this->permissionsRepo->getRoleById($id); + $this->singleFormatter($role); - return response()->json($user); + return response()->json($role); } /** diff --git a/app/Http/Controllers/Images/GalleryImageController.php b/app/Http/Controllers/Images/GalleryImageController.php index 5484411d3..3f2f56265 100644 --- a/app/Http/Controllers/Images/GalleryImageController.php +++ b/app/Http/Controllers/Images/GalleryImageController.php @@ -10,14 +10,9 @@ use Illuminate\Validation\ValidationException; class GalleryImageController extends Controller { - protected $imageRepo; - - /** - * GalleryImageController constructor. - */ - public function __construct(ImageRepo $imageRepo) - { - $this->imageRepo = $imageRepo; + public function __construct( + protected ImageRepo $imageRepo + ) { } /** diff --git a/app/Uploads/Image.php b/app/Uploads/Image.php index c21a3b03f..038e7c199 100644 --- a/app/Uploads/Image.php +++ b/app/Uploads/Image.php @@ -3,9 +3,11 @@ namespace BookStack\Uploads; use BookStack\Auth\Permissions\JointPermission; +use BookStack\Auth\Permissions\PermissionApplicator; use BookStack\Entities\Models\Page; use BookStack\Model; use BookStack\Traits\HasCreatorAndUpdater; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -33,6 +35,15 @@ class Image extends Model ->where('joint_permissions.entity_type', '=', 'page'); } + /** + * Scope the query to just the images visible to the user based upon the + * user visibility of the uploaded_to page. + */ + public function scopeVisible(Builder $query): Builder + { + return app()->make(PermissionApplicator::class)->restrictPageRelationQuery($query, 'images', 'uploaded_to'); + } + /** * Get a thumbnail for this image. * diff --git a/routes/api.php b/routes/api.php index 1b852fed7..c809cdb3a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -13,7 +13,8 @@ use BookStack\Http\Controllers\Api\BookExportApiController; use BookStack\Http\Controllers\Api\BookshelfApiController; use BookStack\Http\Controllers\Api\ChapterApiController; use BookStack\Http\Controllers\Api\ChapterExportApiController; -use BookStack\Http\Controllers\Api\ContentPermissionsController; +use BookStack\Http\Controllers\Api\ContentPermissionApiController; +use BookStack\Http\Controllers\Api\ImageGalleryApiController; use BookStack\Http\Controllers\Api\PageApiController; use BookStack\Http\Controllers\Api\PageExportApiController; use BookStack\Http\Controllers\Api\RecycleBinApiController; @@ -63,6 +64,12 @@ Route::get('pages/{id}/export/pdf', [PageExportApiController::class, 'exportPdf' Route::get('pages/{id}/export/plaintext', [PageExportApiController::class, 'exportPlainText']); Route::get('pages/{id}/export/markdown', [PageExportApiController::class, 'exportMarkdown']); +Route::get('image-gallery', [ImageGalleryApiController::class, 'list']); +Route::post('image-gallery', [ImageGalleryApiController::class, 'create']); +Route::get('image-gallery/{id}', [ImageGalleryApiController::class, 'read']); +Route::put('image-gallery/{id}', [ImageGalleryApiController::class, 'update']); +Route::delete('image-gallery/{id}', [ImageGalleryApiController::class, 'delete']); + Route::get('search', [SearchApiController::class, 'all']); Route::get('shelves', [BookshelfApiController::class, 'list']); @@ -87,5 +94,5 @@ Route::get('recycle-bin', [RecycleBinApiController::class, 'list']); Route::put('recycle-bin/{deletionId}', [RecycleBinApiController::class, 'restore']); Route::delete('recycle-bin/{deletionId}', [RecycleBinApiController::class, 'destroy']); -Route::get('content-permissions/{contentType}/{contentId}', [ContentPermissionsController::class, 'read']); -Route::put('content-permissions/{contentType}/{contentId}', [ContentPermissionsController::class, 'update']); +Route::get('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'read']); +Route::put('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'update']); From 3a808fd76859a90cda0d6a4085bed053d7cabde1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 14 Mar 2023 19:29:08 +0000 Subject: [PATCH 12/76] Added phpunit tests to cover image API endpoints --- .../Api/ImageGalleryApiController.php | 13 +- app/Uploads/Image.php | 2 +- tests/Api/ImageGalleryApiTest.php | 347 ++++++++++++++++++ tests/Api/TestsApi.php | 21 +- 4 files changed, 375 insertions(+), 8 deletions(-) create mode 100644 tests/Api/ImageGalleryApiTest.php diff --git a/app/Http/Controllers/Api/ImageGalleryApiController.php b/app/Http/Controllers/Api/ImageGalleryApiController.php index 85c0c3cef..a9fb3b103 100644 --- a/app/Http/Controllers/Api/ImageGalleryApiController.php +++ b/app/Http/Controllers/Api/ImageGalleryApiController.php @@ -2,6 +2,7 @@ namespace BookStack\Http\Controllers\Api; +use BookStack\Entities\Models\Page; use BookStack\Uploads\Image; use BookStack\Uploads\ImageRepo; use Illuminate\Http\Request; @@ -22,7 +23,7 @@ class ImageGalleryApiController extends ApiController return [ 'create' => [ 'type' => ['required', 'string', 'in:gallery,drawio'], - 'uploaded_to' => ['required', 'integer', 'exists:pages,id'], + 'uploaded_to' => ['required', 'integer'], 'image' => ['required', 'file', ...$this->getImageValidationRules()], 'name' => ['string', 'max:180'], ], @@ -52,10 +53,17 @@ class ImageGalleryApiController extends ApiController */ public function create(Request $request) { + $this->checkPermission('image-create-all'); $data = $this->validate($request, $this->rules()['create']); + Page::visible()->findOrFail($data['uploaded_to']); $image = $this->imageRepo->saveNew($data['image'], $data['type'], $data['uploaded_to']); + if (isset($data['name'])) { + $image->refresh(); + $image->update(['name' => $data['name']]); + } + return response()->json($this->formatForSingleResponse($image)); } @@ -64,8 +72,7 @@ class ImageGalleryApiController extends ApiController */ public function read(string $id) { - $image = $this->imageRepo->getById($id); - $this->checkOwnablePermission('page-view', $image->getPage()); + $image = Image::query()->scopes(['visible'])->findOrFail($id); return response()->json($this->formatForSingleResponse($image)); } diff --git a/app/Uploads/Image.php b/app/Uploads/Image.php index 038e7c199..0ab0b612a 100644 --- a/app/Uploads/Image.php +++ b/app/Uploads/Image.php @@ -49,7 +49,7 @@ class Image extends Model * * @throws \Exception */ - public function getThumb(int $width, int $height, bool $keepRatio = false): string + public function getThumb(?int $width, ?int $height, bool $keepRatio = false): string { return app()->make(ImageService::class)->getThumbnail($this, $width, $height, $keepRatio); } diff --git a/tests/Api/ImageGalleryApiTest.php b/tests/Api/ImageGalleryApiTest.php new file mode 100644 index 000000000..17c90518c --- /dev/null +++ b/tests/Api/ImageGalleryApiTest.php @@ -0,0 +1,347 @@ +actingAsApiAdmin(); + $imagePage = $this->entities->page(); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + + $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id'); + $resp->assertJson(['data' => [ + [ + 'id' => $image->id, + 'name' => $image->name, + 'url' => $image->url, + 'path' => $image->path, + 'type' => 'gallery', + 'uploaded_to' => $imagePage->id, + 'created_by' => $this->users->admin()->id, + 'updated_by' => $this->users->admin()->id, + ], + ]]); + + $resp->assertJson(['total' => Image::query()->count()]); + } + + public function test_index_endpoint_doesnt_show_images_for_those_uploaded_to_non_visible_pages() + { + $this->actingAsApiEditor(); + $imagePage = $this->entities->page(); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + + $resp = $this->getJson($this->baseEndpoint . '?filter[id]=' . $image->id); + $resp->assertJsonCount(1, 'data'); + $resp->assertJson(['total' => 1]); + + $this->permissions->disableEntityInheritedPermissions($imagePage); + + $resp = $this->getJson($this->baseEndpoint . '?filter[id]=' . $image->id); + $resp->assertJsonCount(0, 'data'); + $resp->assertJson(['total' => 0]); + } + + public function test_index_endpoint_doesnt_show_other_image_types() + { + $this->actingAsApiEditor(); + $imagePage = $this->entities->page(); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + + $typesByCountExpectation = [ + 'cover_book' => 0, + 'drawio' => 1, + 'gallery' => 1, + 'user' => 0, + 'system' => 0, + ]; + + foreach ($typesByCountExpectation as $type => $count) { + $image->type = $type; + $image->save(); + + $resp = $this->getJson($this->baseEndpoint . '?filter[id]=' . $image->id); + $resp->assertJsonCount($count, 'data'); + $resp->assertJson(['total' => $count]); + } + } + + public function test_create_endpoint() + { + $this->actingAsApiAdmin(); + + $imagePage = $this->entities->page(); + $resp = $this->call('POST', $this->baseEndpoint, [ + 'type' => 'gallery', + 'uploaded_to' => $imagePage->id, + 'name' => 'My awesome image!', + ], [], [ + 'image' => $this->files->uploadedImage('my-cool-image.png'), + ]); + + $resp->assertStatus(200); + + $image = Image::query()->where('uploaded_to', '=', $imagePage->id)->first(); + $expectedUser = [ + 'id' => $this->users->admin()->id, + 'name' => $this->users->admin()->name, + 'slug' => $this->users->admin()->slug, + ]; + $resp->assertJson([ + 'id' => $image->id, + 'name' => 'My awesome image!', + 'url' => $image->url, + 'path' => $image->path, + 'type' => 'gallery', + 'uploaded_to' => $imagePage->id, + 'created_by' => $expectedUser, + 'updated_by' => $expectedUser, + ]); + } + + public function test_create_endpoint_requires_image_create_permissions() + { + $user = $this->users->editor(); + $this->actingAsForApi($user); + $this->permissions->removeUserRolePermissions($user, ['image-create-all']); + + $makeRequest = function () { + return $this->call('POST', $this->baseEndpoint, []); + }; + + $resp = $makeRequest(); + $resp->assertStatus(403); + + $this->permissions->grantUserRolePermissions($user, ['image-create-all']); + + $resp = $makeRequest(); + $resp->assertStatus(422); + } + + public function test_create_fails_if_uploaded_to_not_visible_or_not_exists() + { + $this->actingAsApiEditor(); + + $makeRequest = function (int $uploadedTo) { + return $this->call('POST', $this->baseEndpoint, [ + 'type' => 'gallery', + 'uploaded_to' => $uploadedTo, + 'name' => 'My awesome image!', + ], [], [ + 'image' => $this->files->uploadedImage('my-cool-image.png'), + ]); + }; + + $page = $this->entities->page(); + $this->permissions->disableEntityInheritedPermissions($page); + $resp = $makeRequest($page->id); + $resp->assertStatus(404); + + $resp = $makeRequest(Page::query()->max('id') + 55); + $resp->assertStatus(404); + } + + public function test_create_has_restricted_types() + { + $this->actingAsApiEditor(); + + $typesByStatusExpectation = [ + 'cover_book' => 422, + 'drawio' => 200, + 'gallery' => 200, + 'user' => 422, + 'system' => 422, + ]; + + $makeRequest = function (string $type) { + return $this->call('POST', $this->baseEndpoint, [ + 'type' => $type, + 'uploaded_to' => $this->entities->page()->id, + 'name' => 'My awesome image!', + ], [], [ + 'image' => $this->files->uploadedImage('my-cool-image.png'), + ]); + }; + + foreach ($typesByStatusExpectation as $type => $status) { + $resp = $makeRequest($type); + $resp->assertStatus($status); + } + } + + public function test_create_will_use_file_name_if_no_name_provided_in_request() + { + $this->actingAsApiEditor(); + + $imagePage = $this->entities->page(); + $resp = $this->call('POST', $this->baseEndpoint, [ + 'type' => 'gallery', + 'uploaded_to' => $imagePage->id, + ], [], [ + 'image' => $this->files->uploadedImage('my-cool-image.png'), + ]); + $resp->assertStatus(200); + + $this->assertDatabaseHas('images', [ + 'type' => 'gallery', + 'uploaded_to' => $imagePage->id, + 'name' => 'my-cool-image.png', + ]); + } + + public function test_read_endpoint() + { + $this->actingAsApiAdmin(); + $imagePage = $this->entities->page(); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + + $resp = $this->getJson($this->baseEndpoint . "/{$image->id}"); + $resp->assertStatus(200); + + $expectedUser = [ + 'id' => $this->users->admin()->id, + 'name' => $this->users->admin()->name, + 'slug' => $this->users->admin()->slug, + ]; + + $displayUrl = $image->getThumb(1680, null, true); + $resp->assertJson([ + 'id' => $image->id, + 'name' => $image->name, + 'url' => $image->url, + 'path' => $image->path, + 'type' => 'gallery', + 'uploaded_to' => $imagePage->id, + 'created_by' => $expectedUser, + 'updated_by' => $expectedUser, + 'content' => [ + 'html' => "url}\" target=\"_blank\">\"{$image-name}\">", + 'markdown' => "![{$image->name}]({$displayUrl})", + ], + ]); + $this->assertStringStartsWith('http://', $resp->json('thumbs.gallery')); + $this->assertStringStartsWith('http://', $resp->json('thumbs.display')); + } + + public function test_read_endpoint_provides_different_content_for_drawings() + { + $this->actingAsApiAdmin(); + $imagePage = $this->entities->page(); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + + $image->type = 'drawio'; + $image->save(); + + $resp = $this->getJson($this->baseEndpoint . "/{$image->id}"); + $resp->assertStatus(200); + + $drawing = "
id}\">url}\">
"; + $resp->assertJson([ + 'id' => $image->id, + 'content' => [ + 'html' => $drawing, + 'markdown' => $drawing, + ], + ]); + } + + public function test_read_endpoint_does_not_show_if_no_permissions_for_related_page() + { + $this->actingAsApiEditor(); + $imagePage = $this->entities->page(); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + + $this->permissions->disableEntityInheritedPermissions($imagePage); + + $resp = $this->getJson($this->baseEndpoint . "/{$image->id}"); + $resp->assertStatus(404); + } + + public function test_update_endpoint() + { + $this->actingAsApiAdmin(); + $imagePage = $this->entities->page(); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + + $resp = $this->putJson($this->baseEndpoint . "/{$image->id}", [ + 'name' => 'My updated image name!', + ]); + + $resp->assertStatus(200); + $resp->assertJson([ + 'id' => $image->id, + 'name' => 'My updated image name!', + ]); + $this->assertDatabaseHas('images', [ + 'id' => $image->id, + 'name' => 'My updated image name!', + ]); + } + + public function test_update_endpoint_requires_image_delete_permission() + { + $user = $this->users->editor(); + $this->actingAsForApi($user); + $imagePage = $this->entities->page(); + $this->permissions->removeUserRolePermissions($user, ['image-update-all', 'image-update-own']); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + + $resp = $this->putJson($this->baseEndpoint . "/{$image->id}", ['name' => 'My new name']); + $resp->assertStatus(403); + $resp->assertJson($this->permissionErrorResponse()); + + $this->permissions->grantUserRolePermissions($user, ['image-update-all']); + $resp = $this->putJson($this->baseEndpoint . "/{$image->id}", ['name' => 'My new name']); + $resp->assertStatus(200); + } + + public function test_delete_endpoint() + { + $this->actingAsApiAdmin(); + $imagePage = $this->entities->page(); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + $this->assertDatabaseHas('images', ['id' => $image->id]); + + $resp = $this->deleteJson($this->baseEndpoint . "/{$image->id}"); + + $resp->assertStatus(204); + $this->assertDatabaseMissing('images', ['id' => $image->id]); + } + + public function test_delete_endpoint_requires_image_delete_permission() + { + $user = $this->users->editor(); + $this->actingAsForApi($user); + $imagePage = $this->entities->page(); + $this->permissions->removeUserRolePermissions($user, ['image-delete-all', 'image-delete-own']); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + + $resp = $this->deleteJson($this->baseEndpoint . "/{$image->id}"); + $resp->assertStatus(403); + $resp->assertJson($this->permissionErrorResponse()); + + $this->permissions->grantUserRolePermissions($user, ['image-delete-all']); + $resp = $this->deleteJson($this->baseEndpoint . "/{$image->id}"); + $resp->assertStatus(204); + } +} diff --git a/tests/Api/TestsApi.php b/tests/Api/TestsApi.php index 501f28754..c566fd8de 100644 --- a/tests/Api/TestsApi.php +++ b/tests/Api/TestsApi.php @@ -2,15 +2,28 @@ namespace Tests\Api; +use BookStack\Auth\User; + trait TestsApi { - protected $apiTokenId = 'apitoken'; - protected $apiTokenSecret = 'password'; + protected string $apiTokenId = 'apitoken'; + protected string $apiTokenSecret = 'password'; + + /** + * Set the given user as the current logged-in user via the API driver. + * This does not ensure API access. The user may still lack required role permissions. + */ + protected function actingAsForApi(User $user): static + { + parent::actingAs($user, 'api'); + + return $this; + } /** * Set the API editor role as the current user via the API driver. */ - protected function actingAsApiEditor() + protected function actingAsApiEditor(): static { $this->actingAs($this->users->editor(), 'api'); @@ -20,7 +33,7 @@ trait TestsApi /** * Set the API admin role as the current user via the API driver. */ - protected function actingAsApiAdmin() + protected function actingAsApiAdmin(): static { $this->actingAs($this->users->admin(), 'api'); From 402eb845abe3312f6e6fe7611acd41541d8be245 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 15 Mar 2023 11:37:03 +0000 Subject: [PATCH 13/76] Added examples, updated docs for image gallery api endpoints --- .../Api/ImageGalleryApiController.php | 18 ++++++-- dev/api/requests/image-gallery-update.json | 3 ++ dev/api/responses/image-gallery-create.json | 28 +++++++++++++ dev/api/responses/image-gallery-list.json | 41 +++++++++++++++++++ dev/api/responses/image-gallery-read.json | 28 +++++++++++++ dev/api/responses/image-gallery-update.json | 28 +++++++++++++ .../api-docs/parts/getting-started.blade.php | 4 +- 7 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 dev/api/requests/image-gallery-update.json create mode 100644 dev/api/responses/image-gallery-create.json create mode 100644 dev/api/responses/image-gallery-list.json create mode 100644 dev/api/responses/image-gallery-read.json create mode 100644 dev/api/responses/image-gallery-update.json diff --git a/app/Http/Controllers/Api/ImageGalleryApiController.php b/app/Http/Controllers/Api/ImageGalleryApiController.php index a9fb3b103..3dba3d464 100644 --- a/app/Http/Controllers/Api/ImageGalleryApiController.php +++ b/app/Http/Controllers/Api/ImageGalleryApiController.php @@ -34,8 +34,8 @@ class ImageGalleryApiController extends ApiController } /** - * Get a listing of gallery images and drawings in the system. - * Requires visibility of the content they're originally uploaded to. + * Get a listing of images in the system. Includes gallery (page content) images and drawings. + * Requires visibility of the page they're originally uploaded to. */ public function list() { @@ -50,6 +50,11 @@ class ImageGalleryApiController extends ApiController /** * Create a new image in the system. + * Since "image" is expected to be a file, this needs to be a 'multipart/form-data' type request. + * The provided "uploaded_to" should be an existing page ID in the system. + * If the "name" parameter is omitted, the filename of the provided image file will be used instead. + * The "type" parameter should be 'gallery' for page content images, and 'drawio' should only be used + * when the file is a PNG file with diagrams.net image data embedded within. */ public function create(Request $request) { @@ -69,6 +74,10 @@ class ImageGalleryApiController extends ApiController /** * View the details of a single image. + * The "thumbs" response property contains links to scaled variants that BookStack may use in its UI. + * The "content" response property provides HTML and Markdown content, in the format that BookStack + * would typically use by default to add the image in page content, as a convenience. + * Actual image file data is not provided but can be fetched via the "url" response property. */ public function read(string $id) { @@ -78,7 +87,8 @@ class ImageGalleryApiController extends ApiController } /** - * Update an existing image in the system. + * Update the details of an existing image in the system. + * Only allows updating of the image name at this time. */ public function update(Request $request, string $id) { @@ -94,6 +104,8 @@ class ImageGalleryApiController extends ApiController /** * Delete an image from the system. + * Will also delete thumbnails for the image. + * Does not check or handle image usage so this could leave pages with broken image references. */ public function delete(string $id) { diff --git a/dev/api/requests/image-gallery-update.json b/dev/api/requests/image-gallery-update.json new file mode 100644 index 000000000..e332e3a8f --- /dev/null +++ b/dev/api/requests/image-gallery-update.json @@ -0,0 +1,3 @@ +{ + "name": "My updated image name" +} \ No newline at end of file diff --git a/dev/api/responses/image-gallery-create.json b/dev/api/responses/image-gallery-create.json new file mode 100644 index 000000000..e27824491 --- /dev/null +++ b/dev/api/responses/image-gallery-create.json @@ -0,0 +1,28 @@ +{ + "name": "cute-cat-image.png", + "path": "\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png", + "url": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png", + "type": "gallery", + "uploaded_to": 1, + "created_by": { + "id": 1, + "name": "Admin", + "slug": "admin" + }, + "updated_by": { + "id": 1, + "name": "Admin", + "slug": "admin" + }, + "updated_at": "2023-03-15 08:17:37", + "created_at": "2023-03-15 08:17:37", + "id": 618, + "thumbs": { + "gallery": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/thumbs-150-150\/cute-cat-image.png", + "display": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png" + }, + "content": { + "html": "\"cute-cat-image.png\"<\/a>", + "markdown": "![cute-cat-image.png](https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png)" + } +} \ No newline at end of file diff --git a/dev/api/responses/image-gallery-list.json b/dev/api/responses/image-gallery-list.json new file mode 100644 index 000000000..054d68a15 --- /dev/null +++ b/dev/api/responses/image-gallery-list.json @@ -0,0 +1,41 @@ +{ + "data": [ + { + "id": 1, + "name": "My cat scribbles", + "url": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-02\/scribbles.jpg", + "path": "\/uploads\/images\/gallery\/2023-02\/scribbles.jpg", + "type": "gallery", + "uploaded_to": 1, + "created_by": 1, + "updated_by": 1, + "created_at": "2023-02-12T16:34:57.000000Z", + "updated_at": "2023-02-12T16:34:57.000000Z" + }, + { + "id": 2, + "name": "Drawing-1.png", + "url": "https:\/\/bookstack.example.com\/uploads\/images\/drawio\/2023-02\/drawing-1.png", + "path": "\/uploads\/images\/drawio\/2023-02\/drawing-1.png", + "type": "drawio", + "uploaded_to": 2, + "created_by": 2, + "updated_by": 2, + "created_at": "2023-02-12T16:39:19.000000Z", + "updated_at": "2023-02-12T16:39:19.000000Z" + }, + { + "id": 8, + "name": "beans.jpg", + "url": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-02\/beans.jpg", + "path": "\/uploads\/images\/gallery\/2023-02\/beans.jpg", + "type": "gallery", + "uploaded_to": 6, + "created_by": 1, + "updated_by": 1, + "created_at": "2023-02-15T19:37:44.000000Z", + "updated_at": "2023-02-15T19:37:44.000000Z" + } + ], + "total": 3 +} \ No newline at end of file diff --git a/dev/api/responses/image-gallery-read.json b/dev/api/responses/image-gallery-read.json new file mode 100644 index 000000000..c6c468daa --- /dev/null +++ b/dev/api/responses/image-gallery-read.json @@ -0,0 +1,28 @@ +{ + "id": 618, + "name": "cute-cat-image.png", + "url": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png", + "created_at": "2023-03-15 08:17:37", + "updated_at": "2023-03-15 08:17:37", + "created_by": { + "id": 1, + "name": "Admin", + "slug": "admin" + }, + "updated_by": { + "id": 1, + "name": "Admin", + "slug": "admin" + }, + "path": "\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png", + "type": "gallery", + "uploaded_to": 1, + "thumbs": { + "gallery": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/thumbs-150-150\/cute-cat-image.png", + "display": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png" + }, + "content": { + "html": "\"cute-cat-image.png\"<\/a>", + "markdown": "![cute-cat-image.png](https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png)" + } +} \ No newline at end of file diff --git a/dev/api/responses/image-gallery-update.json b/dev/api/responses/image-gallery-update.json new file mode 100644 index 000000000..6e6168a1b --- /dev/null +++ b/dev/api/responses/image-gallery-update.json @@ -0,0 +1,28 @@ +{ + "id": 618, + "name": "My updated image name", + "url": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png", + "created_at": "2023-03-15 08:17:37", + "updated_at": "2023-03-15 08:24:50", + "created_by": { + "id": 1, + "name": "Admin", + "slug": "admin" + }, + "updated_by": { + "id": 1, + "name": "Admin", + "slug": "admin" + }, + "path": "\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png", + "type": "gallery", + "uploaded_to": 1, + "thumbs": { + "gallery": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/thumbs-150-150\/cute-cat-image.png", + "display": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png" + }, + "content": { + "html": "\"My<\/a>", + "markdown": "![My updated image name](https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png)" + } +} \ No newline at end of file diff --git a/resources/views/api-docs/parts/getting-started.blade.php b/resources/views/api-docs/parts/getting-started.blade.php index 7358b5cd7..75b71c6be 100644 --- a/resources/views/api-docs/parts/getting-started.blade.php +++ b/resources/views/api-docs/parts/getting-started.blade.php @@ -14,11 +14,11 @@ HTTP POST calls upon events occurring in BookStack.
  • - Visual Theme System - + Visual Theme System - Methods to override views, translations and icons within BookStack.
  • - Logical Theme System - + Logical Theme System - Methods to extend back-end functionality within BookStack.
  • From dce51234527d1a0075f04d81b619e3d00e14ed3a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 21 Mar 2023 20:53:35 +0000 Subject: [PATCH 14/76] Added own twig/smarty packages for cm6 lang support --- package-lock.json | 31 +++++++++++++++++++++++ package.json | 3 +++ resources/js/code/languages.js | 7 ++++-- resources/js/code/setups.js | 4 ++- resources/js/code/themes.js | 46 ++++++++++++++++++++++++++++++++++ resources/js/code/views.js | 2 +- 6 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 resources/js/code/themes.js diff --git a/package-lock.json b/package-lock.json index e8cd4f6db..85489c970 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "@codemirror/state": "^6.1.0", "@codemirror/theme-one-dark": "^6.0.0", "@codemirror/view": "^6.1.2", + "@ssddanbrown/codemirror-lang-smarty": "^1.0.0", + "@ssddanbrown/codemirror-lang-twig": "^1.0.0", "clipboard": "^2.0.11", "codemirror": "^6.0.1", "dropzone": "^5.9.3", @@ -22,6 +24,7 @@ "sortablejs": "^1.15.0" }, "devDependencies": { + "@lezer/generator": "^1.2.2", "chokidar-cli": "^3.0", "esbuild": "^0.17.3", "livereload": "^0.9.3", @@ -557,6 +560,19 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@lezer/generator": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@lezer/generator/-/generator-1.2.2.tgz", + "integrity": "sha512-O//eH9jTPM1GnbZruuD23xU68Pkuragonn1DEIom4Kt/eJN/QFt7Vzvp1YjV/XBmoUKC+2ySPgrA5fMF9FMM2g==", + "dev": true, + "dependencies": { + "@lezer/common": "^1.0.2", + "@lezer/lr": "^1.3.0" + }, + "bin": { + "lezer-generator": "dist/lezer-generator.cjs" + } + }, "node_modules/@lezer/highlight": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.3.tgz", @@ -610,6 +626,21 @@ "@lezer/lr": "^1.1.0" } }, + "node_modules/@ssddanbrown/codemirror-lang-smarty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@ssddanbrown/codemirror-lang-smarty/-/codemirror-lang-smarty-1.0.0.tgz", + "integrity": "sha512-F0ut1kmdbT3eORk3xVIKfQsGCZiQdh+6sLayBa0+FTex2gyIQlVQZRRA7bPSlchI3uZtWwNnqGNz5O/QLWRlFg==" + }, + "node_modules/@ssddanbrown/codemirror-lang-twig": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@ssddanbrown/codemirror-lang-twig/-/codemirror-lang-twig-1.0.0.tgz", + "integrity": "sha512-7WIMIh8Ssc54TooGCY57WU2rKEqZZrcV2tZSVRPtd0gKYsrDEKCSLWpQjUWEx7bdgh3NKHUjq1O4ugIzI/+dwQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", diff --git a/package.json b/package.json index 24f451a9c..6b6dbbe46 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "permissions": "chown -R $USER:$USER bootstrap/cache storage public/uploads" }, "devDependencies": { + "@lezer/generator": "^1.2.2", "chokidar-cli": "^3.0", "esbuild": "^0.17.3", "livereload": "^0.9.3", @@ -31,6 +32,8 @@ "@codemirror/state": "^6.1.0", "@codemirror/theme-one-dark": "^6.0.0", "@codemirror/view": "^6.1.2", + "@ssddanbrown/codemirror-lang-smarty": "^1.0.0", + "@ssddanbrown/codemirror-lang-twig": "^1.0.0", "clipboard": "^2.0.11", "codemirror": "^6.0.1", "dropzone": "^5.9.3", diff --git a/resources/js/code/languages.js b/resources/js/code/languages.js index 61aaeeefe..4a3591624 100644 --- a/resources/js/code/languages.js +++ b/resources/js/code/languages.js @@ -23,15 +23,16 @@ import {ruby} from '@codemirror/legacy-modes/mode/ruby'; import {rust} from '@codemirror/legacy-modes/mode/rust'; import {scheme} from '@codemirror/legacy-modes/mode/scheme'; import {shell} from '@codemirror/legacy-modes/mode/shell'; +import {smarty} from "@ssddanbrown/codemirror-lang-smarty"; import {standardSQL, pgSQL, msSQL, mySQL, sqlite, plSQL} from '@codemirror/legacy-modes/mode/sql'; import {stex} from '@codemirror/legacy-modes/mode/stex'; +import {swift} from "@codemirror/legacy-modes/mode/swift"; import {toml} from '@codemirror/legacy-modes/mode/toml'; -// import {twig, smarty} from '@codemirror/legacy-modes/mode/php'; // TODO +import {twig} from "@ssddanbrown/codemirror-lang-twig"; import {vb} from '@codemirror/legacy-modes/mode/vb'; import {vbScript} from '@codemirror/legacy-modes/mode/vbscript'; import {xml, html} from '@codemirror/legacy-modes/mode/xml'; import {yaml} from '@codemirror/legacy-modes/mode/yaml'; -import {swift} from "@codemirror/legacy-modes/mode/swift"; // Mapping of possible languages or formats from user input to their codemirror modes. @@ -97,10 +98,12 @@ const modeMap = { scheme: () => StreamLanguage.define(scheme), shell: () => StreamLanguage.define(shell), sh: () => StreamLanguage.define(shell), + smarty: () => StreamLanguage.define(smarty), stext: () => StreamLanguage.define(stex), swift: () => StreamLanguage.define(swift), toml: () => StreamLanguage.define(toml), ts: () => StreamLanguage.define(typescript), + twig: () => twig(), typescript: () => StreamLanguage.define(typescript), sql: () => StreamLanguage.define(standardSQL), sqlite: () => StreamLanguage.define(sqlite), diff --git a/resources/js/code/setups.js b/resources/js/code/setups.js index 45cc9c317..e1a150856 100644 --- a/resources/js/code/setups.js +++ b/resources/js/code/setups.js @@ -6,6 +6,8 @@ import {defaultHighlightStyle, syntaxHighlighting, bracketMatching, import {defaultKeymap, history, historyKeymap} from "@codemirror/commands" import {EditorState} from "@codemirror/state" +import {defaultLight} from "./themes"; + export function viewer() { return [ lineNumbers(), @@ -14,7 +16,7 @@ export function viewer() { history(), drawSelection(), dropCursor(), - syntaxHighlighting(defaultHighlightStyle, {fallback: true}), + syntaxHighlighting(defaultLight, {fallback: true}), bracketMatching(), rectangularSelection(), highlightActiveLine(), diff --git a/resources/js/code/themes.js b/resources/js/code/themes.js new file mode 100644 index 000000000..43feb2d53 --- /dev/null +++ b/resources/js/code/themes.js @@ -0,0 +1,46 @@ +import {tags} from "@lezer/highlight"; +import {HighlightStyle} from "@codemirror/language"; + +export const defaultLight = HighlightStyle.define([ + { tag: tags.meta, + color: "#388938" }, + { tag: tags.link, + textDecoration: "underline" }, + { tag: tags.heading, + textDecoration: "underline", + fontWeight: "bold" }, + { tag: tags.emphasis, + fontStyle: "italic" }, + { tag: tags.strong, + fontWeight: "bold" }, + { tag: tags.strikethrough, + textDecoration: "line-through" }, + { tag: tags.keyword, + color: "#708" }, + { tag: [tags.atom, tags.bool, tags.url, tags.contentSeparator, tags.labelName], + color: "#219" }, + { tag: [tags.literal, tags.inserted], + color: "#164" }, + { tag: [tags.string, tags.deleted], + color: "#a11" }, + { tag: [tags.regexp, tags.escape, tags.special(tags.string)], + color: "#e40" }, + { tag: tags.definition(tags.variableName), + color: "#00f" }, + { tag: tags.local(tags.variableName), + color: "#30a" }, + { tag: [tags.typeName, tags.namespace], + color: "#085" }, + { tag: tags.className, + color: "#167" }, + { tag: [tags.special(tags.variableName), tags.macroName], + color: "#256" }, + { tag: tags.definition(tags.propertyName), + color: "#00c" }, + { tag: tags.compareOperator, + color: "#708" }, + { tag: tags.comment, + color: "#940" }, + { tag: tags.invalid, + color: "#f00" } +]); \ No newline at end of file diff --git a/resources/js/code/views.js b/resources/js/code/views.js index cada9a1d6..8202551b3 100644 --- a/resources/js/code/views.js +++ b/resources/js/code/views.js @@ -24,7 +24,7 @@ export function createView(config) { } /** - * Ge the theme extension to use for editor view instance. + * Get the theme extension to use for editor view instance. * @returns {Extension} */ function getTheme(viewParentEl) { From f2293a70f80a13231aea07c1fc24f14f3fc393f9 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Fri, 24 Mar 2023 09:34:37 +0100 Subject: [PATCH 15/76] Allow a user to disable peer check when using TLS/STARTTLS This is useful when developing and on Docker setups. Despite setting encryption to null, if a server supports STARTTLS with a self-signed certificate, the mailer try to upgrade the connection with STARTTLS. --- .env.example.complete | 3 ++- app/Config/mail.php | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.env.example.complete b/.env.example.complete index f81bccae4..8c0accf1d 100644 --- a/.env.example.complete +++ b/.env.example.complete @@ -79,6 +79,7 @@ MAIL_PORT=1025 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null +MAIL_VERIFY_PEER=true # Command to use when email is sent via sendmail MAIL_SENDMAIL_COMMAND="/usr/sbin/sendmail -bs" @@ -372,4 +373,4 @@ LOG_FAILED_LOGIN_CHANNEL=errorlog_plain_webserver # IP address '146.191.42.4' would result in '146.191.x.x' being logged. # For the IPv6 address '2001:db8:85a3:8d3:1319:8a2e:370:7348' this would result as: # '2001:db8:85a3:8d3:x:x:x:x' -IP_ADDRESS_PRECISION=4 \ No newline at end of file +IP_ADDRESS_PRECISION=4 diff --git a/app/Config/mail.php b/app/Config/mail.php index b57c152d9..6cd5ee28f 100644 --- a/app/Config/mail.php +++ b/app/Config/mail.php @@ -32,6 +32,7 @@ return [ 'encryption' => env('MAIL_ENCRYPTION', 'tls'), 'username' => env('MAIL_USERNAME'), 'password' => env('MAIL_PASSWORD'), + 'verify_peer' => env('MAIL_VERIFY_PEER', true), 'timeout' => null, 'local_domain' => env('MAIL_EHLO_DOMAIN'), ], From 0e43618dda5564da7b136c9c3d6b5ef3408dc494 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 24 Mar 2023 14:43:48 +0000 Subject: [PATCH 16/76] Fixed issue with user delete ownership not migrating Caused by input not being part of the submitted form. Updated test to ensure the input is within a form. For #4124 --- resources/views/users/delete.blade.php | 52 +++++++++++++------------- tests/User/UserManagementTest.php | 1 + 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/resources/views/users/delete.blade.php b/resources/views/users/delete.blade.php index b18c182eb..b2f08b641 100644 --- a/resources/views/users/delete.blade.php +++ b/resources/views/users/delete.blade.php @@ -5,40 +5,40 @@ @include('settings.parts.navbar', ['selected' => 'users']) -
    -

    {{ trans('settings.users_delete') }}

    +
    id}") }}" method="POST"> + {!! csrf_field() !!} -

    {{ trans('settings.users_delete_warning', ['userName' => $user->name]) }}

    +
    +

    {{ trans('settings.users_delete') }}

    + +

    {{ trans('settings.users_delete_warning', ['userName' => $user->name]) }}

    + + @if(userCan('users-manage')) +
    + +
    +
    + +

    {{ trans('settings.users_migrate_ownership_desc') }}

    +
    +
    + @include('form.user-select', ['name' => 'new_owner_id', 'user' => null]) +
    +
    + @endif - @if(userCan('users-manage'))
    -
    -
    - -

    {{ trans('settings.users_migrate_ownership_desc') }}

    -
    -
    - @include('form.user-select', ['name' => 'new_owner_id', 'user' => null]) -
    -
    - @endif - -
    - -
    -

    {{ trans('settings.users_delete_confirm') }}

    -
    - id}") }}" method="POST" class="text-right"> - {!! csrf_field() !!} - +
    +

    {{ trans('settings.users_delete_confirm') }}

    +
    -
    -
    +
    +
    @stop diff --git a/tests/User/UserManagementTest.php b/tests/User/UserManagementTest.php index af17db52b..d5cd7ac79 100644 --- a/tests/User/UserManagementTest.php +++ b/tests/User/UserManagementTest.php @@ -145,6 +145,7 @@ class UserManagementTest extends TestCase $resp = $this->asEditor()->get("settings/users/{$editor->id}/delete"); $resp->assertSee('Migrate Ownership'); + $this->withHtml($resp)->assertElementExists('form input[name="new_owner_id"]'); $resp->assertSee('new_owner_id'); } From 970088a8a14840ce5ec659dba54de5c9a4b0ff8d Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 24 Mar 2023 14:46:30 +0000 Subject: [PATCH 17/76] Updated php deps --- composer.lock | 51 +++++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/composer.lock b/composer.lock index bbdf4efa9..766f3d51b 100644 --- a/composer.lock +++ b/composer.lock @@ -58,16 +58,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.261.10", + "version": "3.262.0", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "4889eff2b3fe35e878fbcaf8374d73f043609170" + "reference": "f45eefe4735d5a16ecc44cfd9a6c29421ae1e802" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/4889eff2b3fe35e878fbcaf8374d73f043609170", - "reference": "4889eff2b3fe35e878fbcaf8374d73f043609170", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/f45eefe4735d5a16ecc44cfd9a6c29421ae1e802", + "reference": "f45eefe4735d5a16ecc44cfd9a6c29421ae1e802", "shasum": "" }, "require": { @@ -146,9 +146,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.261.10" + "source": "https://github.com/aws/aws-sdk-php/tree/3.262.0" }, - "time": "2023-03-13T18:19:14+00:00" + "time": "2023-03-23T18:21:18+00:00" }, { "name": "bacon/bacon-qr-code", @@ -1873,16 +1873,16 @@ }, { "name": "knplabs/knp-snappy", - "version": "v1.4.1", + "version": "v1.4.2", "source": { "type": "git", "url": "https://github.com/KnpLabs/snappy.git", - "reference": "5126fb5b335ec929a226314d40cd8dad497c3d67" + "reference": "b66f79334421c26d9c244427963fa2d92980b5d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/5126fb5b335ec929a226314d40cd8dad497c3d67", - "reference": "5126fb5b335ec929a226314d40cd8dad497c3d67", + "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/b66f79334421c26d9c244427963fa2d92980b5d3", + "reference": "b66f79334421c26d9c244427963fa2d92980b5d3", "shasum": "" }, "require": { @@ -1941,9 +1941,9 @@ ], "support": { "issues": "https://github.com/KnpLabs/snappy/issues", - "source": "https://github.com/KnpLabs/snappy/tree/v1.4.1" + "source": "https://github.com/KnpLabs/snappy/tree/v1.4.2" }, - "time": "2022-01-07T13:03:38+00:00" + "time": "2023-03-17T14:47:54+00:00" }, { "name": "laravel/framework", @@ -4578,16 +4578,16 @@ }, { "name": "psy/psysh", - "version": "v0.11.12", + "version": "v0.11.13", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "52cb7c47d403c31c0adc9bf7710fc355f93c20f7" + "reference": "722317c9f5627e588788e340f29b923e58f92f54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/52cb7c47d403c31c0adc9bf7710fc355f93c20f7", - "reference": "52cb7c47d403c31c0adc9bf7710fc355f93c20f7", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/722317c9f5627e588788e340f29b923e58f92f54", + "reference": "722317c9f5627e588788e340f29b923e58f92f54", "shasum": "" }, "require": { @@ -4648,9 +4648,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.11.12" + "source": "https://github.com/bobthecow/psysh/tree/v0.11.13" }, - "time": "2023-01-29T21:24:40+00:00" + "time": "2023-03-21T14:22:44+00:00" }, { "name": "ralouphie/getallheaders", @@ -8737,16 +8737,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.6", + "version": "1.10.8", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "50d089a3e0904b0fe7e2cf2d4fd37d427d64235a" + "reference": "0166aef76e066f0dd2adc2799bdadfa1635711e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/50d089a3e0904b0fe7e2cf2d4fd37d427d64235a", - "reference": "50d089a3e0904b0fe7e2cf2d4fd37d427d64235a", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0166aef76e066f0dd2adc2799bdadfa1635711e9", + "reference": "0166aef76e066f0dd2adc2799bdadfa1635711e9", "shasum": "" }, "require": { @@ -8775,8 +8775,11 @@ "static analysis" ], "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.10.6" + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" }, "funding": [ { @@ -8792,7 +8795,7 @@ "type": "tidelift" } ], - "time": "2023-03-09T16:55:12+00:00" + "time": "2023-03-24T10:28:16+00:00" }, { "name": "phpunit/php-code-coverage", From dde38e91b5fe63c5673d3c669b303a067228dc1b Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 25 Mar 2023 12:07:59 +0000 Subject: [PATCH 18/76] Fixed delete role failing with no migrate role provided For #4128 --- app/Http/Controllers/RoleController.php | 3 ++- tests/Permissions/RolesTest.php | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/RoleController.php b/app/Http/Controllers/RoleController.php index 135ba329f..6d397bdae 100644 --- a/app/Http/Controllers/RoleController.php +++ b/app/Http/Controllers/RoleController.php @@ -151,7 +151,8 @@ class RoleController extends Controller $this->checkPermission('user-roles-manage'); try { - $this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id', 0)); + $migrateRoleId = intval($request->get('migrate_role_id') ?: "0"); + $this->permissionsRepo->deleteRole($id, $migrateRoleId); } catch (PermissionsException $e) { $this->showErrorNotification($e->getMessage()); diff --git a/tests/Permissions/RolesTest.php b/tests/Permissions/RolesTest.php index d4d975dbd..971479e28 100644 --- a/tests/Permissions/RolesTest.php +++ b/tests/Permissions/RolesTest.php @@ -163,6 +163,22 @@ class RolesTest extends TestCase $this->assertEquals($this->user->id, $roleA->users()->first()->id); } + public function test_delete_with_empty_migrate_option_works() + { + $role = $this->users->attachNewRole($this->user); + + $this->assertCount(1, $role->users()->get()); + + $deletePage = $this->asAdmin()->get("/settings/roles/delete/$role->id"); + $this->withHtml($deletePage)->assertElementExists('select[name=migrate_role_id]'); + $resp = $this->asAdmin()->delete("/settings/roles/delete/$role->id", [ + 'migrate_role_id' => '', + ]); + + $resp->assertRedirect('/settings/roles'); + $this->assertDatabaseMissing('roles', ['id' => $role->id]); + } + public function test_entity_permissions_are_removed_on_delete() { /** @var Role $roleA */ From d353e87ca17c42b39ebab877b485f37e908db963 Mon Sep 17 00:00:00 2001 From: Daiki Urata <7nohe.urata@gmail.com> Date: Thu, 30 Mar 2023 17:58:17 +0900 Subject: [PATCH 19/76] Add WKHTMLTOPDF to .env.example.complete --- .env.example.complete | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.env.example.complete b/.env.example.complete index f81bccae4..0ac40a90b 100644 --- a/.env.example.complete +++ b/.env.example.complete @@ -372,4 +372,10 @@ LOG_FAILED_LOGIN_CHANNEL=errorlog_plain_webserver # IP address '146.191.42.4' would result in '146.191.x.x' being logged. # For the IPv6 address '2001:db8:85a3:8d3:1319:8a2e:370:7348' this would result as: # '2001:db8:85a3:8d3:x:x:x:x' -IP_ADDRESS_PRECISION=4 \ No newline at end of file +IP_ADDRESS_PRECISION=4 + +# Use wkhtmltopdf to generate PDF documents instead of dompdf. +# Set the path to the wkhtmltopdf binary +# Example: WKHTMLTOPDF=/home/user/bins/wkhtmltopdf +# Refer to https://www.bookstackapp.com/docs/admin/pdf-rendering/#using-wkhtmltopdf +WKHTMLTOPDF=false \ No newline at end of file From 4835a0dcb138bc443559656a2f6a197bec3efcb3 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 4 Apr 2023 10:44:38 +0100 Subject: [PATCH 20/76] Cleaned up old token services --- app/Auth/Access/EmailConfirmationService.php | 4 +- app/Auth/Access/UserInviteService.php | 6 +-- app/Auth/Access/UserTokenService.php | 44 +++---------------- .../Auth/ConfirmEmailController.php | 16 ++----- 4 files changed, 14 insertions(+), 56 deletions(-) diff --git a/app/Auth/Access/EmailConfirmationService.php b/app/Auth/Access/EmailConfirmationService.php index 9c357d95f..1873cad08 100644 --- a/app/Auth/Access/EmailConfirmationService.php +++ b/app/Auth/Access/EmailConfirmationService.php @@ -8,8 +8,8 @@ use BookStack\Notifications\ConfirmEmail; class EmailConfirmationService extends UserTokenService { - protected $tokenTable = 'email_confirmations'; - protected $expiryTime = 24; + protected string $tokenTable = 'email_confirmations'; + protected int $expiryTime = 24; /** * Create new confirmation for a user, diff --git a/app/Auth/Access/UserInviteService.php b/app/Auth/Access/UserInviteService.php index d884cd636..191a03dd5 100644 --- a/app/Auth/Access/UserInviteService.php +++ b/app/Auth/Access/UserInviteService.php @@ -7,14 +7,12 @@ use BookStack\Notifications\UserInvite; class UserInviteService extends UserTokenService { - protected $tokenTable = 'user_invites'; - protected $expiryTime = 336; // Two weeks + protected string $tokenTable = 'user_invites'; + protected int $expiryTime = 336; // Two weeks /** * Send an invitation to a user to sign into BookStack * Removes existing invitation tokens. - * - * @param User $user */ public function sendInvitation(User $user) { diff --git a/app/Auth/Access/UserTokenService.php b/app/Auth/Access/UserTokenService.php index ffd828ab5..8dfe570f9 100644 --- a/app/Auth/Access/UserTokenService.php +++ b/app/Auth/Access/UserTokenService.php @@ -14,41 +14,29 @@ class UserTokenService { /** * Name of table where user tokens are stored. - * - * @var string */ - protected $tokenTable = 'user_tokens'; + protected string $tokenTable = 'user_tokens'; /** * Token expiry time in hours. - * - * @var int */ - protected $expiryTime = 24; + protected int $expiryTime = 24; /** - * Delete all email confirmations that belong to a user. - * - * @param User $user - * - * @return mixed + * Delete all tokens that belong to a user. */ - public function deleteByUser(User $user) + public function deleteByUser(User $user): void { - return DB::table($this->tokenTable) + DB::table($this->tokenTable) ->where('user_id', '=', $user->id) ->delete(); } /** - * Get the user id from a token, while check the token exists and has not expired. - * - * @param string $token + * Get the user id from a token, while checking the token exists and has not expired. * * @throws UserTokenNotFoundException * @throws UserTokenExpiredException - * - * @return int */ public function checkTokenAndGetUserId(string $token): int { @@ -67,8 +55,6 @@ class UserTokenService /** * Creates a unique token within the email confirmation database. - * - * @return string */ protected function generateToken(): string { @@ -82,10 +68,6 @@ class UserTokenService /** * Generate and store a token for the given user. - * - * @param User $user - * - * @return string */ protected function createTokenForUser(User $user): string { @@ -102,10 +84,6 @@ class UserTokenService /** * Check if the given token exists. - * - * @param string $token - * - * @return bool */ protected function tokenExists(string $token): bool { @@ -115,12 +93,8 @@ class UserTokenService /** * Get a token entry for the given token. - * - * @param string $token - * - * @return object|null */ - protected function getEntryByToken(string $token) + protected function getEntryByToken(string $token): ?stdClass { return DB::table($this->tokenTable) ->where('token', '=', $token) @@ -129,10 +103,6 @@ class UserTokenService /** * Check if the given token entry has expired. - * - * @param stdClass $tokenEntry - * - * @return bool */ protected function entryExpired(stdClass $tokenEntry): bool { diff --git a/app/Http/Controllers/Auth/ConfirmEmailController.php b/app/Http/Controllers/Auth/ConfirmEmailController.php index b282d0601..fdde8e70c 100644 --- a/app/Http/Controllers/Auth/ConfirmEmailController.php +++ b/app/Http/Controllers/Auth/ConfirmEmailController.php @@ -14,21 +14,11 @@ use Illuminate\Http\Request; class ConfirmEmailController extends Controller { - protected EmailConfirmationService $emailConfirmationService; - protected LoginService $loginService; - protected UserRepo $userRepo; - - /** - * Create a new controller instance. - */ public function __construct( - EmailConfirmationService $emailConfirmationService, - LoginService $loginService, - UserRepo $userRepo + protected EmailConfirmationService $emailConfirmationService, + protected LoginService $loginService, + protected UserRepo $userRepo ) { - $this->emailConfirmationService = $emailConfirmationService; - $this->loginService = $loginService; - $this->userRepo = $userRepo; } /** From 95b75c067fbcf2fd15b7cbbb503346af485ef9c6 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 7 Apr 2023 17:59:34 +0100 Subject: [PATCH 21/76] Updated translations with latest Crowdin changes (#4131) --- lang/bg/activities.php | 16 +++--- lang/bg/auth.php | 26 ++++----- lang/bg/common.php | 70 ++++++++++++------------ lang/bg/editor.php | 66 +++++++++++------------ lang/bg/errors.php | 2 +- lang/bg/preferences.php | 8 +-- lang/es_AR/settings.php | 4 +- lang/eu/entities.php | 14 ++--- lang/fa/activities.php | 6 +-- lang/fa/auth.php | 8 +-- lang/fa/common.php | 2 +- lang/fa/editor.php | 4 +- lang/fa/entities.php | 2 +- lang/fa/preferences.php | 20 +++---- lang/fr/entities.php | 2 +- lang/fr/settings.php | 20 +++---- lang/nb/entities.php | 54 +++++++++---------- lang/nb/errors.php | 12 ++--- lang/nb/preferences.php | 20 +++---- lang/nb/settings.php | 110 +++++++++++++++++++------------------- lang/nb/validation.php | 2 +- lang/pl/settings.php | 4 +- lang/pt_BR/activities.php | 6 +-- lang/pt_BR/errors.php | 2 +- 24 files changed, 240 insertions(+), 240 deletions(-) diff --git a/lang/bg/activities.php b/lang/bg/activities.php index af7497e12..5f281acb5 100644 --- a/lang/bg/activities.php +++ b/lang/bg/activities.php @@ -17,18 +17,18 @@ return [ 'page_move' => 'преместена страница', // Chapters - 'chapter_create' => 'създадена страница', - 'chapter_create_notification' => 'Главата е добавена успешно', + 'chapter_create' => 'създадена глава', + 'chapter_create_notification' => 'Успешно създадена глава', 'chapter_update' => 'обновена глава', - 'chapter_update_notification' => 'Главата е обновена успешно', + 'chapter_update_notification' => 'Успешно обновена глава', 'chapter_delete' => 'изтрита глава', - 'chapter_delete_notification' => 'Главата е изтрита успешно', + 'chapter_delete_notification' => 'Успешно изтрита глава', 'chapter_move' => 'преместена глава', // Books 'book_create' => 'създадена книга', 'book_create_notification' => 'Книгата е създадена успешно', - 'book_create_from_chapter' => 'converted chapter to book', + 'book_create_from_chapter' => 'превърната глава в книга', 'book_create_from_chapter_notification' => 'Chapter successfully converted to a book', 'book_update' => 'обновена книга', 'book_update_notification' => 'Книгата е обновена успешно', @@ -68,9 +68,9 @@ return [ 'user_delete_notification' => 'Потребителят е премахнат успешно', // Roles - 'role_create_notification' => 'Role successfully created', - 'role_update_notification' => 'Role successfully updated', - 'role_delete_notification' => 'Role successfully deleted', + 'role_create_notification' => 'Успешна създадена роля', + 'role_update_notification' => 'Успешно обновена роля', + 'role_delete_notification' => 'Успешно изтрита роля', // Other 'commented_on' => 'коментирано на', diff --git a/lang/bg/auth.php b/lang/bg/auth.php index dd3c1566a..ec3e75351 100644 --- a/lang/bg/auth.php +++ b/lang/bg/auth.php @@ -25,23 +25,23 @@ return [ 'forgot_password' => 'Забравена парола?', 'remember_me' => 'Запомни ме', 'ldap_email_hint' => 'Моля въведете емейл, който да използвате за дадения профил.', - 'create_account' => 'Създай Акаунт', + 'create_account' => 'Създаване на акаунт', 'already_have_account' => 'Вече имате профил?', - 'dont_have_account' => 'Нямате акаунт?', + 'dont_have_account' => 'Нямате ли акаунт?', 'social_login' => 'Влизане по друг начин', 'social_registration' => 'Регистрация по друг начин', - 'social_registration_text' => 'Регистрация и вписване чрез друга услуга.', + 'social_registration_text' => 'Регистриране и влизане посредством друга услуга.', - 'register_thanks' => 'Благодарим Ви за регистрацията!', + 'register_thanks' => 'Благодарности за регистрирането!', 'register_confirm' => 'Моля, провери своя имейл адрес и натисни бутона за потвърждение, за да достъпиш :appName.', 'registrations_disabled' => 'Регистрациите към момента са забранени', 'registration_email_domain_invalid' => 'Този емейл домейн към момента няма достъп до приложението', 'register_success' => 'Благодарим Ви за регистрацията! В момента сте регистриран и сте вписани в приложението.', // Login auto-initiation - 'auto_init_starting' => 'Attempting Login', - 'auto_init_starting_desc' => 'We\'re contacting your authentication system to start the login process. If there\'s no progress after 5 seconds you can try clicking the link below.', - 'auto_init_start_link' => 'Proceed with authentication', + 'auto_init_starting' => 'Опит за вход в системата', + 'auto_init_starting_desc' => 'Свързахме системата ви за удостоверяване към началото на процеса при влизане. Ако няма напредък след 5 секунди, то може да опитате да щракнете върху долната връзка.', + 'auto_init_start_link' => 'Продължаване с удостоверяването', // Password Reset 'reset_password' => 'Нулиране на паролата', @@ -56,13 +56,13 @@ return [ // Email Confirmation 'email_confirm_subject' => 'Потвърди емейла си за :appName', 'email_confirm_greeting' => 'Благодарим Ви, че се присъединихте към :appName!', - 'email_confirm_text' => 'Моля, потвърдете вашия имейл адрес, като следвате връзката по-долу:', + 'email_confirm_text' => 'Потвърдете адреса на имейла си, щраквайки върху връзката по-долу:', 'email_confirm_action' => 'Потвърдете имейл', 'email_confirm_send_error' => 'Нужно ви е потвърждение чрез емейл, но системата не успя да го изпрати. Моля свържете се с администратора, за да проверите дали вашият емейл адрес е конфигуриран правилно.', 'email_confirm_success' => 'Имейлът ти е потвърден! Вече би трябвало да можеш да се впишеш с този имейл адрес.', - 'email_confirm_resent' => 'Беше изпратен имейл с потвърждение, Моля, проверете кутията си.', - 'email_confirm_thanks' => 'Thanks for confirming!', - 'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.', + 'email_confirm_resent' => 'Е-писмо за потвърждение е изпратено пак, проверете кутията си.', + 'email_confirm_thanks' => 'Благодарности за потвърждаването!', + 'email_confirm_thanks_desc' => 'Почакайте малко, обработвайки потвърждението ви. Ако не сте пренасочени след 3 секунди, то натиснете долу връзката "Продължаване", за да продължите.', 'email_not_confirmed' => 'Имейл адресът не е потвърден', 'email_not_confirmed_text' => 'Вашият емейл адрес все още не е потвърден.', @@ -74,9 +74,9 @@ return [ 'user_invite_email_subject' => 'Вие бяхте поканен да се присъедините към :appName!', 'user_invite_email_greeting' => 'Беше създаден акаунт за Вас във :appName.', 'user_invite_email_text' => 'Натисните бутона по-долу за да определите парола и да получите достъп:', - 'user_invite_email_action' => 'Парола на акаунта', + 'user_invite_email_action' => 'Задаване на парола на акаунта', 'user_invite_page_welcome' => 'Добре дошли в :appName!', - 'user_invite_page_text' => 'За да финализирате вашият акаунт и да получите достъп трябва да определите парола, която да бъде използвана за следващия влизания в :appName.', + 'user_invite_page_text' => 'За да довършвам окончателно акаунта ви и да получите достъп трябва да зададете парола, която ще се използва за влизане в :appName при бъдещи посещения.', 'user_invite_page_confirm_button' => 'Потвърди паролата', 'user_invite_success_login' => 'Паролата е настроена, вече можеш да се впишеш с новата парола, за да достъпиш :appName!', diff --git a/lang/bg/common.php b/lang/bg/common.php index 0b065ecc9..7d8db8953 100644 --- a/lang/bg/common.php +++ b/lang/bg/common.php @@ -6,11 +6,11 @@ return [ // Buttons 'cancel' => 'Отказ', - 'confirm' => 'Потвърди', + 'confirm' => 'Потвърждаване', 'back' => 'Назад', - 'save' => 'Запази', - 'continue' => 'Продължи', - 'select' => 'Избери', + 'save' => 'Запис', + 'continue' => 'Продължаване', + 'select' => 'Изберете', 'toggle_all' => 'Избери всички', 'more' => 'Повече', @@ -18,38 +18,38 @@ return [ 'name' => 'Име', 'description' => 'Описание', 'role' => 'Роля', - 'cover_image' => 'Основно изображение', - 'cover_image_description' => 'Картината трябва да е приблизително 440х250 пиксела.', + 'cover_image' => 'Образ на корицата', + 'cover_image_description' => 'Образът трябва да е горе-долу 440х250 пиксела.', // Actions 'actions' => 'Действия', 'view' => 'Преглед', 'view_all' => 'Преглед на всички', - 'new' => 'New', - 'create' => 'Създай', + 'new' => 'Ново', + 'create' => 'Създаване', 'update' => 'Обновяване', 'edit' => 'Редактиране', 'sort' => 'Сортиране', 'move' => 'Преместване', - 'copy' => 'Копирай', - 'reply' => 'Отговори', - 'delete' => 'Изтрий', - 'delete_confirm' => 'Потвърдете изтриването', - 'search' => 'Търси', - 'search_clear' => 'Изчисти търсенето', - 'reset' => 'Нулирай', + 'copy' => 'Копиране', + 'reply' => 'Отговор', + 'delete' => 'Изтриване', + 'delete_confirm' => 'Потвърждаване на изтриването', + 'search' => 'Търсене', + 'search_clear' => 'Изчистване на търсенето', + 'reset' => 'Нулиране', 'remove' => 'Премахване', - 'add' => 'Добави', - 'configure' => 'Конфигурирай', - 'fullscreen' => 'Пълен екран', - 'favourite' => 'Добави в любими', - 'unfavourite' => 'Премахни от любими', - 'next' => 'Следващ', + 'add' => 'Добавяне', + 'configure' => 'Конфигуриране', + 'fullscreen' => 'Цял екран', + 'favourite' => 'Любимо', + 'unfavourite' => 'Не е любимо', + 'next' => 'Напред', 'previous' => 'Предишен', 'filter_active' => 'Активен филтър:', - 'filter_clear' => 'Изчисти филтъра', - 'download' => 'Download', - 'open_in_tab' => 'Open in Tab', + 'filter_clear' => 'Изчистване на филтрите', + 'download' => 'Изтегляне', + 'open_in_tab' => 'Отваряне в раздел', // Sort Options 'sort_options' => 'Опции за сортиране', @@ -74,27 +74,27 @@ return [ 'list_view' => 'Изглед списък', 'default' => 'Основен', 'breadcrumb' => 'Трасиране', - 'status' => 'Статус', + 'status' => 'Състояние', 'status_active' => 'Активен', 'status_inactive' => 'Неактивен', 'never' => 'Никога', - 'none' => 'Няма', + 'none' => 'Нищо', // Header - 'homepage' => 'Homepage', + 'homepage' => 'Начална страница', 'header_menu_expand' => 'Разшири заглавното меню', - 'profile_menu' => 'Профил меню', - 'view_profile' => 'Разглеждане на профил', + 'profile_menu' => 'Меню на профила', + 'view_profile' => 'Преглед на профила', 'edit_profile' => 'Редактиране на профила', 'dark_mode' => 'Тъмен режим', 'light_mode' => 'Светъл режим', - 'global_search' => 'Global Search', + 'global_search' => 'Глобално търсене', // Layout tabs - 'tab_info' => 'Информация', - 'tab_info_label' => 'Таб: Покажи вторична информация', + 'tab_info' => 'Инфо.', + 'tab_info_label' => 'Раздел: показва вторична информация', 'tab_content' => 'Съдържание', - 'tab_content_label' => 'Таб: Покажи първично съдържание', + 'tab_content_label' => 'Раздел: Показва първично съдържание', // Email Content 'email_action_help' => 'Ако имате проблеми с бутона ":actionText" по-горе, копирайте и поставете URL адреса по-долу в уеб браузъра си:', @@ -102,6 +102,6 @@ return [ // Footer Link Options // Not directly used but available for convenience to users. - 'privacy_policy' => 'Лични данни', - 'terms_of_service' => 'Общи условия', + 'privacy_policy' => 'Политика за поверителност', + 'terms_of_service' => 'Условия на услугата', ]; diff --git a/lang/bg/editor.php b/lang/bg/editor.php index e47f629d4..f677afa63 100644 --- a/lang/bg/editor.php +++ b/lang/bg/editor.php @@ -9,22 +9,22 @@ return [ // General editor terms 'general' => 'Общи', 'advanced' => 'Разширени', - 'none' => 'Няма', - 'cancel' => 'Откажи', - 'save' => 'Запази', - 'close' => 'Затвори', - 'undo' => 'Отмени', - 'redo' => 'Преправи', + 'none' => 'Нищо', + 'cancel' => 'Отказ', + 'save' => 'Запис', + 'close' => 'Затваряне', + 'undo' => 'Отмяна', + 'redo' => 'Повтаряне', 'left' => 'Вляво', 'center' => 'По средата', 'right' => 'Вдясно', 'top' => 'Отгоре', 'middle' => 'Среда', 'bottom' => 'Отдолу', - 'width' => 'Широчина', + 'width' => 'Ширина', 'height' => 'Височина', 'More' => 'Още', - 'select' => 'Select...', + 'select' => 'Изберете...', // Toolbar 'formats' => 'Формати', @@ -50,9 +50,9 @@ return [ 'custom_color' => 'Цвят по избор', 'remove_color' => 'Премахване на цвят', 'background_color' => 'Фонов цвят', - 'align_left' => 'Приравни вляво', - 'align_center' => 'Приравни в центъра', - 'align_right' => 'Приравни вдясно', + 'align_left' => 'Подравняване отляво', + 'align_center' => 'Подравняване в средата', + 'align_right' => 'Подравняване отдясно', 'align_justify' => 'Justify', 'list_bullet' => 'Списък', 'list_numbered' => 'Номериран списък', @@ -60,38 +60,38 @@ return [ 'indent_increase' => 'Увеличаване на отстъпа', 'indent_decrease' => 'Намаляване на отстъпа', 'table' => 'Таблица', - 'insert_image' => 'Вмъкни изображение', - 'insert_image_title' => 'Вмъкни/редактирай изображение', - 'insert_link' => 'Вмъкни/редактирай връзка', + 'insert_image' => 'Вмъкване на образ', + 'insert_image_title' => 'Вмъкване/редактиране на образ', + 'insert_link' => 'Вмъкване/редактиране на връзка', 'insert_link_title' => 'Вмъкни/редактирай връзка', - 'insert_horizontal_line' => 'Вмъкни хоризонтална линия', - 'insert_code_block' => 'Въведи код', + 'insert_horizontal_line' => 'Вмъкване на хоризонтална линия', + 'insert_code_block' => 'Вмъкване на блок код', 'edit_code_block' => 'Edit code block', - 'insert_drawing' => 'Вмъкни/редактирай рисунка', + 'insert_drawing' => 'Вмъкване/редактиране на рисунка', 'drawing_manager' => 'Управление на рисунките', - 'insert_media' => 'Вмъкни/редактирай мултимедия', - 'insert_media_title' => 'Вмъкни/редактирай мултимедия', - 'clear_formatting' => 'Изчисти форматирането', + 'insert_media' => 'Вмъкване/редактиране на мултимедията', + 'insert_media_title' => 'Вмъкване/редактиране на мултимедията', + 'clear_formatting' => 'Изчистване на форматирането', 'source_code' => 'Изходен код', 'source_code_title' => 'Изходен код', 'fullscreen' => 'Цял екран', - 'image_options' => 'Настройки на изображението', + 'image_options' => 'Възможности на образа', // Tables - 'table_properties' => 'Настройки на таблицата', - 'table_properties_title' => 'Настройки на таблицата', - 'delete_table' => 'Изтрий таблицата', + 'table_properties' => 'Свойства на таблицата', + 'table_properties_title' => 'Свойства на таблица', + 'delete_table' => 'Изтриване на таблица', 'insert_row_before' => 'Вмъкни реда преди', 'insert_row_after' => 'Вмъкни реда след', - 'delete_row' => 'Изтрий реда', + 'delete_row' => 'Изтриване на ред', 'insert_column_before' => 'Вмъкни колоната преди', 'insert_column_after' => 'Вмъкни колоната след', 'delete_column' => 'Изтрий колоната', 'table_cell' => 'Клетка', 'table_row' => 'Ред', 'table_column' => 'Колона', - 'cell_properties' => 'Настройки на клетката', - 'cell_properties_title' => 'Настройки на клетката', + 'cell_properties' => 'Свойства на клетката', + 'cell_properties_title' => 'Свойства на клетката', 'cell_type' => 'Тип на клетката', 'cell_type_cell' => 'Клетка', 'cell_scope' => 'Scope', @@ -115,7 +115,7 @@ return [ 'row_type_header' => 'Заглавка', 'row_type_body' => 'Тяло', 'row_type_footer' => 'Долна част', - 'alignment' => 'Разположение', + 'alignment' => 'Подравняване', 'cut_column' => 'Изрежи колоната', 'copy_column' => 'Копирай колоната', 'paste_column_before' => 'Постави колоната преди', @@ -148,7 +148,7 @@ return [ 'open_link_in' => 'Open link in...', 'open_link_current' => 'Текущ прозорец', 'open_link_new' => 'Нов прозорец', - 'remove_link' => 'Remove link', + 'remove_link' => 'Премахване на връзка', 'insert_collapsible' => 'Вмъкни сгъваем блок', 'collapsible_unwrap' => 'Разгъни', 'edit_label' => 'Редактирай етикета', @@ -157,10 +157,10 @@ return [ 'toggle_label' => 'Превключи надписа', // About view - 'about' => 'About the editor', + 'about' => 'За редактора', 'about_title' => 'Относно визуалния редактор', 'editor_license' => 'Лиценз, авторски и сходни права на редактора', - 'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under the MIT license.', + 'editor_tiny_license' => 'Този редактор е изграден посредством :tinyLink, което е предоставен под лиценз MIT.', 'editor_tiny_license_link' => 'Авторското и сходните му права, както и лицензът на TinyMCE, могат да бъдат намерени тук.', 'save_continue' => 'Запази страницата и продължи', 'callouts_cycle' => '(Продължавай да натискаш, за да превключваш типовете)', @@ -168,7 +168,7 @@ return [ 'shortcuts' => 'Преки пътища', 'shortcut' => 'Пряк път', 'shortcuts_intro' => 'Следните клавишни комбинации са налични за редактора:', - 'windows_linux' => '(Windows/Linux)', - 'mac' => '(Mac)', + 'windows_linux' => '(Уиндоус/Линукс)', + 'mac' => '(Мак.)', 'description' => 'Описание', ]; diff --git a/lang/bg/errors.php b/lang/bg/errors.php index 5d927ea5e..fe83dabe1 100644 --- a/lang/bg/errors.php +++ b/lang/bg/errors.php @@ -61,7 +61,7 @@ return [ // Entities 'entity_not_found' => 'Обектът не е намерен', - 'bookshelf_not_found' => 'Shelf not found', + 'bookshelf_not_found' => 'Няма намерен рафт', 'book_not_found' => 'Книгата не е намерена', 'page_not_found' => 'Страницата не е намерена', 'chapter_not_found' => 'Главата не е намерена', diff --git a/lang/bg/preferences.php b/lang/bg/preferences.php index e9a47461b..b38f35e48 100644 --- a/lang/bg/preferences.php +++ b/lang/bg/preferences.php @@ -5,14 +5,14 @@ */ return [ - 'shortcuts' => 'Shortcuts', + 'shortcuts' => 'Преки пътища', 'shortcuts_interface' => 'Interface Keyboard Shortcuts', 'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.', 'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.', 'shortcuts_toggle_label' => 'Keyboard shortcuts enabled', - 'shortcuts_section_navigation' => 'Navigation', + 'shortcuts_section_navigation' => 'Навигация', 'shortcuts_section_actions' => 'Common Actions', - 'shortcuts_save' => 'Save Shortcuts', + 'shortcuts_save' => 'Запазване на преките пътища', 'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.', - 'shortcuts_update_success' => 'Shortcut preferences have been updated!', + 'shortcuts_update_success' => 'Обновени предпочитания за преки пътища!', ]; \ No newline at end of file diff --git a/lang/es_AR/settings.php b/lang/es_AR/settings.php index 77d0e897e..7fc214c13 100644 --- a/lang/es_AR/settings.php +++ b/lang/es_AR/settings.php @@ -50,8 +50,8 @@ return [ // Color settings 'color_scheme' => 'Esquema de color de la aplicación', - 'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.', - 'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.', + 'color_scheme_desc' => 'Establece los colores a usar en la interfaz de BookStack. Los colores pueden configurarse por separado para que los modos oscuros y claros se ajusten mejor al tema y garanticen la legibilidad.', + 'ui_colors_desc' => 'Establece el color principal y el color de los enlaces para BookStack. El color principal se utiliza principalmente para la cabecera, botones y decoraciones de la interfaz. El color de los enlaces se utiliza para enlaces y acciones de texto, tanto dentro del contenido escrito como en la interfaz de Bookstack.', 'app_color' => 'Color principal', 'link_color' => 'Color de enlaces por defecto', 'content_colors_desc' => 'Establece los colores para todos los elementos en la jerarquía de la organización de la página. Se recomienda elegir colores con un brillo similar al predeterminado para mayor legibilidad.', diff --git a/lang/eu/entities.php b/lang/eu/entities.php index d8d98d917..260c87516 100644 --- a/lang/eu/entities.php +++ b/lang/eu/entities.php @@ -42,14 +42,14 @@ return [ // Permissions and restrictions 'permissions' => 'Baimenak', - 'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.', + 'permissions_desc' => 'Ezarri baimenak hemen, erabiltzaileen rolek ematen dituzten baimenak gainidazteko.', 'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.', 'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.', 'permissions_save' => 'Gorde baimenak', 'permissions_owner' => 'Jabea', 'permissions_role_everyone_else' => 'Everyone Else', 'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.', - 'permissions_role_override' => 'Override permissions for role', + 'permissions_role_override' => 'Gainidatzi baimenak rol honi', 'permissions_inherit_defaults' => 'Inherit defaults', // Search @@ -102,13 +102,13 @@ return [ 'shelves_delete_named' => 'Delete Shelf :name', 'shelves_delete_explain' => "This will delete the shelf with the name ':name'. Contained books will not be deleted.", 'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?', - 'shelves_permissions' => 'Shelf Permissions', - 'shelves_permissions_updated' => 'Shelf Permissions Updated', - 'shelves_permissions_active' => 'Shelf Permissions Active', - 'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', + 'shelves_permissions' => 'Apalategi baimenak', + 'shelves_permissions_updated' => 'Apalategi baimenak eguneratuta', + 'shelves_permissions_active' => 'Apalategi baimenak aktibatuta', + 'shelves_permissions_cascade_warning' => 'Apaletako baimenak ez dira automatikoki hauen barneko liburuetan gordeko. Liburu bat apalategi askotan egon daitekeelako. Hala ere, baimenak apalategiko liburutara kopiatu daitezke, behean agertzen den aukera erabiliz.', 'shelves_copy_permissions_to_books' => 'Kopiatu baimenak liburura', 'shelves_copy_permissions' => 'Gorde baimenak', - 'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.', + 'shelves_copy_permissions_explain' => 'Honek apalategi honen egungo baimen-konfigurazioa aplikatuko die barruan dauden liburu guztiei. Aktibatu aurretik, ziurtatu apaletan aldaketak gorde direla.', 'shelves_copy_permission_success' => 'Shelf permissions copied to :count books', // Books diff --git a/lang/fa/activities.php b/lang/fa/activities.php index 86daadcf3..2e61cce8d 100644 --- a/lang/fa/activities.php +++ b/lang/fa/activities.php @@ -68,9 +68,9 @@ return [ 'user_delete_notification' => 'کاربر با موفقیت حذف شد', // Roles - 'role_create_notification' => 'Role successfully created', - 'role_update_notification' => 'Role successfully updated', - 'role_delete_notification' => 'Role successfully deleted', + 'role_create_notification' => 'نقش با موفقیت ایجاد شد', + 'role_update_notification' => 'نقش با موفقیت به روز شد', + 'role_delete_notification' => 'نقش با موفقیت حذف شد', // Other 'commented_on' => 'ثبت دیدگاه', diff --git a/lang/fa/auth.php b/lang/fa/auth.php index 4de8b5800..f4b30d438 100644 --- a/lang/fa/auth.php +++ b/lang/fa/auth.php @@ -40,8 +40,8 @@ return [ // Login auto-initiation 'auto_init_starting' => 'تلاش برای ورود', - 'auto_init_starting_desc' => 'We\'re contacting your authentication system to start the login process. If there\'s no progress after 5 seconds you can try clicking the link below.', - 'auto_init_start_link' => 'Proceed with authentication', + 'auto_init_starting_desc' => 'برای شروع فرآیند ورود به سیستم با سیستم احراز هویت شما تماس می گیریم. اگر بعد از 5 ثانیه پیشرفتی حاصل نشد، می توانید روی لینک زیر کلیک کنید.', + 'auto_init_start_link' => 'احراز هویت را ادامه دهید', // Password Reset 'reset_password' => 'بازنشانی کلمه عبور', @@ -61,8 +61,8 @@ return [ 'email_confirm_send_error' => 'تایید پست الکترونیک الزامی می باشد، اما سیستم قادر به ارسال پیام نمی باشد.', 'email_confirm_success' => 'ایمیل شما تایید شد! اکنون باید بتوانید با استفاده از این آدرس ایمیل وارد شوید.', 'email_confirm_resent' => 'پیام تایید پست الکترونیک مجدد ارسال گردید، لطفا صندوق ورودی خود را بررسی نمایید.', - 'email_confirm_thanks' => 'Thanks for confirming!', - 'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.', + 'email_confirm_thanks' => 'تشکر بابت تایید!', + 'email_confirm_thanks_desc' => 'لطفاً یک لحظه صبر کنید تا تأیید شما بررسی شود. اگر بعد از 3 ثانیه هدایت نشدید، بر روی لینک "ادامه" کلیک کنید تا ادامه دهید.', 'email_not_confirmed' => 'پست الکترونیک تایید نشده است', 'email_not_confirmed_text' => 'پست الکترونیک شما هنوز تایید نشده است.', diff --git a/lang/fa/common.php b/lang/fa/common.php index bd6b95857..2cdca7342 100644 --- a/lang/fa/common.php +++ b/lang/fa/common.php @@ -88,7 +88,7 @@ return [ 'edit_profile' => 'ویرایش پروفایل', 'dark_mode' => 'حالت تاریک', 'light_mode' => 'حالت روشن', - 'global_search' => 'Global Search', + 'global_search' => 'جستجوی سراسری', // Layout tabs 'tab_info' => 'اطلاعات', diff --git a/lang/fa/editor.php b/lang/fa/editor.php index 0109e6cad..3e0be53b8 100644 --- a/lang/fa/editor.php +++ b/lang/fa/editor.php @@ -66,7 +66,7 @@ return [ 'insert_link_title' => 'افزودن/ویرایش پیوند', 'insert_horizontal_line' => 'افزودن خط افقی', 'insert_code_block' => 'افزودن بلوک کد', - 'edit_code_block' => 'Edit code block', + 'edit_code_block' => 'code block را ویرایش کنید', 'insert_drawing' => 'افزودن/ویرایش طرح', 'drawing_manager' => 'مدیریت طراحی', 'insert_media' => 'افزودن/ویرایش رسانه', @@ -165,7 +165,7 @@ return [ 'save_continue' => 'ذخیره صفحه و ادامه', 'callouts_cycle' => '(جهت تغییر نوع ها چندین بار فشار دهید)', 'link_selector' => 'پیوند به محتوا', - 'shortcuts' => 'میانبرها', + 'shortcuts' => 'کلیدهای میانبر', 'shortcut' => 'میانبر', 'shortcuts_intro' => 'میانبرهای قابل استفاده در این ویرایشگر:', 'windows_linux' => '(ویندوز/لینوکس)', diff --git a/lang/fa/entities.php b/lang/fa/entities.php index bc3961d4b..186c2e418 100644 --- a/lang/fa/entities.php +++ b/lang/fa/entities.php @@ -22,7 +22,7 @@ return [ 'meta_created_name' => 'ایجاد شده :timeLength توسط :user', 'meta_updated' => 'به روزرسانی شده :timeLength', 'meta_updated_name' => 'به روزرسانی شده :timeLength توسط :user', - 'meta_owned_name' => 'توسط :user ایجاد شده‌است', + 'meta_owned_name' => 'متعلق به :user', 'meta_reference_page_count' => 'Referenced on :count page|Referenced on :count pages', 'entity_select' => 'انتخاب موجودیت', 'entity_select_lack_permission' => 'شما مجوزهای لازم برای انتخاب این مورد را ندارید', diff --git a/lang/fa/preferences.php b/lang/fa/preferences.php index e9a47461b..d0f1598e1 100644 --- a/lang/fa/preferences.php +++ b/lang/fa/preferences.php @@ -5,14 +5,14 @@ */ return [ - 'shortcuts' => 'Shortcuts', - 'shortcuts_interface' => 'Interface Keyboard Shortcuts', - 'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.', - 'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.', - 'shortcuts_toggle_label' => 'Keyboard shortcuts enabled', - 'shortcuts_section_navigation' => 'Navigation', - 'shortcuts_section_actions' => 'Common Actions', - 'shortcuts_save' => 'Save Shortcuts', - 'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.', - 'shortcuts_update_success' => 'Shortcut preferences have been updated!', + 'shortcuts' => 'میانبرها', + 'shortcuts_interface' => 'میانبرهای صفحه کلید', + 'shortcuts_toggle_desc' => 'در اینجا می توانید میانبرهای سیستم را که برای پیمایش و ... استفاده می شود، فعال یا غیرفعال کنید.', + 'shortcuts_customize_desc' => 'می توانید هر یک از میانبرهای زیر را سفارشی کنید. کافی است پس از انتخاب ورودی برای میانبر، کلید ترکیبی مورد نظر خود را فشار دهید.', + 'shortcuts_toggle_label' => 'میانبرهای صفحه کلید فعال شد', + 'shortcuts_section_navigation' => 'ناوبری و پیمایش', + 'shortcuts_section_actions' => 'فعالیت/اقدامات مرسوم', + 'shortcuts_save' => 'ذخیره کلیدهای میانبر', + 'shortcuts_overlay_desc' => 'توجه: هنگامی که میانبرها فعال هستند، یک رابط کمکی با فشار دادن "؟" در دسترس است که میانبرهای موجود برای اقداماتی که در حال حاضر روی صفحه قابل مشاهده است را برجسته می‌کند.', + 'shortcuts_update_success' => 'تنظیمات میانبر به روز شده است!', ]; \ No newline at end of file diff --git a/lang/fr/entities.php b/lang/fr/entities.php index 8151251af..7ff76acf5 100644 --- a/lang/fr/entities.php +++ b/lang/fr/entities.php @@ -23,7 +23,7 @@ return [ 'meta_updated' => 'Mis à jour :timeLength', 'meta_updated_name' => 'Mis à jour :timeLength par :user', 'meta_owned_name' => 'Appartient à :user', - 'meta_reference_page_count' => 'Referenced on :count page|Referenced on :count pages', + 'meta_reference_page_count' => 'Référencé sur :count page|Référencé sur :count pages', 'entity_select' => 'Sélectionner l\'entité', 'entity_select_lack_permission' => 'Vous n\'avez pas les permissions requises pour sélectionner cet élément', 'images' => 'Images', diff --git a/lang/fr/settings.php b/lang/fr/settings.php index 770b913f9..8b13d4211 100644 --- a/lang/fr/settings.php +++ b/lang/fr/settings.php @@ -33,9 +33,9 @@ return [ 'app_custom_html_desc' => 'Le contenu inséré ici sera ajouté en bas de la balise de toutes les pages. Vous pouvez l\'utiliser pour ajouter du CSS personnalisé ou un tracker analytique.', 'app_custom_html_disabled_notice' => 'Le contenu de l\'en-tête HTML personnalisé est désactivé sur cette page de paramètres pour garantir que les modifications les plus récentes puissent être annulées.', 'app_logo' => 'Logo de l\'application', - 'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.', - 'app_icon' => 'Application Icon', - 'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.', + 'app_logo_desc' => 'Celui-ci est utilisé dans la barre d\'en-tête de l\'application, entre autres zones. L\'image doit être de 86 px de hauteur. Les plus grandes images seront réduites.', + 'app_icon' => 'Icône de l\'application', + 'app_icon_desc' => 'Cette icône est utilisée pour les onglets du navigateur et les icônes de raccourci. Doit être une image PNG carrée de 256 px.', 'app_homepage' => 'Page d\'accueil de l\'application', 'app_homepage_desc' => 'Choisissez une page à afficher sur la page d\'accueil au lieu de la vue par défaut. Les permissions sont ignorées pour les pages sélectionnées.', 'app_homepage_select' => 'Choisissez une page', @@ -49,12 +49,12 @@ return [ 'app_disable_comments_desc' => 'Désactive les commentaires sur toutes les pages de l\'application. Les commentaires existants ne sont pas affichés.', // Color settings - 'color_scheme' => 'Application Color Scheme', - 'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.', - 'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.', - 'app_color' => 'Primary Color', - 'link_color' => 'Default Link Color', - 'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.', + 'color_scheme' => 'Schéma de couleurs de l\'application', + 'color_scheme_desc' => 'Défini les couleurs à utiliser dans l\'interface utilisateur de l\'application. Les couleurs peuvent être configurées séparément pour les modes sombre et clair pour mieux correspondre au thème et assurer la lisibilité.', + 'ui_colors_desc' => 'Défini la couleur primaire de l\'application et la couleur de lien par défaut. La couleur primaire est principalement utilisée pour la bannière d\'en-tête, les boutons et les décorations de l\'interface. La couleur par défaut du lien est utilisée pour les liens et les actions basées sur le texte, à la fois dans le contenu écrit et dans l\'interface de l\'application.', + 'app_color' => 'Couleur primaire', + 'link_color' => 'Couleur de lien par défaut', + 'content_colors_desc' => 'Défini les couleurs pour tous les éléments de la hiérarchie d\'organisation des pages. Choisir les couleurs avec une luminosité similaire aux couleurs par défaut est recommandé pour la lisibilité.', 'bookshelf_color' => 'Couleur des étagères', 'book_color' => 'Couleur des livres', 'chapter_color' => 'Couleur des chapitres', @@ -249,7 +249,7 @@ return [ // Webhooks 'webhooks' => 'Webhooks', 'webhooks_index_desc' => 'Les Webhooks sont un moyen d\'envoyer des données à des URL externes lorsque certaines actions et événements se produisent dans le système, ce qui permet une intégration basée sur des événements avec des plates-formes externes telles que les systèmes de messagerie ou de notification.', - 'webhooks_x_trigger_events' => ':count trigger event|:count trigger events', + 'webhooks_x_trigger_events' => ':count événement déclencheur|:count événements déclencheurs', 'webhooks_create' => 'Créer un nouveau Webhook', 'webhooks_none_created' => 'Aucun webhook n\'a encore été créé.', 'webhooks_edit' => 'Éditer le Webhook', diff --git a/lang/nb/entities.php b/lang/nb/entities.php index 2adfb02ae..f1bdc12a2 100644 --- a/lang/nb/entities.php +++ b/lang/nb/entities.php @@ -237,7 +237,7 @@ return [ 'pages_md_insert_link' => 'Sett inn lenke', 'pages_md_insert_drawing' => 'Sett inn tegning', 'pages_md_show_preview' => 'Forhåndsvisning', - 'pages_md_sync_scroll' => 'Sync preview scroll', + 'pages_md_sync_scroll' => 'Synkroniser forhåndsvisningsrulle', 'pages_not_in_chapter' => 'Siden tilhører ingen kapittel', 'pages_move' => 'Flytt side', 'pages_move_success' => 'Siden ble flyttet til «:parentName»', @@ -248,7 +248,7 @@ return [ 'pages_permissions_success' => 'Sidens tilganger ble endret', 'pages_revision' => 'Revisjon', 'pages_revisions' => 'Sidens revisjoner', - 'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.', + 'pages_revisions_desc' => 'Oppført nedenfor er alle tidligere revisjoner av denne siden. Du kan se tilbake igjen, sammenligne og gjenopprette tidligere sideversjoner hvis du tillater det. Den hele sidens historikk kan kanskje ikke gjenspeiles fullstendig her, avhengig av systemkonfigurasjonen, kan gamle revisjoner bli slettet automatisk.', 'pages_revisions_named' => 'Revisjoner for :pageName', 'pages_revision_named' => 'Revisjoner for :pageName', 'pages_revision_restored_from' => 'Gjenopprettet fra #:id; :summary', @@ -292,17 +292,17 @@ return [ 'shelf_tags' => 'Hyllemerker', 'tag' => 'Merke', 'tags' => 'Merker', - 'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.', + 'tags_index_desc' => 'Merker kan brukes på innhold i systemet for å anvende en kategorisering på en fleksibel måte. Etiketter kan ha både en nøkkel og verdi, med valgfri. Når det er brukt, kan innhold sjekkes ved hjelp av taggnavn og verdi.', 'tag_name' => 'Merketittel', 'tag_value' => 'Merkeverdi (Valgfritt)', 'tags_explain' => "Legg til merker for å kategorisere innholdet ditt. \n Du kan legge til merkeverdier for å beskrive dem ytterligere.", 'tags_add' => 'Legg til flere merker', 'tags_remove' => 'Fjern merke', - 'tags_usages' => 'Total tag usages', - 'tags_assigned_pages' => 'Assigned to Pages', - 'tags_assigned_chapters' => 'Assigned to Chapters', - 'tags_assigned_books' => 'Assigned to Books', - 'tags_assigned_shelves' => 'Assigned to Shelves', + 'tags_usages' => 'Totalt emneordbruk', + 'tags_assigned_pages' => 'Tilordnet sider', + 'tags_assigned_chapters' => 'Tildelt til kapitler', + 'tags_assigned_books' => 'Tilordnet til bøker', + 'tags_assigned_shelves' => 'Tilordnet hyller', 'tags_x_unique_values' => ':count unike verdier', 'tags_all_values' => 'Alle verdier', 'tags_view_tags' => 'Vis etiketter', @@ -374,27 +374,27 @@ return [ 'revision_cannot_delete_latest' => 'CKan ikke slette siste revisjon.', // Copy view - 'copy_consider' => 'Please consider the below when copying content.', - 'copy_consider_permissions' => 'Custom permission settings will not be copied.', - 'copy_consider_owner' => 'You will become the owner of all copied content.', - 'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.', - 'copy_consider_attachments' => 'Page attachments will not be copied.', - 'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.', + 'copy_consider' => 'Vennligst vurder nedenfor når du kopierer innholdet.', + 'copy_consider_permissions' => 'Egendefinerte tilgangsinnstillinger vil ikke bli kopiert.', + 'copy_consider_owner' => 'Du vil bli eier av alt kopiert innhold.', + 'copy_consider_images' => 'Sidebildefiler vil ikke bli duplisert og de opprinnelige bildene beholder relasjonen til siden de opprinnelig ble lastet opp til.', + 'copy_consider_attachments' => 'Sidevedlegg vil ikke bli kopiert.', + 'copy_consider_access' => 'Endring av sted, eier eller rettigheter kan føre til at innholdet er tilgjengelig for dem som tidligere har vært uten adgang.', // Conversions - 'convert_to_shelf' => 'Convert to Shelf', - 'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.', - 'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.', - 'convert_book' => 'Convert Book', - 'convert_book_confirm' => 'Are you sure you want to convert this book?', - 'convert_undo_warning' => 'This cannot be as easily undone.', - 'convert_to_book' => 'Convert to Book', - 'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.', - 'convert_chapter' => 'Convert Chapter', - 'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?', + 'convert_to_shelf' => 'Konverter til bokhylle', + 'convert_to_shelf_contents_desc' => 'Du kan konvertere denne boken til en ny hylle med samme innhold. Kapitteler i denne boken vil bli konvertert til nye bøker. Hvis boken inneholder noen sider, som ikke er i et kapitler, boka blir omdøpt og med slike sider, og boka blir en del av den nye bokhyllen.', + 'convert_to_shelf_permissions_desc' => 'Eventuelle tillatelser som er satt på denne boka, vil bli kopiert til ny hylle og til alle nye under-bøker som ikke har egne tillatelser satt. Vær oppmerksom på at tillatelser på hyllene ikke skjuler automatisk innhold innenfor, da de gjør for bøker.', + 'convert_book' => 'Konverter bok', + 'convert_book_confirm' => 'Er du sikker på at du vil konvertere denne boken?', + 'convert_undo_warning' => 'Dette kan ikke bli så lett å angre.', + 'convert_to_book' => 'Konverter til bok', + 'convert_to_book_desc' => 'Du kan konvertere kapittelet til en ny bok med samme innhold. Alle tillatelser som er angitt i dette kapittelet vil bli kopiert til den nye boken, men alle arvede tillatelser, fra overordnet bok vil ikke kopieres noe som kan føre til en endring av tilgangskontroll.', + 'convert_chapter' => 'Konverter kapittel', + 'convert_chapter_confirm' => 'Er du sikker på at du vil konvertere dette kapittelet?', // References - 'references' => 'References', - 'references_none' => 'There are no tracked references to this item.', - 'references_to_desc' => 'Shown below are all the known pages in the system that link to this item.', + 'references' => 'Referanser', + 'references_none' => 'Det er ingen sporede referanser til dette elementet.', + 'references_to_desc' => 'Nedenfor vises alle de kjente sidene i systemet som lenker til denne oppføringen.', ]; diff --git a/lang/nb/errors.php b/lang/nb/errors.php index 66eb2bb78..fe420c1d4 100644 --- a/lang/nb/errors.php +++ b/lang/nb/errors.php @@ -23,10 +23,10 @@ return [ 'saml_no_email_address' => 'Denne kontoinformasjonen finnes ikke i det eksterne autentiseringssystemet.', 'saml_invalid_response_id' => 'Forespørselen fra det eksterne autentiseringssystemet gjenkjennes ikke av en prosess som startes av dette programmet. Å navigere tilbake etter pålogging kan forårsake dette problemet.', 'saml_fail_authed' => 'Innlogging gjennom :system feilet. Fikk ikke kontakt med autentiseringstjeneren.', - 'oidc_already_logged_in' => 'Already logged in', - 'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled', - 'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system', - 'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization', + 'oidc_already_logged_in' => 'Allerede logget inn', + 'oidc_user_not_registered' => 'Brukeren :name er ikke registrert og automatisk registrering er deaktivert', + 'oidc_no_email_address' => 'Finner ikke en e-postadresse, for denne brukeren, i dataene som leveres av det eksterne autentiseringssystemet', + 'oidc_fail_authed' => 'Innlogging ved hjelp av :system feilet, systemet ga ikke vellykket godkjenning', 'social_no_action_defined' => 'Ingen handlinger er definert', 'social_login_bad_response' => "Feilmelding mottat fra :socialAccount innloggingstjeneste: \n:error", 'social_account_in_use' => 'Denne :socialAccount kontoen er allerede registrert, Prøv å logge inn med :socialAccount alternativet.', @@ -50,7 +50,7 @@ return [ // Drawing & Images 'image_upload_error' => 'Bildet kunne ikke lastes opp, forsøk igjen.', 'image_upload_type_error' => 'Bildeformatet støttes ikke, forsøk med et annet format.', - 'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.', + 'drawing_data_not_found' => 'Tegningsdata kunne ikke lastes. Det er mulig at tegningsfilen ikke finnes lenger, eller du har ikke rettigheter til å få tilgang til den.', // Attachments 'attachment_not_found' => 'Vedlegget ble ikke funnet', @@ -61,7 +61,7 @@ return [ // Entities 'entity_not_found' => 'Entitet ble ikke funnet', - 'bookshelf_not_found' => 'Shelf not found', + 'bookshelf_not_found' => 'Bokhyllen ble ikke funnet', 'book_not_found' => 'Boken ble ikke funnet', 'page_not_found' => 'Siden ble ikke funnet', 'chapter_not_found' => 'Kapittel ble ikke funnet', diff --git a/lang/nb/preferences.php b/lang/nb/preferences.php index e9a47461b..bc066d035 100644 --- a/lang/nb/preferences.php +++ b/lang/nb/preferences.php @@ -5,14 +5,14 @@ */ return [ - 'shortcuts' => 'Shortcuts', - 'shortcuts_interface' => 'Interface Keyboard Shortcuts', - 'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.', - 'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.', - 'shortcuts_toggle_label' => 'Keyboard shortcuts enabled', - 'shortcuts_section_navigation' => 'Navigation', - 'shortcuts_section_actions' => 'Common Actions', - 'shortcuts_save' => 'Save Shortcuts', - 'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.', - 'shortcuts_update_success' => 'Shortcut preferences have been updated!', + 'shortcuts' => 'Snarveier', + 'shortcuts_interface' => 'Grensesnitt hurtigtaster', + 'shortcuts_toggle_desc' => 'Her kan du aktivere eller deaktivere snarveier for tastatur system som brukes til navigasjon og handlinger.', + 'shortcuts_customize_desc' => 'Du kan tilpasse hver av snarveiene nedenfor. Trykk på ønsket nøkkelkombinasjon etter å ha valgt inndata for en snarvei.', + 'shortcuts_toggle_label' => 'Tastatursnarveier aktivert', + 'shortcuts_section_navigation' => 'Navigasjon', + 'shortcuts_section_actions' => 'Vanlige handlinger', + 'shortcuts_save' => 'Lagre snarveier', + 'shortcuts_overlay_desc' => 'Merk: Når snarveier er aktivert er et hjelperoverlegg tilgjengelig via å trykke "?" som vil fremheve de tilgjengelige snarveiene som for øyeblikket er synlige på skjermen.', + 'shortcuts_update_success' => 'Snarvei innstillinger er oppdatert!', ]; \ No newline at end of file diff --git a/lang/nb/settings.php b/lang/nb/settings.php index cfa9d9b60..913078626 100644 --- a/lang/nb/settings.php +++ b/lang/nb/settings.php @@ -10,8 +10,8 @@ return [ 'settings' => 'Innstillinger', 'settings_save' => 'Lagre innstillinger', 'settings_save_success' => 'Innstillinger lagret', - 'system_version' => 'System Version', - 'categories' => 'Categories', + 'system_version' => 'System versjon', + 'categories' => 'Kategorier', // App Settings 'app_customization' => 'Tilpassing', @@ -27,15 +27,15 @@ return [ 'app_secure_images' => 'Høyere sikkerhet på bildeopplastinger', 'app_secure_images_toggle' => 'Enable høyere sikkerhet på bildeopplastinger', 'app_secure_images_desc' => 'Av ytelsesgrunner er alle bilder offentlige. Dette alternativet legger til en tilfeldig streng som er vanskelig å gjette foran bildets nettadresser. Forsikre deg om at katalogindekser ikke er aktivert for å forhindre enkel tilgang.', - 'app_default_editor' => 'Default Page Editor', - 'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.', + 'app_default_editor' => 'Standard sideredigeringsprogram', + 'app_default_editor_desc' => 'Velg hvilken tekstbehandler som skal brukes som standard når du redigerer nye sider. Dette kan overskrives på et sidenivå der tillatelser tillates.', 'app_custom_html' => 'Tilpasset HTML-hodeinnhold', 'app_custom_html_desc' => 'Alt innhold som legges til her, blir satt inn i bunnen av -delen på hver side. Dette er praktisk for å overstyre stiler eller legge til analysekode.', 'app_custom_html_disabled_notice' => 'Tilpasset HTML-hodeinnhold er deaktivert på denne innstillingssiden for å sikre at eventuelle endringer ødelegger noe, kan tilbakestilles.', 'app_logo' => 'Applikasjonslogo', - 'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.', - 'app_icon' => 'Application Icon', - 'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.', + 'app_logo_desc' => 'Dette brukes i programtoppfeltet blant andre områder. Dette bildet skal være 86px i høyde. Store bilder vil bli skalert ned.', + 'app_icon' => 'Applikasjons ikon', + 'app_icon_desc' => 'Dette ikonet brukes for nettleserfaner og snarveisikoner. Dette bør være et bilde på 256 px kvadrat PNG.', 'app_homepage' => 'Applikasjonens hjemmeside', 'app_homepage_desc' => 'Velg en visning som skal vises på hjemmesiden i stedet for standardvisningen. Sidetillatelser ignoreres for utvalgte sider.', 'app_homepage_select' => 'Velg en side', @@ -49,12 +49,12 @@ return [ 'app_disable_comments_desc' => 'Deaktiver kommentarer på tvers av alle sidene i applikasjonen.
    Eksisterende kommentarer vises ikke.', // Color settings - 'color_scheme' => 'Application Color Scheme', - 'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.', - 'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.', - 'app_color' => 'Primary Color', - 'link_color' => 'Default Link Color', - 'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.', + 'color_scheme' => 'Applikasjonens farge oppsett', + 'color_scheme_desc' => 'Sett farger for å bruke i programmets brukergrensesnitt. Farger kan konfigureres separat for mørke og lysmoduser for å passe best inn temaet og sørge for lesbarhet.', + 'ui_colors_desc' => 'Angi primær farge for programmet og standard link farge. Primær farge brukes hovedsakelig for toppbanner, knapper og grensesnittets dekorasjoner. Standardfargen for koblinger brukes for tekstbaserte lenker og handlinger, både i skriftlig innhold og i programgrensesnittet.', + 'app_color' => 'Primær farge', + 'link_color' => 'Standard koblingsfarge', + 'content_colors_desc' => 'Angi farger for alle elementer i organiseringshierarkiet. Velger du farger med lik lysstyrke til standard farger anbefales for lesbarhet.', 'bookshelf_color' => 'Hyllefarge', 'book_color' => 'Bokfarge', 'chapter_color' => 'Kapittelfarge', @@ -93,10 +93,10 @@ return [ 'maint_send_test_email_mail_text' => 'Gratulerer! Da du mottok dette e-postvarselet, ser det ut til at e-postinnstillingene dine er konfigurert riktig.', 'maint_recycle_bin_desc' => 'Slettede hyller, bøker, kapitler og sider kastes i papirkurven så de kan bli gjenopprettet eller slettet permanent. Eldre utgaver i papirkurven kan slettes automatisk etter en stund, avhengig av systemkonfigurasjonen.', 'maint_recycle_bin_open' => 'Åpne papirkurven', - 'maint_regen_references' => 'Regenerate References', - 'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.', - 'maint_regen_references_success' => 'Reference index has been regenerated!', - 'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.', + 'maint_regen_references' => 'Regenerer referanser', + 'maint_regen_references_desc' => 'Denne handlingen gjenoppbygger referanseindeksen for krysselement i databasen. Dette håndteres vanligvis automatisk, men denne handlingen kan være nyttig for å indeksere gammelt innhold eller innhold lagt til via uoffisielle metoder.', + 'maint_regen_references_success' => 'Referanseindeksen har blitt regenerert!', + 'maint_timeout_command_note' => 'Merk: Denne handlingen kan ta tid å kjøre, noe som kan føre til tidsavbruddsmessige problemer i noen webomgivelser. Dette gjøres som et alternativ ved hjelp av en terminalkommando.', // Recycle Bin 'recycle_bin' => 'Papirkurven', @@ -129,7 +129,7 @@ return [ 'audit_table_user' => 'Kontoholder', 'audit_table_event' => 'Hendelse', 'audit_table_related' => 'Relaterte elementer eller detaljer', - 'audit_table_ip' => 'IP Address', + 'audit_table_ip' => 'IP Adresse', 'audit_table_date' => 'Aktivitetsdato', 'audit_date_from' => 'Datoperiode fra', 'audit_date_to' => 'Datoperiode til', @@ -137,13 +137,13 @@ return [ // Role Settings 'roles' => 'Roller', 'role_user_roles' => 'Kontoroller', - 'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.', - 'roles_x_users_assigned' => ':count user assigned|:count users assigned', - 'roles_x_permissions_provided' => ':count permission|:count permissions', - 'roles_assigned_users' => 'Assigned Users', - 'roles_permissions_provided' => 'Provided Permissions', + 'roles_index_desc' => 'Roller brukes til å gruppere brukere og gi systemtilgang til medlemmene. Når en bruker er medlem av flere roller, vil de tildelte rettighetene samles inn, og brukeren vil arve alle evner.', + 'roles_x_users_assigned' => ':count bruker tildelt|:count brukere tildelt', + 'roles_x_permissions_provided' => ':count tillatelse|:count tillatelser', + 'roles_assigned_users' => 'Tilordnede brukere', + 'roles_permissions_provided' => 'Tilbudte rettigheter', 'role_create' => 'Opprett ny rolle', - 'role_delete' => 'Rolle slettet', + 'role_delete' => 'Slett rolle', 'role_delete_confirm' => 'Dette vil slette rollen «:roleName».', 'role_delete_users_assigned' => 'Denne rollen har :userCount kontoer koblet opp mot seg. Velg hvilke rolle du vil flytte disse til.', 'role_delete_no_migration' => "Ikke flytt kontoer", @@ -162,13 +162,13 @@ return [ 'role_manage_page_templates' => 'Behandle sidemaler', 'role_access_api' => 'Systemtilgang API', 'role_manage_settings' => 'Behandle applikasjonsinnstillinger', - 'role_export_content' => 'Export content', - 'role_editor_change' => 'Change page editor', + 'role_export_content' => 'Eksporter innhold', + 'role_editor_change' => 'Endre sideredigering', 'role_asset' => 'Eiendomstillatelser', 'roles_system_warning' => 'Vær oppmerksom på at tilgang til noen av de ovennevnte tre tillatelsene kan tillate en bruker å endre sine egne rettigheter eller rettighetene til andre i systemet. Bare tildel roller med disse tillatelsene til pålitelige brukere.', 'role_asset_desc' => 'Disse tillatelsene kontrollerer standard tilgang til eiendelene i systemet. Tillatelser til bøker, kapitler og sider overstyrer disse tillatelsene.', 'role_asset_admins' => 'Administratorer får automatisk tilgang til alt innhold, men disse alternativene kan vise eller skjule UI-alternativer.', - 'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.', + 'role_asset_image_view_note' => 'Dette gjelder synlighet innenfor bilde-administrasjonen. Faktisk tilgang på opplastede bildefiler vil være avhengig av valget for systemlagring av bildet.', 'role_all' => 'Alle', 'role_own' => 'Egne', 'role_controlled_by_asset' => 'Kontrollert av eiendelen de er lastet opp til', @@ -178,7 +178,7 @@ return [ // Users 'users' => 'Brukere', - 'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.', + 'users_index_desc' => 'Opprett og administrer individuelle brukerkontoer innenfor systemet. Brukerkontoer brukes for innlogging og navngivelse av innhold og aktivitet. Tilgangstillatelser er primært rollebasert, men brukerinnhold eierskap, blant andre faktorer, kan også påvirke tillatelser og tilgang.', 'user_profile' => 'Profil', 'users_add_new' => 'Register ny konto', 'users_search' => 'Søk i kontoer', @@ -189,7 +189,7 @@ return [ 'users_role' => 'Roller', 'users_role_desc' => 'Velg hvilke roller denne kontoholderen vil bli tildelt. Hvis en kontoholderen er tildelt flere roller, vil tillatelsene fra disse rollene stable seg, og de vil motta alle evnene til de tildelte rollene.', 'users_password' => 'Passord', - 'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.', + 'users_password_desc' => 'Angi et passord som brukes til å logge inn til programmet. Dette må være minst 8 tegn langt.', 'users_send_invite_text' => 'Du kan velge å sende denne kontoholderen en invitasjons-e-post som lar dem angi sitt eget passord, ellers kan du selv angi passordet.', 'users_send_invite_option' => 'Send invitasjonsmelding', 'users_external_auth_id' => 'Ekstern godkjennings-ID', @@ -248,32 +248,32 @@ return [ // Webhooks 'webhooks' => 'Webhooks', - 'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.', - 'webhooks_x_trigger_events' => ':count trigger event|:count trigger events', - 'webhooks_create' => 'Create New Webhook', - 'webhooks_none_created' => 'No webhooks have yet been created.', - 'webhooks_edit' => 'Edit Webhook', - 'webhooks_save' => 'Save Webhook', - 'webhooks_details' => 'Webhook Details', - 'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.', - 'webhooks_events' => 'Webhook Events', - 'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.', - 'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\'t expose confidential content.', - 'webhooks_events_all' => 'All system events', - 'webhooks_name' => 'Webhook Name', - 'webhooks_timeout' => 'Webhook Request Timeout (Seconds)', - 'webhooks_endpoint' => 'Webhook Endpoint', - 'webhooks_active' => 'Webhook Active', - 'webhook_events_table_header' => 'Events', - 'webhooks_delete' => 'Delete Webhook', - 'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \':webhookName\', from the system.', - 'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?', - 'webhooks_format_example' => 'Webhook Format Example', - 'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The "related_item" and "url" properties are optional and will depend on the type of event triggered.', - 'webhooks_status' => 'Webhook Status', - 'webhooks_last_called' => 'Last Called:', - 'webhooks_last_errored' => 'Last Errored:', - 'webhooks_last_error_message' => 'Last Error Message:', + 'webhooks_index_desc' => 'Webhooks er en måte å sende data til eksterne nettadresser når bestemte handlinger og hendelser oppstår i systemet som gjør det mulig å integrer med eksterne plattformer som meldingssystemer eller varslingssystemer.', + 'webhooks_x_trigger_events' => ':count utløsende hendelse:count utløsende hendelser', + 'webhooks_create' => 'Lag ny Webhook', + 'webhooks_none_created' => 'Ingen webhooks er opprettet ennå.', + 'webhooks_edit' => 'Rediger webhook', + 'webhooks_save' => 'Lagre Webhook', + 'webhooks_details' => 'Webhook detaljer', + 'webhooks_details_desc' => 'Gi et brukervennlig navn og et POST endepunkt som et sted der webhook-dataene skal sendes til.', + 'webhooks_events' => 'Webhook hendelser', + 'webhooks_events_desc' => 'Velg alle hendelsene som skal utløse denne webhook som skal kalles.', + 'webhooks_events_warning' => 'Husk at disse hendelsene vil bli utløst for alle valgte hendelser, selv om egendefinerte tillatelser brukes. Pass på at bruk av denne webhooken ikke vil utsette konfidensiell innhold.', + 'webhooks_events_all' => 'Alle systemhendelser', + 'webhooks_name' => 'Webhook navn', + 'webhooks_timeout' => 'Tidsavbrudd for Webhook forespørsler (sekunder)', + 'webhooks_endpoint' => 'Webhook endepunkt', + 'webhooks_active' => 'Webhook aktiv', + 'webhook_events_table_header' => 'Hendelser', + 'webhooks_delete' => 'Slett webhook', + 'webhooks_delete_warning' => 'Dette vil slette webhook, med navnet \':webhookName\', fra systemet.', + 'webhooks_delete_confirm' => 'Er du sikker på at du vil slette denne webhooken?', + 'webhooks_format_example' => 'Webhook formattering eksempel', + 'webhooks_format_example_desc' => 'Webhook-data sendes som en POST-forespørsel til det konfigurerte endepunktet som JSON ved hjelp av formatet nedenfor. «related_item» og «url» egenskaper er valgfrie og vil avhenge av hvilken type hendelse som utløses.', + 'webhooks_status' => 'Webhook status', + 'webhooks_last_called' => 'Sist ringt:', + 'webhooks_last_errored' => 'Siste feil:', + 'webhooks_last_error_message' => 'Siste feilmelding:', //! If editing translations files directly please ignore this in all diff --git a/lang/nb/validation.php b/lang/nb/validation.php index 0e8917f37..7e3784e30 100644 --- a/lang/nb/validation.php +++ b/lang/nb/validation.php @@ -32,7 +32,7 @@ return [ 'digits_between' => ':attribute må være mellomg :min og :max tall.', 'email' => ':attribute må være en gyldig e-post.', 'ends_with' => ':attribute må slutte med en av verdiene: :values', - 'file' => 'The :attribute must be provided as a valid file.', + 'file' => 'Attributtet :attribute må angis som en gyldig fil.', 'filled' => ':attribute feltet er påkrevd.', 'gt' => [ 'numeric' => ':attribute må være større enn :value.', diff --git a/lang/pl/settings.php b/lang/pl/settings.php index 54d818954..bfc8a56e8 100644 --- a/lang/pl/settings.php +++ b/lang/pl/settings.php @@ -50,8 +50,8 @@ return [ // Color settings 'color_scheme' => 'Schemat kolorów aplikacji', - 'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.', - 'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.', + 'color_scheme_desc' => 'Ustaw kolory używane w interfejsie aplikacji. Kolory można skonfigurować oddzielnie dla trybu ciemnego i jasnego, aby najlepiej pasowały do motywu i zapewniały czytelność.', + 'ui_colors_desc' => 'Ustaw podstawowy kolor aplikacji i domyślny kolor linku. Podstawowy kolor jest używany głównie w banerze aplikacji, przyciskach i interfejsie. Domyślny kolor linku jest używany dla tekstowych linków i akcji, zarówno w napisanych treściach, jak i w interfejsie aplikacji.', 'app_color' => 'Kolor podstawowy', 'link_color' => 'Domyślny kolor linku', 'content_colors_desc' => 'Ustaw kolory dla wszystkich elementów w hierarchii organizacji stron. Wybór kolorów o jasności podobnej do domyślnych kolorów jest zalecany dla czytelności.', diff --git a/lang/pt_BR/activities.php b/lang/pt_BR/activities.php index 870f5ab73..6375afb56 100644 --- a/lang/pt_BR/activities.php +++ b/lang/pt_BR/activities.php @@ -68,9 +68,9 @@ return [ 'user_delete_notification' => 'Usuário removido com sucesso', // Roles - 'role_create_notification' => 'Role successfully created', - 'role_update_notification' => 'Role successfully updated', - 'role_delete_notification' => 'Role successfully deleted', + 'role_create_notification' => 'Perfil criado com sucesso', + 'role_update_notification' => 'Perfil atualizado com sucesso', + 'role_delete_notification' => 'Perfil excluído com sucesso', // Other 'commented_on' => 'comentou em', diff --git a/lang/pt_BR/errors.php b/lang/pt_BR/errors.php index ac16aaa31..34843670a 100644 --- a/lang/pt_BR/errors.php +++ b/lang/pt_BR/errors.php @@ -50,7 +50,7 @@ return [ // Drawing & Images 'image_upload_error' => 'Um erro aconteceu enquanto o servidor tentava efetuar o upload da imagem', 'image_upload_type_error' => 'O tipo de imagem que está sendo enviada é inválido', - 'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.', + 'drawing_data_not_found' => 'Dados de desenho não puderam ser carregados. Talvez o arquivo de desenho não exista mais ou você não tenha permissão para acessá-lo.', // Attachments 'attachment_not_found' => 'Anexo não encontrado', From 572037ef1fd1e778c33c609ef295c50de33a0652 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 10 Apr 2023 15:01:44 +0100 Subject: [PATCH 22/76] Got markdown editor barely functional Updated content sync and preview scoll sync to work. Many features commented out until they can be updated. --- package.json | 2 +- resources/js/code/index.mjs | 39 ++++---- resources/js/code/setups.js | 27 +++++- resources/js/components/markdown-editor.js | 13 ++- resources/js/markdown/actions.js | 12 +-- resources/js/markdown/codemirror.js | 103 +++++++++++---------- resources/sass/_forms.scss | 4 + 7 files changed, 121 insertions(+), 79 deletions(-) diff --git a/package.json b/package.json index 6b6dbbe46..9ee57afa5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "build:css:watch": "sass ./resources/sass:./public/dist --watch --embed-sources", "build:css:production": "sass ./resources/sass:./public/dist -s compressed", "build:js:dev": "node dev/build/esbuild.js", - "build:js:watch": "chokidar --initial \"./resources/**/*.js\" -c \"npm run build:js:dev\"", + "build:js:watch": "chokidar --initial \"./resources/**/*.js\" \"./resources/**/*.mjs\" -c \"npm run build:js:dev\"", "build:js:production": "node dev/build/esbuild.js production", "build": "npm-run-all --parallel build:*:dev", "production": "npm-run-all --parallel build:*:production", diff --git a/resources/js/code/index.mjs b/resources/js/code/index.mjs index 6ef659994..6cad052f4 100644 --- a/resources/js/code/index.mjs +++ b/resources/js/code/index.mjs @@ -2,7 +2,7 @@ import {EditorView} from "@codemirror/view" import Clipboard from "clipboard/dist/clipboard.min"; // Modes -import {viewer} from "./setups.js"; +import {viewer, editor} from "./setups.js"; import {createView, updateViewLanguage} from "./views.js"; /** @@ -180,25 +180,31 @@ export function updateLayout(cmInstance) { /** * Get a CodeMirror instance to use for the markdown editor. * @param {HTMLElement} elem + * @param {function} onChange + * @param {object} domEventHandlers * @returns {*} */ -export function markdownEditor(elem) { +export function markdownEditor(elem, onChange, domEventHandlers) { const content = elem.textContent; - const config = { - value: content, - mode: "markdown", - lineNumbers: true, - lineWrapping: true, - theme: getTheme(), - scrollPastEnd: true, - }; - window.$events.emitPublic(elem, 'editor-markdown-cm::pre-init', {config}); + // TODO - Change to pass something else that's useful, probably extension array? + // window.$events.emitPublic(elem, 'editor-markdown-cm::pre-init', {config}); - return CodeMirror(function (elt) { - elem.parentNode.insertBefore(elt, elem); - elem.style.display = 'none'; - }, config); + const ev = createView({ + parent: elem.parentNode, + doc: content, + extensions: [ + ...editor('markdown'), + EditorView.updateListener.of((v) => { + onChange(v); + }), + EditorView.domEventHandlers(domEventHandlers), + ], + }); + + elem.style.display = 'none'; + + return ev; } /** @@ -206,6 +212,7 @@ export function markdownEditor(elem) { * @returns {string} */ export function getMetaKey() { - let mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault; + // TODO - Redo, Is needed? No CodeMirror instance to use. + const mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault; return mac ? "Cmd" : "Ctrl"; } \ No newline at end of file diff --git a/resources/js/code/setups.js b/resources/js/code/setups.js index e1a150856..00366ee5e 100644 --- a/resources/js/code/setups.js +++ b/resources/js/code/setups.js @@ -1,12 +1,12 @@ -import {keymap, highlightSpecialChars, drawSelection, highlightActiveLine, dropCursor, +import {EditorView, keymap, highlightSpecialChars, drawSelection, highlightActiveLine, dropCursor, rectangularSelection, lineNumbers, highlightActiveLineGutter} from "@codemirror/view" -import {defaultHighlightStyle, syntaxHighlighting, bracketMatching, - foldKeymap} from "@codemirror/language" +import {syntaxHighlighting, bracketMatching} from "@codemirror/language" import {defaultKeymap, history, historyKeymap} from "@codemirror/commands" import {EditorState} from "@codemirror/state" import {defaultLight} from "./themes"; +import {getLanguageExtension} from "./languages"; export function viewer() { return [ @@ -23,8 +23,27 @@ export function viewer() { keymap.of([ ...defaultKeymap, ...historyKeymap, - ...foldKeymap, ]), EditorState.readOnly.of(true), ]; +} + +export function editor(language) { + return [ + lineNumbers(), + highlightActiveLineGutter(), + highlightSpecialChars(), + history(), + drawSelection(), + dropCursor(), + syntaxHighlighting(defaultLight, {fallback: true}), + bracketMatching(), + rectangularSelection(), + highlightActiveLine(), + keymap.of([ + ...defaultKeymap, + ...historyKeymap, + ]), + getLanguageExtension(language, ''), + ]; } \ No newline at end of file diff --git a/resources/js/components/markdown-editor.js b/resources/js/components/markdown-editor.js index 5cd92cae2..6b4682d1e 100644 --- a/resources/js/components/markdown-editor.js +++ b/resources/js/components/markdown-editor.js @@ -45,7 +45,8 @@ export class MarkdownEditor extends Component { window.$events.emitPublic(this.elem, 'editor-markdown::setup', { markdownIt: this.editor.markdown.getRenderer(), displayEl: this.display, - codeMirrorInstance: this.editor.cm, + // TODO + // codeMirrorInstance: this.editor.cm, }); } @@ -81,9 +82,10 @@ export class MarkdownEditor extends Component { }); // Refresh CodeMirror on container resize - const resizeDebounced = debounce(() => this.editor.cm.refresh(), 100, false); - const observer = new ResizeObserver(resizeDebounced); - observer.observe(this.elem); + // TODO + // const resizeDebounced = debounce(() => this.editor.cm.refresh(), 100, false); + // const observer = new ResizeObserver(resizeDebounced); + // observer.observe(this.elem); this.handleDividerDrag(); } @@ -102,7 +104,8 @@ export class MarkdownEditor extends Component { window.removeEventListener('pointerup', upListener); this.display.style.pointerEvents = null; document.body.style.userSelect = null; - this.editor.cm.refresh(); + // TODO + // this.editor.cm.refresh(); }; this.display.style.pointerEvents = 'none'; diff --git a/resources/js/markdown/actions.js b/resources/js/markdown/actions.js index 9faf43de3..666998723 100644 --- a/resources/js/markdown/actions.js +++ b/resources/js/markdown/actions.js @@ -13,7 +13,7 @@ export class Actions { } updateAndRender() { - const content = this.editor.cm.getValue(); + const content = this.editor.cm.state.doc.toString(); this.editor.config.inputEl.value = content; const html = this.editor.markdown.render(content); @@ -411,17 +411,17 @@ export class Actions { }); } - syncDisplayPosition() { + syncDisplayPosition(event) { // Thanks to http://liuhao.im/english/2015/11/10/the-sync-scroll-of-markdown-editor-in-javascript.html - const scroll = this.editor.cm.getScrollInfo(); - const atEnd = scroll.top + scroll.clientHeight === scroll.height; + const scrollEl = event.target; + const atEnd = Math.abs(scrollEl.scrollHeight - scrollEl.clientHeight - scrollEl.scrollTop) < 1; if (atEnd) { this.editor.display.scrollToIndex(-1); return; } - const lineNum = this.editor.cm.lineAtHeight(scroll.top, 'local'); - const range = this.editor.cm.getRange({line: 0, ch: null}, {line: lineNum, ch: null}); + const blockInfo = this.editor.cm.lineBlockAtHeight(scrollEl.scrollTop); + const range = this.editor.cm.state.sliceDoc(0, blockInfo.from); const parser = new DOMParser(); const doc = parser.parseFromString(this.editor.markdown.render(range), 'text/html'); const totalLines = doc.documentElement.querySelectorAll('body > *'); diff --git a/resources/js/markdown/codemirror.js b/resources/js/markdown/codemirror.js index 8724a23c8..dad999e7a 100644 --- a/resources/js/markdown/codemirror.js +++ b/resources/js/markdown/codemirror.js @@ -9,62 +9,71 @@ import Clipboard from "../services/clipboard"; */ export async function init(editor) { const Code = await window.importVersioned('code'); - const cm = Code.markdownEditor(editor.config.inputEl); - // Will force to remain as ltr for now due to issues when HTML is in editor. - cm.setOption('direction', 'ltr'); - // Register shortcuts - cm.setOption('extraKeys', provideShortcuts(editor, Code.getMetaKey())); + /** + * @param {ViewUpdate} v + */ + function onViewUpdate(v) { + if (v.docChanged) { + editor.actions.updateAndRender(); + } + } - - // Register codemirror events - - // Update data on content change - cm.on('change', (instance, changeObj) => editor.actions.updateAndRender()); - - // Handle scroll to sync display view const onScrollDebounced = debounce(editor.actions.syncDisplayPosition.bind(editor.actions), 100, false); let syncActive = editor.settings.get('scrollSync'); editor.settings.onChange('scrollSync', val => syncActive = val); - cm.on('scroll', instance => { - if (syncActive) { - onScrollDebounced(instance); - } - }); + + const domEventHandlers = { + // Handle scroll to sync display view + scroll: (event) => syncActive && onScrollDebounced(event) + } + + const cm = Code.markdownEditor(editor.config.inputEl, onViewUpdate, domEventHandlers); + window.cm = cm; + + // Will force to remain as ltr for now due to issues when HTML is in editor. + // TODO + // cm.setOption('direction', 'ltr'); + // Register shortcuts + // TODO + // cm.setOption('extraKeys', provideShortcuts(editor, Code.getMetaKey())); + // Handle image paste - cm.on('paste', (cm, event) => { - const clipboard = new Clipboard(event.clipboardData || event.dataTransfer); - - // Don't handle the event ourselves if no items exist of contains table-looking data - if (!clipboard.hasItems() || clipboard.containsTabularData()) { - return; - } - - const images = clipboard.getImages(); - for (const image of images) { - editor.actions.uploadImage(image); - } - }); + // TODO + // cm.on('paste', (cm, event) => { + // const clipboard = new Clipboard(event.clipboardData || event.dataTransfer); + // + // // Don't handle the event ourselves if no items exist of contains table-looking data + // if (!clipboard.hasItems() || clipboard.containsTabularData()) { + // return; + // } + // + // const images = clipboard.getImages(); + // for (const image of images) { + // editor.actions.uploadImage(image); + // } + // }); // Handle image & content drag n drop - cm.on('drop', (cm, event) => { - - const templateId = event.dataTransfer.getData('bookstack/template'); - if (templateId) { - event.preventDefault(); - editor.actions.insertTemplate(templateId, event.pageX, event.pageY); - } - - const clipboard = new Clipboard(event.dataTransfer); - const clipboardImages = clipboard.getImages(); - if (clipboardImages.length > 0) { - event.stopPropagation(); - event.preventDefault(); - editor.actions.insertClipboardImages(clipboardImages); - } - - }); + // TODO + // cm.on('drop', (cm, event) => { + // + // const templateId = event.dataTransfer.getData('bookstack/template'); + // if (templateId) { + // event.preventDefault(); + // editor.actions.insertTemplate(templateId, event.pageX, event.pageY); + // } + // + // const clipboard = new Clipboard(event.dataTransfer); + // const clipboardImages = clipboard.getImages(); + // if (clipboardImages.length > 0) { + // event.stopPropagation(); + // event.preventDefault(); + // editor.actions.insertClipboardImages(clipboardImages); + // } + // + // }); return cm; } \ No newline at end of file diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss index b7fc52f7d..b7b1b6d4d 100644 --- a/resources/sass/_forms.scss +++ b/resources/sass/_forms.scss @@ -82,6 +82,10 @@ flex-grow: 0; } +.markdown-editor-wrap .cm-editor { + flex: 1; +} + .markdown-panel-divider { width: 2px; @include lightDark(background-color, #ddd, #000); From da3e4f5f75d2012bc4e0eaac358a196d99ab3fbb Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 11 Apr 2023 11:48:58 +0100 Subject: [PATCH 23/76] Got md shortcuts working, marked actions for update --- resources/js/code/index.mjs | 16 ++---- resources/js/components/markdown-editor.js | 4 +- resources/js/markdown/actions.js | 35 +++++++++++- resources/js/markdown/codemirror.js | 12 ++-- resources/js/markdown/shortcuts.js | 65 ++++++++++++---------- 5 files changed, 84 insertions(+), 48 deletions(-) diff --git a/resources/js/code/index.mjs b/resources/js/code/index.mjs index 6cad052f4..3fe4a6d86 100644 --- a/resources/js/code/index.mjs +++ b/resources/js/code/index.mjs @@ -1,4 +1,4 @@ -import {EditorView} from "@codemirror/view" +import {EditorView, keymap} from "@codemirror/view" import Clipboard from "clipboard/dist/clipboard.min"; // Modes @@ -182,9 +182,10 @@ export function updateLayout(cmInstance) { * @param {HTMLElement} elem * @param {function} onChange * @param {object} domEventHandlers + * @param {Array} keyBindings * @returns {*} */ -export function markdownEditor(elem, onChange, domEventHandlers) { +export function markdownEditor(elem, onChange, domEventHandlers, keyBindings) { const content = elem.textContent; // TODO - Change to pass something else that's useful, probably extension array? @@ -199,20 +200,11 @@ export function markdownEditor(elem, onChange, domEventHandlers) { onChange(v); }), EditorView.domEventHandlers(domEventHandlers), + keymap.of(keyBindings), ], }); elem.style.display = 'none'; return ev; -} - -/** - * Get the 'meta' key dependent on the user's system. - * @returns {string} - */ -export function getMetaKey() { - // TODO - Redo, Is needed? No CodeMirror instance to use. - const mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault; - return mac ? "Cmd" : "Ctrl"; } \ No newline at end of file diff --git a/resources/js/components/markdown-editor.js b/resources/js/components/markdown-editor.js index 6b4682d1e..922916701 100644 --- a/resources/js/components/markdown-editor.js +++ b/resources/js/components/markdown-editor.js @@ -45,7 +45,7 @@ export class MarkdownEditor extends Component { window.$events.emitPublic(this.elem, 'editor-markdown::setup', { markdownIt: this.editor.markdown.getRenderer(), displayEl: this.display, - // TODO + // TODO - change to codeMirrorView? // codeMirrorInstance: this.editor.cm, }); } @@ -58,7 +58,7 @@ export class MarkdownEditor extends Component { if (button === null) return; const action = button.getAttribute('data-action'); - if (action === 'insertImage') this.editor.actions.insertImage(); + if (action === 'insertImage') this.editor.actions.showImageInsert(); if (action === 'insertLink') this.editor.actions.showLinkSelector(); if (action === 'insertDrawing' && (event.ctrlKey || event.metaKey)) { this.editor.actions.showImageManager(); diff --git a/resources/js/markdown/actions.js b/resources/js/markdown/actions.js index 666998723..dfbe89c5a 100644 --- a/resources/js/markdown/actions.js +++ b/resources/js/markdown/actions.js @@ -28,7 +28,8 @@ export class Actions { return this.lastContent; } - insertImage() { + showImageInsert() { + // TODO const cursorPos = this.editor.cm.getCursor('from'); /** @type {ImageManager} **/ const imageManager = window.$components.first('image-manager'); @@ -42,7 +43,17 @@ export class Actions { }, 'gallery'); } + insertImage() { + // TODO + const selectedText = this.editor.cm.getSelection(); + const newText = `![${selectedText}](http://)`; + const cursorPos = this.editor.cm.getCursor('from'); + this.editor.cm.replaceSelection(newText); + this.editor.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length -1); + } + insertLink() { + // TODO const cursorPos = this.editor.cm.getCursor('from'); const selectedText = this.editor.cm.getSelection() || ''; const newText = `[${selectedText}]()`; @@ -53,6 +64,7 @@ export class Actions { } showImageManager() { + // TODO const cursorPos = this.editor.cm.getCursor('from'); /** @type {ImageManager} **/ const imageManager = window.$components.first('image-manager'); @@ -63,6 +75,7 @@ export class Actions { // Show the popup link selector and insert a link when finished showLinkSelector() { + // TODO const cursorPos = this.editor.cm.getCursor('from'); /** @type {EntitySelectorPopup} **/ const selector = window.$components.first('entity-selector-popup'); @@ -77,6 +90,7 @@ export class Actions { // Show draw.io if enabled and handle save. startDrawing() { + // TODO const url = this.editor.config.drawioUrl; if (!url) return; @@ -101,6 +115,7 @@ export class Actions { } insertDrawing(image, originalCursor) { + // TODO const newText = `
    `; this.editor.cm.focus(); this.editor.cm.replaceSelection(newText); @@ -109,6 +124,7 @@ export class Actions { // Show draw.io if enabled and handle save. editDrawing(imgContainer) { + // TODO const drawioUrl = this.editor.config.drawioUrl; if (!drawioUrl) { return; @@ -145,6 +161,7 @@ export class Actions { } handleDrawingUploadError(error) { + // TODO if (error.status === 413) { window.$events.emit('error', this.editor.config.text.serverUploadLimit); } else { @@ -155,6 +172,7 @@ export class Actions { // Make the editor full screen fullScreen() { + // TODO const container = this.editor.config.container; const alreadyFullscreen = container.classList.contains('fullscreen'); container.classList.toggle('fullscreen', !alreadyFullscreen); @@ -163,6 +181,7 @@ export class Actions { // Scroll to a specified text scrollToText(searchText) { + // TODO if (!searchText) { return; } @@ -189,6 +208,7 @@ export class Actions { } focus() { + // TODO this.editor.cm.focus(); } @@ -197,6 +217,7 @@ export class Actions { * @param {String} content */ insertContent(content) { + // TODO this.editor.cm.replaceSelection(content); } @@ -205,6 +226,7 @@ export class Actions { * @param {String} content */ prependContent(content) { + // TODO const cursorPos = this.editor.cm.getCursor('from'); const newContent = content + '\n' + this.editor.cm.getValue(); this.editor.cm.setValue(newContent); @@ -217,6 +239,7 @@ export class Actions { * @param {String} content */ appendContent(content) { + // TODO const cursorPos = this.editor.cm.getCursor('from'); const newContent = this.editor.cm.getValue() + '\n' + content; this.editor.cm.setValue(newContent); @@ -228,6 +251,7 @@ export class Actions { * @param {String} content */ replaceContent(content) { + // TODO this.editor.cm.setValue(content); } @@ -236,6 +260,7 @@ export class Actions { * @param {String} replace */ findAndReplaceContent(search, replace) { + // TODO const text = this.editor.cm.getValue(); const cursor = this.editor.cm.listSelections(); this.editor.cm.setValue(text.replace(search, replace)); @@ -247,6 +272,7 @@ export class Actions { * @param {String} newStart */ replaceLineStart(newStart) { + // TODO const cursor = this.editor.cm.getCursor(); let lineContent = this.editor.cm.getLine(cursor.line); const lineLen = lineContent.length; @@ -279,6 +305,7 @@ export class Actions { * @param {String} end */ wrapLine(start, end) { + // TODO const cursor = this.editor.cm.getCursor(); const lineContent = this.editor.cm.getLine(cursor.line); const lineLen = lineContent.length; @@ -300,6 +327,7 @@ export class Actions { * @param {String} end */ wrapSelection(start, end) { + // TODO const selection = this.editor.cm.getSelection(); if (selection === '') return this.wrapLine(start, end); @@ -324,6 +352,7 @@ export class Actions { } replaceLineStartForOrderedList() { + // TODO const cursor = this.editor.cm.getCursor(); const prevLineContent = this.editor.cm.getLine(cursor.line - 1) || ''; const listMatch = prevLineContent.match(/^(\s*)(\d)([).])\s/) || []; @@ -341,6 +370,7 @@ export class Actions { * Creates a callout block if none existing, and removes it if cycling past the danger type. */ cycleCalloutTypeAtSelection() { + // TODO const selectionRange = this.editor.cm.listSelections()[0]; const lineContent = this.editor.cm.getLine(selectionRange.anchor.line); const lineLength = lineContent.length; @@ -379,6 +409,7 @@ export class Actions { * @param {File} file */ uploadImage(file) { + // TODO if (file === null || file.type.indexOf('image') !== 0) return; let ext = 'png'; @@ -436,6 +467,7 @@ export class Actions { * @param {Number} posY */ insertTemplate(templateId, posX, posY) { + // TODO const cursorPos = this.editor.cm.coordsChar({left: posX, top: posY}); this.editor.cm.setCursor(cursorPos); window.$http.get(`/templates/${templateId}`).then(resp => { @@ -449,6 +481,7 @@ export class Actions { * @param {File[]} images */ insertClipboardImages(images) { + // TODO const cursorPos = this.editor.cm.coordsChar({left: event.pageX, top: event.pageY}); this.editor.cm.setCursor(cursorPos); for (const image of images) { diff --git a/resources/js/markdown/codemirror.js b/resources/js/markdown/codemirror.js index dad999e7a..cd620137d 100644 --- a/resources/js/markdown/codemirror.js +++ b/resources/js/markdown/codemirror.js @@ -1,4 +1,4 @@ -import {provide as provideShortcuts} from "./shortcuts"; +import {provideKeyBindings} from "./shortcuts"; import {debounce} from "../services/util"; import Clipboard from "../services/clipboard"; @@ -28,15 +28,17 @@ export async function init(editor) { scroll: (event) => syncActive && onScrollDebounced(event) } - const cm = Code.markdownEditor(editor.config.inputEl, onViewUpdate, domEventHandlers); + const cm = Code.markdownEditor( + editor.config.inputEl, + onViewUpdate, + domEventHandlers, + provideKeyBindings(editor), + ); window.cm = cm; // Will force to remain as ltr for now due to issues when HTML is in editor. // TODO // cm.setOption('direction', 'ltr'); - // Register shortcuts - // TODO - // cm.setOption('extraKeys', provideShortcuts(editor, Code.getMetaKey())); // Handle image paste diff --git a/resources/js/markdown/shortcuts.js b/resources/js/markdown/shortcuts.js index 17ffe2fb3..08841e6c2 100644 --- a/resources/js/markdown/shortcuts.js +++ b/resources/js/markdown/shortcuts.js @@ -1,48 +1,57 @@ /** - * Provide shortcuts for the given codemirror instance. + * Provide shortcuts for the editor instance. * @param {MarkdownEditor} editor - * @param {String} metaKey * @returns {Object} */ -export function provide(editor, metaKey) { +function provide(editor) { const shortcuts = {}; // Insert Image shortcut - shortcuts[`${metaKey}-Alt-I`] = function(cm) { - const selectedText = cm.getSelection(); - const newText = `![${selectedText}](http://)`; - const cursorPos = cm.getCursor('from'); - cm.replaceSelection(newText); - cm.setCursor(cursorPos.line, cursorPos.ch + newText.length -1); - }; + shortcuts['Mod-Alt-i'] = () => editor.actions.insertImage(); // Save draft - shortcuts[`${metaKey}-S`] = cm => window.$events.emit('editor-save-draft'); + shortcuts['Mod-s'] = cm => window.$events.emit('editor-save-draft'); // Save page - shortcuts[`${metaKey}-Enter`] = cm => window.$events.emit('editor-save-page'); + shortcuts['Mod-Enter'] = cm => window.$events.emit('editor-save-page'); // Show link selector - shortcuts[`Shift-${metaKey}-K`] = cm => editor.actions.showLinkSelector(); + shortcuts['Shift-Mod-k'] = cm => editor.actions.showLinkSelector(); // Insert Link - shortcuts[`${metaKey}-K`] = cm => editor.actions.insertLink(); + shortcuts['Mod-k'] = cm => editor.actions.insertLink(); // FormatShortcuts - shortcuts[`${metaKey}-1`] = cm => editor.actions.replaceLineStart('##'); - shortcuts[`${metaKey}-2`] = cm => editor.actions.replaceLineStart('###'); - shortcuts[`${metaKey}-3`] = cm => editor.actions.replaceLineStart('####'); - shortcuts[`${metaKey}-4`] = cm => editor.actions.replaceLineStart('#####'); - shortcuts[`${metaKey}-5`] = cm => editor.actions.replaceLineStart(''); - shortcuts[`${metaKey}-D`] = cm => editor.actions.replaceLineStart(''); - shortcuts[`${metaKey}-6`] = cm => editor.actions.replaceLineStart('>'); - shortcuts[`${metaKey}-Q`] = cm => editor.actions.replaceLineStart('>'); - shortcuts[`${metaKey}-7`] = cm => editor.actions.wrapSelection('\n```\n', '\n```'); - shortcuts[`${metaKey}-8`] = cm => editor.actions.wrapSelection('`', '`'); - shortcuts[`Shift-${metaKey}-E`] = cm => editor.actions.wrapSelection('`', '`'); - shortcuts[`${metaKey}-9`] = cm => editor.actions.cycleCalloutTypeAtSelection(); - shortcuts[`${metaKey}-P`] = cm => editor.actions.replaceLineStart('-') - shortcuts[`${metaKey}-O`] = cm => editor.actions.replaceLineStartForOrderedList() + shortcuts['Mod-1'] = cm => editor.actions.replaceLineStart('##'); + shortcuts['Mod-2'] = cm => editor.actions.replaceLineStart('###'); + shortcuts['Mod-3'] = cm => editor.actions.replaceLineStart('####'); + shortcuts['Mod-4'] = cm => editor.actions.replaceLineStart('#####'); + shortcuts['Mod-5'] = cm => editor.actions.replaceLineStart(''); + shortcuts['Mod-d'] = cm => editor.actions.replaceLineStart(''); + shortcuts['Mod-6'] = cm => editor.actions.replaceLineStart('>'); + shortcuts['Mod-q'] = cm => editor.actions.replaceLineStart('>'); + shortcuts['Mod-7'] = cm => editor.actions.wrapSelection('\n```\n', '\n```'); + shortcuts['Mod-8'] = cm => editor.actions.wrapSelection('`', '`'); + shortcuts['Shift-Mod-e'] = cm => editor.actions.wrapSelection('`', '`'); + shortcuts['Mod-9'] = cm => editor.actions.cycleCalloutTypeAtSelection(); + shortcuts['Mod-p'] = cm => editor.actions.replaceLineStart('-') + shortcuts['Mod-o'] = cm => editor.actions.replaceLineStartForOrderedList() return shortcuts; +} + +/** + * Get the editor shortcuts in CodeMirror keybinding format. + * @param {MarkdownEditor} editor + * @return {{key: String, run: function, preventDefault: boolean}[]} + */ +export function provideKeyBindings(editor) { + const shortcuts= provide(editor); + const keyBindings = []; + + for (const [shortcut, action] of Object.entries(shortcuts)) { + keyBindings.push({key: shortcut, run: action, preventDefault: true}); + } + + return keyBindings; } \ No newline at end of file From 9813c9472098453ff04352b9a454c5e923e695e3 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 11 Apr 2023 13:16:04 +0100 Subject: [PATCH 24/76] Made a start on updating editor actions --- resources/js/markdown/actions.js | 209 +++++++++++++++-------------- resources/js/markdown/editor.js | 2 +- resources/js/markdown/shortcuts.js | 2 +- resources/sass/_codemirror.scss | 5 + 4 files changed, 118 insertions(+), 100 deletions(-) diff --git a/resources/js/markdown/actions.js b/resources/js/markdown/actions.js index dfbe89c5a..15f1c1e7f 100644 --- a/resources/js/markdown/actions.js +++ b/resources/js/markdown/actions.js @@ -29,72 +29,57 @@ export class Actions { } showImageInsert() { - // TODO - const cursorPos = this.editor.cm.getCursor('from'); /** @type {ImageManager} **/ const imageManager = window.$components.first('image-manager'); + imageManager.show(image => { const imageUrl = image.thumbs.display || image.url; - let selectedText = this.editor.cm.getSelection(); - let newText = "[![" + (selectedText || image.name) + "](" + imageUrl + ")](" + image.url + ")"; - this.editor.cm.focus(); - this.editor.cm.replaceSelection(newText); - this.editor.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length); + const selectedText = this.#getSelectionText(); + const newText = "[![" + (selectedText || image.name) + "](" + imageUrl + ")](" + image.url + ")"; + this.#replaceSelection(newText, newText.length); }, 'gallery'); } insertImage() { - // TODO - const selectedText = this.editor.cm.getSelection(); - const newText = `![${selectedText}](http://)`; - const cursorPos = this.editor.cm.getCursor('from'); - this.editor.cm.replaceSelection(newText); - this.editor.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length -1); + const newText = `![${this.#getSelectionText()}](http://)`; + this.#replaceSelection(newText, newText.length - 1); } insertLink() { - // TODO - const cursorPos = this.editor.cm.getCursor('from'); - const selectedText = this.editor.cm.getSelection() || ''; + const selectedText = this.#getSelectionText(); const newText = `[${selectedText}]()`; - this.editor.cm.focus(); - this.editor.cm.replaceSelection(newText); const cursorPosDiff = (selectedText === '') ? -3 : -1; - this.editor.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length+cursorPosDiff); + this.#replaceSelection(newText, newText.length+cursorPosDiff); } showImageManager() { - // TODO - const cursorPos = this.editor.cm.getCursor('from'); + const selectionRange = this.#getSelectionRange(); /** @type {ImageManager} **/ const imageManager = window.$components.first('image-manager'); imageManager.show(image => { - this.insertDrawing(image, cursorPos); + this.#insertDrawing(image, selectionRange); }, 'drawio'); } // Show the popup link selector and insert a link when finished showLinkSelector() { - // TODO - const cursorPos = this.editor.cm.getCursor('from'); + const selectionRange = this.#getSelectionRange(); + /** @type {EntitySelectorPopup} **/ const selector = window.$components.first('entity-selector-popup'); selector.show(entity => { - let selectedText = this.editor.cm.getSelection() || entity.name; - let newText = `[${selectedText}](${entity.link})`; - this.editor.cm.focus(); - this.editor.cm.replaceSelection(newText); - this.editor.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length); + const selectedText = this.#getSelectionText(selectionRange) || entity.name; + const newText = `[${selectedText}](${entity.link})`; + this.#replaceSelection(newText, newText.length, selectionRange); }); } // Show draw.io if enabled and handle save. startDrawing() { - // TODO const url = this.editor.config.drawioUrl; if (!url) return; - const cursorPos = this.editor.cm.getCursor('from'); + const selectionRange = this.#getSelectionRange(); DrawIO.show(url,() => { return Promise.resolve(''); @@ -106,7 +91,7 @@ export class Actions { }; window.$http.post("/images/drawio", data).then(resp => { - this.insertDrawing(resp.data, cursorPos); + this.#insertDrawing(resp.data, selectionRange); DrawIO.close(); }).catch(err => { this.handleDrawingUploadError(err); @@ -114,12 +99,9 @@ export class Actions { }); } - insertDrawing(image, originalCursor) { - // TODO + #insertDrawing(image, originalSelectionRange) { const newText = `
    `; - this.editor.cm.focus(); - this.editor.cm.replaceSelection(newText); - this.editor.cm.setCursor(originalCursor.line, originalCursor.ch + newText.length); + this.#replaceSelection(newText, newText.length, originalSelectionRange); } // Show draw.io if enabled and handle save. @@ -161,7 +143,6 @@ export class Actions { } handleDrawingUploadError(error) { - // TODO if (error.status === 413) { window.$events.emit('error', this.editor.config.text.serverUploadLimit); } else { @@ -172,7 +153,6 @@ export class Actions { // Make the editor full screen fullScreen() { - // TODO const container = this.editor.config.container; const alreadyFullscreen = container.classList.contains('fullscreen'); container.classList.toggle('fullscreen', !alreadyFullscreen); @@ -181,35 +161,37 @@ export class Actions { // Scroll to a specified text scrollToText(searchText) { - // TODO if (!searchText) { return; } - const content = this.editor.cm.getValue(); - const lines = content.split(/\r?\n/); - let lineNumber = lines.findIndex(line => { - return line && line.indexOf(searchText) !== -1; - }); + const text = this.editor.cm.state.doc; + let lineCount = 1; + let scrollToLine = -1; + for (const line of text.iterLines()) { + if (line.includes(searchText)) { + scrollToLine = lineCount; + break; + } + lineCount++; + } - if (lineNumber === -1) { + if (scrollToLine === -1) { return; } - this.editor.cm.scrollIntoView({ - line: lineNumber, - }, 200); - this.editor.cm.focus(); - // set the cursor location. - this.editor.cm.setCursor({ - line: lineNumber, - char: lines[lineNumber].length - }) + const line = text.line(scrollToLine); + this.editor.cm.dispatch({ + selection: {anchor: line.from, head: line.to}, + scrollIntoView: true, + }); + this.focus(); } focus() { - // TODO - this.editor.cm.focus(); + if (!this.editor.cm.hasFocus) { + this.editor.cm.focus(); + } } /** @@ -217,8 +199,7 @@ export class Actions { * @param {String} content */ insertContent(content) { - // TODO - this.editor.cm.replaceSelection(content); + this.#replaceSelection(content, content.length); } /** @@ -404,44 +385,6 @@ export class Actions { } } - /** - * Handle image upload and add image into markdown content - * @param {File} file - */ - uploadImage(file) { - // TODO - if (file === null || file.type.indexOf('image') !== 0) return; - let ext = 'png'; - - if (file.name) { - let fileNameMatches = file.name.match(/\.(.+)$/); - if (fileNameMatches.length > 1) ext = fileNameMatches[1]; - } - - // Insert image into markdown - const id = "image-" + Math.random().toString(16).slice(2); - const placeholderImage = window.baseUrl(`/loading.gif#upload${id}`); - const selectedText = this.editor.cm.getSelection(); - const placeHolderText = `![${selectedText}](${placeholderImage})`; - const cursor = this.editor.cm.getCursor(); - this.editor.cm.replaceSelection(placeHolderText); - this.editor.cm.setCursor({line: cursor.line, ch: cursor.ch + selectedText.length + 3}); - - const remoteFilename = "image-" + Date.now() + "." + ext; - const formData = new FormData(); - formData.append('file', file, remoteFilename); - formData.append('uploaded_to', this.editor.config.pageId); - - window.$http.post('/images/gallery', formData).then(resp => { - const newContent = `[![${selectedText}](${resp.data.thumbs.display})](${resp.data.url})`; - this.findAndReplaceContent(placeHolderText, newContent); - }).catch(err => { - window.$events.emit('error', this.editor.config.text.imageUploadError); - this.findAndReplaceContent(placeHolderText, selectedText); - console.log(err); - }); - } - syncDisplayPosition(event) { // Thanks to http://liuhao.im/english/2015/11/10/the-sync-scroll-of-markdown-editor-in-javascript.html const scrollEl = event.target; @@ -485,7 +428,77 @@ export class Actions { const cursorPos = this.editor.cm.coordsChar({left: event.pageX, top: event.pageY}); this.editor.cm.setCursor(cursorPos); for (const image of images) { - this.uploadImage(image); + this.#uploadImage(image); } } + + /** + * Handle image upload and add image into markdown content + * @param {File} file + */ + #uploadImage(file) { + // TODO + if (file === null || file.type.indexOf('image') !== 0) return; + let ext = 'png'; + + if (file.name) { + let fileNameMatches = file.name.match(/\.(.+)$/); + if (fileNameMatches.length > 1) ext = fileNameMatches[1]; + } + + // Insert image into markdown + const id = "image-" + Math.random().toString(16).slice(2); + const placeholderImage = window.baseUrl(`/loading.gif#upload${id}`); + const selectedText = this.editor.cm.getSelection(); + const placeHolderText = `![${selectedText}](${placeholderImage})`; + const cursor = this.editor.cm.getCursor(); + this.editor.cm.replaceSelection(placeHolderText); + this.editor.cm.setCursor({line: cursor.line, ch: cursor.ch + selectedText.length + 3}); + + const remoteFilename = "image-" + Date.now() + "." + ext; + const formData = new FormData(); + formData.append('file', file, remoteFilename); + formData.append('uploaded_to', this.editor.config.pageId); + + window.$http.post('/images/gallery', formData).then(resp => { + const newContent = `[![${selectedText}](${resp.data.thumbs.display})](${resp.data.url})`; + this.findAndReplaceContent(placeHolderText, newContent); + }).catch(err => { + window.$events.emit('error', this.editor.config.text.imageUploadError); + this.findAndReplaceContent(placeHolderText, selectedText); + console.log(err); + }); + } + + /** + * Replace the current selection and focus the editor. + * Takes an offset for the cursor, after the change, relative to the start of the provided string. + * Can be provided a selection range to use instead of the current selection range. + * @param {String} newContent + * @param {Number} cursorOffset + * @param {?SelectionRange} selectionRange + */ + #replaceSelection(newContent, cursorOffset = 0, selectionRange = null) { + selectionRange = selectionRange || this.editor.cm.state.selection.main; + this.editor.cm.dispatch({ + changes: {from: selectionRange.from, to: selectionRange.to, insert: newContent}, + selection: {anchor: selectionRange.from + cursorOffset}, + }); + + this.focus(); + } + + /** + * Get the text content of the main current selection. + * @param {SelectionRange} selectionRange + * @return {string} + */ + #getSelectionText(selectionRange = null) { + selectionRange = selectionRange || this.#getSelectionRange(); + return this.editor.cm.state.sliceDoc(selectionRange.from, selectionRange.to); + } + + #getSelectionRange() { + return this.editor.cm.state.selection.main; + } } \ No newline at end of file diff --git a/resources/js/markdown/editor.js b/resources/js/markdown/editor.js index 1cf4cef2b..cb5bf7d1a 100644 --- a/resources/js/markdown/editor.js +++ b/resources/js/markdown/editor.js @@ -49,6 +49,6 @@ export async function init(config) { * @property {Display} display * @property {Markdown} markdown * @property {Actions} actions - * @property {CodeMirror} cm + * @property {EditorView} cm * @property {Settings} settings */ \ No newline at end of file diff --git a/resources/js/markdown/shortcuts.js b/resources/js/markdown/shortcuts.js index 08841e6c2..336b276d1 100644 --- a/resources/js/markdown/shortcuts.js +++ b/resources/js/markdown/shortcuts.js @@ -7,7 +7,7 @@ function provide(editor) { const shortcuts = {}; // Insert Image shortcut - shortcuts['Mod-Alt-i'] = () => editor.actions.insertImage(); + shortcuts['Mod-Alt-i'] = cm => editor.actions.insertImage(); // Save draft shortcuts['Mod-s'] = cm => window.$events.emit('editor-save-draft'); diff --git a/resources/sass/_codemirror.scss b/resources/sass/_codemirror.scss index 330923d4f..1dee39cd2 100644 --- a/resources/sass/_codemirror.scss +++ b/resources/sass/_codemirror.scss @@ -412,6 +412,11 @@ span.CodeMirror-selectedtext { background: none; } /** * Custom BookStack overrides */ +.cm-editor { + @include lightDark(background-color, #FFF, #000); +} + +// TODO - All below are old .CodeMirror, .CodeMirror pre { font-size: 12px; } From 32c765d0c3ef0be89e10f9e38b13e4f82b3fd6c3 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 13 Apr 2023 12:51:52 +0100 Subject: [PATCH 25/76] Updated another range of actions for cm6 --- resources/js/markdown/actions.js | 212 ++++++++++++++++++------------- resources/sass/_forms.scss | 2 + 2 files changed, 126 insertions(+), 88 deletions(-) diff --git a/resources/js/markdown/actions.js b/resources/js/markdown/actions.js index 15f1c1e7f..d140bb284 100644 --- a/resources/js/markdown/actions.js +++ b/resources/js/markdown/actions.js @@ -13,7 +13,7 @@ export class Actions { } updateAndRender() { - const content = this.editor.cm.state.doc.toString(); + const content = this.#getText(); this.editor.config.inputEl.value = content; const html = this.editor.markdown.render(content); @@ -106,13 +106,12 @@ export class Actions { // Show draw.io if enabled and handle save. editDrawing(imgContainer) { - // TODO const drawioUrl = this.editor.config.drawioUrl; if (!drawioUrl) { return; } - const cursorPos = this.editor.cm.getCursor('from'); + const selectionRange = this.#getSelectionRange(); const drawingId = imgContainer.getAttribute('drawio-diagram'); DrawIO.show(drawioUrl, () => { @@ -126,15 +125,13 @@ export class Actions { window.$http.post("/images/drawio", data).then(resp => { const newText = `
    `; - const newContent = this.editor.cm.getValue().split('\n').map(line => { + const newContent = this.#getText().split('\n').map(line => { if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) { return newText; } return line; }).join('\n'); - this.editor.cm.setValue(newContent); - this.editor.cm.setCursor(cursorPos); - this.editor.cm.focus(); + this.#setText(newContent, selectionRange); DrawIO.close(); }).catch(err => { this.handleDrawingUploadError(err); @@ -207,12 +204,13 @@ export class Actions { * @param {String} content */ prependContent(content) { - // TODO - const cursorPos = this.editor.cm.getCursor('from'); - const newContent = content + '\n' + this.editor.cm.getValue(); - this.editor.cm.setValue(newContent); - const prependLineCount = content.split('\n').length; - this.editor.cm.setCursor(cursorPos.line + prependLineCount, cursorPos.ch); + content = this.#cleanTextForEditor(content); + const selectionRange = this.#getSelectionRange(); + this.editor.cm.dispatch({ + changes: {from: 0, to: 0, insert: content + '\n'}, + selection: {anchor: selectionRange.from + content.length + 1} + }); + this.focus(); } /** @@ -220,11 +218,11 @@ export class Actions { * @param {String} content */ appendContent(content) { - // TODO - const cursorPos = this.editor.cm.getCursor('from'); - const newContent = this.editor.cm.getValue() + '\n' + content; - this.editor.cm.setValue(newContent); - this.editor.cm.setCursor(cursorPos.line, cursorPos.ch); + content = this.#cleanTextForEditor(content); + this.editor.cm.dispatch({ + changes: {from: this.editor.cm.state.doc.length, insert: '\n' + content}, + }); + this.focus(); } /** @@ -232,20 +230,7 @@ export class Actions { * @param {String} content */ replaceContent(content) { - // TODO - this.editor.cm.setValue(content); - } - - /** - * @param {String|RegExp} search - * @param {String} replace - */ - findAndReplaceContent(search, replace) { - // TODO - const text = this.editor.cm.getValue(); - const cursor = this.editor.cm.listSelections(); - this.editor.cm.setValue(text.replace(search, replace)); - this.editor.cm.setSelections(cursor); + this.#setText(content) } /** @@ -253,53 +238,34 @@ export class Actions { * @param {String} newStart */ replaceLineStart(newStart) { - // TODO - const cursor = this.editor.cm.getCursor(); - let lineContent = this.editor.cm.getLine(cursor.line); - const lineLen = lineContent.length; + const selectionRange = this.#getSelectionRange(); + const line = this.editor.cm.state.doc.lineAt(selectionRange.from); + + const lineContent = line.text; const lineStart = lineContent.split(' ')[0]; // Remove symbol if already set if (lineStart === newStart) { - lineContent = lineContent.replace(`${newStart} `, ''); - this.editor.cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen}); - this.editor.cm.setCursor({line: cursor.line, ch: cursor.ch - (newStart.length + 1)}); + const newLineContent = lineContent.replace(`${newStart} `, ''); + this.editor.cm.dispatch({ + changes: {from: line.from, to: line.to, insert: newLineContent}, + selection: {anchor: selectionRange.from + (newLineContent.length - lineContent.length)} + }); return; } - const alreadySymbol = /^[#>`]/.test(lineStart); - let posDif = 0; - if (alreadySymbol) { - posDif = newStart.length - lineStart.length; - lineContent = lineContent.replace(lineStart, newStart).trim(); - } else if (newStart !== '') { - posDif = newStart.length + 1; - lineContent = newStart + ' ' + lineContent; - } - this.editor.cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen}); - this.editor.cm.setCursor({line: cursor.line, ch: cursor.ch + posDif}); - } - - /** - * Wrap the line in the given start and end contents. - * @param {String} start - * @param {String} end - */ - wrapLine(start, end) { - // TODO - const cursor = this.editor.cm.getCursor(); - const lineContent = this.editor.cm.getLine(cursor.line); - const lineLen = lineContent.length; let newLineContent = lineContent; - - if (lineContent.indexOf(start) === 0 && lineContent.slice(-end.length) === end) { - newLineContent = lineContent.slice(start.length, lineContent.length - end.length); - } else { - newLineContent = `${start}${lineContent}${end}`; + const alreadySymbol = /^[#>`]/.test(lineStart); + if (alreadySymbol) { + newLineContent = lineContent.replace(lineStart, newStart).trim(); + } else if (newStart !== '') { + newLineContent = newStart + ' ' + lineContent; } - this.editor.cm.replaceRange(newLineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen}); - this.editor.cm.setCursor({line: cursor.line, ch: cursor.ch + start.length}); + this.editor.cm.dispatch({ + changes: {from: line.from, to: line.to, insert: newLineContent}, + selection: {anchor: selectionRange.from + (newLineContent.length - lineContent.length)} + }); } /** @@ -308,28 +274,25 @@ export class Actions { * @param {String} end */ wrapSelection(start, end) { - // TODO - const selection = this.editor.cm.getSelection(); - if (selection === '') return this.wrapLine(start, end); + const selectionRange = this.#getSelectionRange(); + const selectionText = this.#getSelectionText(selectionRange); + if (!selectionText) return this.#wrapLine(start, end); - let newSelection = selection; - const frontDiff = 0; - let endDiff; + let newSelectionText = selectionText; + let newRange; - if (selection.indexOf(start) === 0 && selection.slice(-end.length) === end) { - newSelection = selection.slice(start.length, selection.length - end.length); - endDiff = -(end.length + start.length); + if (selectionText.startsWith(start) && selectionText.endsWith(end)) { + newSelectionText = selectionText.slice(start.length, selectionText.length - end.length); + newRange = selectionRange.extend(selectionRange.from, selectionRange.to - (start.length + end.length)); } else { - newSelection = `${start}${selection}${end}`; - endDiff = start.length + end.length; + newSelectionText = `${start}${selectionText}${end}`; + newRange = selectionRange.extend(selectionRange.from, selectionRange.to + (start.length + end.length)); } - const selections = this.editor.cm.listSelections()[0]; - this.editor.cm.replaceSelection(newSelection); - const headFirst = selections.head.ch <= selections.anchor.ch; - selections.head.ch += headFirst ? frontDiff : endDiff; - selections.anchor.ch += headFirst ? endDiff : frontDiff; - this.editor.cm.setSelections([selections]); + this.editor.cm.dispatch({ + changes: {from: selectionRange.from, to: selectionRange.to, insert: newSelectionText}, + selection: {anchor: newRange.anchor, head: newRange.head}, + }); } replaceLineStartForOrderedList() { @@ -462,14 +425,37 @@ export class Actions { window.$http.post('/images/gallery', formData).then(resp => { const newContent = `[![${selectedText}](${resp.data.thumbs.display})](${resp.data.url})`; - this.findAndReplaceContent(placeHolderText, newContent); + this.#findAndReplaceContent(placeHolderText, newContent); }).catch(err => { window.$events.emit('error', this.editor.config.text.imageUploadError); - this.findAndReplaceContent(placeHolderText, selectedText); + this.#findAndReplaceContent(placeHolderText, selectedText); console.log(err); }); } + /** + * Get the current text of the editor instance. + * @return {string} + */ + #getText() { + return this.editor.cm.state.doc.toString(); + } + + /** + * Set the text of the current editor instance. + * @param {String} text + * @param {?SelectionRange} selectionRange + */ + #setText(text, selectionRange = null) { + selectionRange = selectionRange || this.#getSelectionRange(); + this.editor.cm.dispatch({ + changes: {from: 0, to: this.editor.cm.state.doc.length, insert: text}, + selection: {anchor: selectionRange.from}, + }); + + this.focus(); + } + /** * Replace the current selection and focus the editor. * Takes an offset for the cursor, after the change, relative to the start of the provided string. @@ -498,7 +484,57 @@ export class Actions { return this.editor.cm.state.sliceDoc(selectionRange.from, selectionRange.to); } + /** + * Get the range of the current main selection. + * @return {SelectionRange} + */ #getSelectionRange() { return this.editor.cm.state.selection.main; } + + /** + * Cleans the given text to work with the editor. + * Standardises line endings to what's expected. + * @param {String} text + * @return {String} + */ + #cleanTextForEditor(text) { + return text.replace(/\r\n|\r/g, "\n"); + } + + /** + * Find and replace the first occurrence of [search] with [replace] + * @param {String} search + * @param {String} replace + */ + #findAndReplaceContent(search, replace) { + const newText = this.#getText().replace(search, replace); + this.#setText(newText); + } + + /** + * Wrap the line in the given start and end contents. + * @param {String} start + * @param {String} end + */ + #wrapLine(start, end) { + const selectionRange = this.#getSelectionRange(); + const line = this.editor.cm.state.doc.lineAt(selectionRange.from); + const lineContent = line.text; + let newLineContent; + let lineOffset = 0; + + if (lineContent.startsWith(start) && lineContent.endsWith(end)) { + newLineContent = lineContent.slice(start.length, lineContent.length - end.length); + lineOffset = -(start.length); + } else { + newLineContent = `${start}${lineContent}${end}`; + lineOffset = start.length; + } + + this.editor.cm.dispatch({ + changes: {from: line.from, to: line.to, insert: newLineContent}, + selection: {anchor: selectionRange.from + lineOffset} + }); + } } \ No newline at end of file diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss index b7b1b6d4d..84825ddc6 100644 --- a/resources/sass/_forms.scss +++ b/resources/sass/_forms.scss @@ -75,6 +75,7 @@ @include lightDark(border-color, #ddd, #000); position: relative; flex: 1; + min-width: 0; } .markdown-editor-wrap + .markdown-editor-wrap { flex-basis: 50%; @@ -84,6 +85,7 @@ .markdown-editor-wrap .cm-editor { flex: 1; + max-width: 100%; } .markdown-panel-divider { From 6f45d34bf8cba306477d5ab34ba2fdd8fbbe0d87 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 13 Apr 2023 17:18:32 +0100 Subject: [PATCH 26/76] Finished update pass of all md editor actions to cm6 --- resources/js/markdown/actions.js | 96 ++++++++++++++--------------- resources/js/markdown/codemirror.js | 69 ++++++++++----------- 2 files changed, 78 insertions(+), 87 deletions(-) diff --git a/resources/js/markdown/actions.js b/resources/js/markdown/actions.js index d140bb284..3aa6b5e81 100644 --- a/resources/js/markdown/actions.js +++ b/resources/js/markdown/actions.js @@ -296,10 +296,11 @@ export class Actions { } replaceLineStartForOrderedList() { - // TODO - const cursor = this.editor.cm.getCursor(); - const prevLineContent = this.editor.cm.getLine(cursor.line - 1) || ''; - const listMatch = prevLineContent.match(/^(\s*)(\d)([).])\s/) || []; + const selectionRange = this.#getSelectionRange(); + const line = this.editor.cm.state.doc.lineAt(selectionRange.from); + const prevLine = this.editor.cm.state.doc.line(line.number - 1); + + const listMatch = prevLine.text.match(/^(\s*)(\d)([).])\s/) || []; const number = (Number(listMatch[2]) || 0) + 1; const whiteSpace = listMatch[1] || ''; @@ -314,37 +315,28 @@ export class Actions { * Creates a callout block if none existing, and removes it if cycling past the danger type. */ cycleCalloutTypeAtSelection() { - // TODO - const selectionRange = this.editor.cm.listSelections()[0]; - const lineContent = this.editor.cm.getLine(selectionRange.anchor.line); - const lineLength = lineContent.length; - const contentRange = { - anchor: {line: selectionRange.anchor.line, ch: 0}, - head: {line: selectionRange.anchor.line, ch: lineLength}, - }; + const selectionRange = this.#getSelectionRange(); + const line = this.editor.cm.state.doc.lineAt(selectionRange.from); const formats = ['info', 'success', 'warning', 'danger']; const joint = formats.join('|'); const regex = new RegExp(`class="((${joint})\\s+callout|callout\\s+(${joint}))"`, 'i'); - const matches = regex.exec(lineContent); + const matches = regex.exec(line.text); const format = (matches ? (matches[2] || matches[3]) : '').toLowerCase(); if (format === formats[formats.length - 1]) { - this.wrapLine(`

    `, '

    '); + this.#wrapLine(`

    `, '

    '); } else if (format === '') { - this.wrapLine('

    ', '

    '); + this.#wrapLine('

    ', '

    '); } else { const newFormatIndex = formats.indexOf(format) + 1; const newFormat = formats[newFormatIndex]; - const newContent = lineContent.replace(matches[0], matches[0].replace(format, newFormat)); - this.editor.cm.replaceRange(newContent, contentRange.anchor, contentRange.head); - - const chDiff = newContent.length - lineContent.length; - selectionRange.anchor.ch += chDiff; - if (selectionRange.anchor !== selectionRange.head) { - selectionRange.head.ch += chDiff; - } - this.editor.cm.setSelection(selectionRange.anchor, selectionRange.head); + const newContent = line.text.replace(matches[0], matches[0].replace(format, newFormat)); + const lineDiff = newContent.length - line.text.length; + this.editor.cm.dispatch({ + changes: {from: line.from, to: line.to, insert: newContent}, + selection: {anchor: selectionRange.anchor + lineDiff, head: selectionRange.head + lineDiff}, + }); } } @@ -372,38 +364,43 @@ export class Actions { * @param {Number} posX * @param {Number} posY */ - insertTemplate(templateId, posX, posY) { - // TODO - const cursorPos = this.editor.cm.coordsChar({left: posX, top: posY}); - this.editor.cm.setCursor(cursorPos); - window.$http.get(`/templates/${templateId}`).then(resp => { - const content = resp.data.markdown || resp.data.html; - this.editor.cm.replaceSelection(content); + async insertTemplate(templateId, posX, posY) { + const cursorPos = this.editor.cm.posAtCoords({x: posX, y: posY}, false); + const {data} = await window.$http.get(`/templates/${templateId}`); + const content = data.markdown || data.html; + this.editor.cm.dispatch({ + changes: {from: cursorPos, to: cursorPos, insert: content}, + selection: {anchor: cursorPos}, }); } /** - * Insert multiple images from the clipboard. + * Insert multiple images from the clipboard from an event at the provided + * screen coordinates (Typically form a paste event). * @param {File[]} images + * @param {Number} posX + * @param {Number} posY */ - insertClipboardImages(images) { - // TODO - const cursorPos = this.editor.cm.coordsChar({left: event.pageX, top: event.pageY}); - this.editor.cm.setCursor(cursorPos); + insertClipboardImages(images, posX, posY) { + const cursorPos = this.editor.cm.posAtCoords({x: posX, y: posY}, false); for (const image of images) { - this.#uploadImage(image); + this.uploadImage(image, cursorPos); } } /** * Handle image upload and add image into markdown content * @param {File} file + * @param {?Number} position */ - #uploadImage(file) { - // TODO + async uploadImage(file, position= null) { if (file === null || file.type.indexOf('image') !== 0) return; let ext = 'png'; + if (position === null) { + position = this.#getSelectionRange().from; + } + if (file.name) { let fileNameMatches = file.name.match(/\.(.+)$/); if (fileNameMatches.length > 1) ext = fileNameMatches[1]; @@ -412,25 +409,26 @@ export class Actions { // Insert image into markdown const id = "image-" + Math.random().toString(16).slice(2); const placeholderImage = window.baseUrl(`/loading.gif#upload${id}`); - const selectedText = this.editor.cm.getSelection(); - const placeHolderText = `![${selectedText}](${placeholderImage})`; - const cursor = this.editor.cm.getCursor(); - this.editor.cm.replaceSelection(placeHolderText); - this.editor.cm.setCursor({line: cursor.line, ch: cursor.ch + selectedText.length + 3}); + const placeHolderText = `![](${placeholderImage})`; + this.editor.cm.dispatch({ + changes: {from: position, to: position, insert: placeHolderText}, + selection: {anchor: position}, + }); const remoteFilename = "image-" + Date.now() + "." + ext; const formData = new FormData(); formData.append('file', file, remoteFilename); formData.append('uploaded_to', this.editor.config.pageId); - window.$http.post('/images/gallery', formData).then(resp => { - const newContent = `[![${selectedText}](${resp.data.thumbs.display})](${resp.data.url})`; + try { + const {data} = await window.$http.post('/images/gallery', formData); + const newContent = `[![](${data.thumbs.display})](${data.url})`; this.#findAndReplaceContent(placeHolderText, newContent); - }).catch(err => { + } catch (err) { window.$events.emit('error', this.editor.config.text.imageUploadError); - this.#findAndReplaceContent(placeHolderText, selectedText); + this.#findAndReplaceContent(placeHolderText, ''); console.log(err); - }); + } } /** diff --git a/resources/js/markdown/codemirror.js b/resources/js/markdown/codemirror.js index cd620137d..dbf1925c0 100644 --- a/resources/js/markdown/codemirror.js +++ b/resources/js/markdown/codemirror.js @@ -25,7 +25,37 @@ export async function init(editor) { const domEventHandlers = { // Handle scroll to sync display view - scroll: (event) => syncActive && onScrollDebounced(event) + scroll: (event) => syncActive && onScrollDebounced(event), + // Handle image & content drag n drop + drop: (event) => { + const templateId = event.dataTransfer.getData('bookstack/template'); + if (templateId) { + event.preventDefault(); + editor.actions.insertTemplate(templateId, event.pageX, event.pageY); + } + + const clipboard = new Clipboard(event.dataTransfer); + const clipboardImages = clipboard.getImages(); + if (clipboardImages.length > 0) { + event.stopPropagation(); + event.preventDefault(); + editor.actions.insertClipboardImages(clipboardImages, event.pageX, event.pageY); + } + }, + // Handle image paste + paste: (event) => { + const clipboard = new Clipboard(event.clipboardData || event.dataTransfer); + + // Don't handle the event ourselves if no items exist of contains table-looking data + if (!clipboard.hasItems() || clipboard.containsTabularData()) { + return; + } + + const images = clipboard.getImages(); + for (const image of images) { + editor.actions.uploadImage(image); + } + } } const cm = Code.markdownEditor( @@ -40,42 +70,5 @@ export async function init(editor) { // TODO // cm.setOption('direction', 'ltr'); - - // Handle image paste - // TODO - // cm.on('paste', (cm, event) => { - // const clipboard = new Clipboard(event.clipboardData || event.dataTransfer); - // - // // Don't handle the event ourselves if no items exist of contains table-looking data - // if (!clipboard.hasItems() || clipboard.containsTabularData()) { - // return; - // } - // - // const images = clipboard.getImages(); - // for (const image of images) { - // editor.actions.uploadImage(image); - // } - // }); - - // Handle image & content drag n drop - // TODO - // cm.on('drop', (cm, event) => { - // - // const templateId = event.dataTransfer.getData('bookstack/template'); - // if (templateId) { - // event.preventDefault(); - // editor.actions.insertTemplate(templateId, event.pageX, event.pageY); - // } - // - // const clipboard = new Clipboard(event.dataTransfer); - // const clipboardImages = clipboard.getImages(); - // if (clipboardImages.length > 0) { - // event.stopPropagation(); - // event.preventDefault(); - // editor.actions.insertClipboardImages(clipboardImages); - // } - // - // }); - return cm; } \ No newline at end of file From fdda813d5f623100378d29f8169b0067fe3f2dd9 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 13 Apr 2023 17:38:11 +0100 Subject: [PATCH 27/76] Cleaned up change handling in cm6 editor action handling --- resources/js/markdown/actions.js | 92 ++++++++++++++++---------------- 1 file changed, 45 insertions(+), 47 deletions(-) diff --git a/resources/js/markdown/actions.js b/resources/js/markdown/actions.js index 3aa6b5e81..a73cddd30 100644 --- a/resources/js/markdown/actions.js +++ b/resources/js/markdown/actions.js @@ -178,10 +178,7 @@ export class Actions { } const line = text.line(scrollToLine); - this.editor.cm.dispatch({ - selection: {anchor: line.from, head: line.to}, - scrollIntoView: true, - }); + this.#setSelection(line.from, line.to, true); this.focus(); } @@ -206,10 +203,8 @@ export class Actions { prependContent(content) { content = this.#cleanTextForEditor(content); const selectionRange = this.#getSelectionRange(); - this.editor.cm.dispatch({ - changes: {from: 0, to: 0, insert: content + '\n'}, - selection: {anchor: selectionRange.from + content.length + 1} - }); + const selectFrom = selectionRange.from + content.length + 1; + this.#dispatchChange(0, 0, content + '\n', selectFrom); this.focus(); } @@ -219,9 +214,7 @@ export class Actions { */ appendContent(content) { content = this.#cleanTextForEditor(content); - this.editor.cm.dispatch({ - changes: {from: this.editor.cm.state.doc.length, insert: '\n' + content}, - }); + this.#dispatchChange(this.editor.cm.state.doc.length, '\n' + content); this.focus(); } @@ -247,10 +240,8 @@ export class Actions { // Remove symbol if already set if (lineStart === newStart) { const newLineContent = lineContent.replace(`${newStart} `, ''); - this.editor.cm.dispatch({ - changes: {from: line.from, to: line.to, insert: newLineContent}, - selection: {anchor: selectionRange.from + (newLineContent.length - lineContent.length)} - }); + const selectFrom = selectionRange.from + (newLineContent.length - lineContent.length); + this.#dispatchChange(line.from, line.to, newLineContent, selectFrom); return; } @@ -262,10 +253,8 @@ export class Actions { newLineContent = newStart + ' ' + lineContent; } - this.editor.cm.dispatch({ - changes: {from: line.from, to: line.to, insert: newLineContent}, - selection: {anchor: selectionRange.from + (newLineContent.length - lineContent.length)} - }); + const selectFrom = selectionRange.from + (newLineContent.length - lineContent.length); + this.#dispatchChange(line.from, line.to, newLineContent, selectFrom); } /** @@ -289,10 +278,7 @@ export class Actions { newRange = selectionRange.extend(selectionRange.from, selectionRange.to + (start.length + end.length)); } - this.editor.cm.dispatch({ - changes: {from: selectionRange.from, to: selectionRange.to, insert: newSelectionText}, - selection: {anchor: newRange.anchor, head: newRange.head}, - }); + this.#dispatchChange(selectionRange.from, selectionRange.to, newSelectionText, newRange.anchor, newRange.head); } replaceLineStartForOrderedList() { @@ -333,10 +319,7 @@ export class Actions { const newFormat = formats[newFormatIndex]; const newContent = line.text.replace(matches[0], matches[0].replace(format, newFormat)); const lineDiff = newContent.length - line.text.length; - this.editor.cm.dispatch({ - changes: {from: line.from, to: line.to, insert: newContent}, - selection: {anchor: selectionRange.anchor + lineDiff, head: selectionRange.head + lineDiff}, - }); + this.#dispatchChange(line.from, line.to, newContent, selectionRange.anchor + lineDiff, selectionRange.head + lineDiff); } } @@ -368,10 +351,7 @@ export class Actions { const cursorPos = this.editor.cm.posAtCoords({x: posX, y: posY}, false); const {data} = await window.$http.get(`/templates/${templateId}`); const content = data.markdown || data.html; - this.editor.cm.dispatch({ - changes: {from: cursorPos, to: cursorPos, insert: content}, - selection: {anchor: cursorPos}, - }); + this.#dispatchChange(cursorPos, cursorPos, content, cursorPos); } /** @@ -410,10 +390,7 @@ export class Actions { const id = "image-" + Math.random().toString(16).slice(2); const placeholderImage = window.baseUrl(`/loading.gif#upload${id}`); const placeHolderText = `![](${placeholderImage})`; - this.editor.cm.dispatch({ - changes: {from: position, to: position, insert: placeHolderText}, - selection: {anchor: position}, - }); + this.#dispatchChange(position, position, placeHolderText, position); const remoteFilename = "image-" + Date.now() + "." + ext; const formData = new FormData(); @@ -446,11 +423,7 @@ export class Actions { */ #setText(text, selectionRange = null) { selectionRange = selectionRange || this.#getSelectionRange(); - this.editor.cm.dispatch({ - changes: {from: 0, to: this.editor.cm.state.doc.length, insert: text}, - selection: {anchor: selectionRange.from}, - }); - + this.#dispatchChange(0, this.editor.cm.state.doc.length, text, selectionRange.from); this.focus(); } @@ -464,11 +437,7 @@ export class Actions { */ #replaceSelection(newContent, cursorOffset = 0, selectionRange = null) { selectionRange = selectionRange || this.editor.cm.state.selection.main; - this.editor.cm.dispatch({ - changes: {from: selectionRange.from, to: selectionRange.to, insert: newContent}, - selection: {anchor: selectionRange.from + cursorOffset}, - }); - + this.#dispatchChange(selectionRange.from, selectionRange.to, newContent, selectionRange.from + cursorOffset); this.focus(); } @@ -530,9 +499,38 @@ export class Actions { lineOffset = start.length; } + this.#dispatchChange(line.from, line.to, newLineContent, selectionRange.from + lineOffset); + } + + /** + * Dispatch changes to the editor. + * @param {Number} from + * @param {?Number} to + * @param {?String} text + * @param {?Number} selectFrom + * @param {?Number} selectTo + */ + #dispatchChange(from, to = null, text = null, selectFrom = null, selectTo = null) { + const tr = {changes: {from, to: to, insert: text}}; + + if (selectFrom) { + tr.selection = {anchor: selectFrom}; + } + + this.editor.cm.dispatch(tr); + } + + /** + * Set the current selection range. + * Optionally will scroll the new range into view. + * @param {Number} from + * @param {Number} to + * @param {Boolean} scrollIntoView + */ + #setSelection(from, to, scrollIntoView = false) { this.editor.cm.dispatch({ - changes: {from: line.from, to: line.to, insert: newLineContent}, - selection: {anchor: selectionRange.from + lineOffset} + selection: {anchor: from, head: to}, + scrollIntoView, }); } } \ No newline at end of file From 257a7038788d7ce6183c946cf37ab2d27d114100 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 14 Apr 2023 14:08:40 +0100 Subject: [PATCH 28/76] Addressed existing cm6 todos - Updated clipboard handling - Removed old clipboard package for browser-native API. - Updated codemirror editor events to use new props for new data types. --- package-lock.json | 34 ------------ package.json | 1 - readme.md | 1 - resources/js/code/index.mjs | 54 +++++++++---------- resources/js/code/setups.js | 1 + resources/js/components/markdown-editor.js | 12 +---- resources/js/components/pointer.js | 18 ++++--- resources/js/markdown/codemirror.js | 7 ++- resources/js/services/clipboard.js | 6 ++- resources/sass/_codemirror.scss | 4 +- .../pages/parts/markdown-editor.blade.php | 2 +- resources/views/pages/parts/pointer.blade.php | 4 +- 12 files changed, 51 insertions(+), 93 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85489c970..241e5392f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "@codemirror/view": "^6.1.2", "@ssddanbrown/codemirror-lang-smarty": "^1.0.0", "@ssddanbrown/codemirror-lang-twig": "^1.0.0", - "clipboard": "^2.0.11", "codemirror": "^6.0.1", "dropzone": "^5.9.3", "markdown-it": "^13.0.1", @@ -810,16 +809,6 @@ "node": ">= 8.10.0" } }, - "node_modules/clipboard": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", - "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", - "dependencies": { - "good-listener": "^1.2.2", - "select": "^1.1.2", - "tiny-emitter": "^2.0.0" - } - }, "node_modules/cliui": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", @@ -912,11 +901,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delegate": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", - "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" - }, "node_modules/dropzone": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/dropzone/-/dropzone-5.9.3.tgz", @@ -1218,14 +1202,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/good-listener": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", - "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", - "dependencies": { - "delegate": "^3.1.2" - } - }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -2062,11 +2038,6 @@ "node": ">=12.0.0" } }, - "node_modules/select": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", - "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==" - }, "node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -2289,11 +2260,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tiny-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", - "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index 9ee57afa5..3579b2d25 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "@codemirror/view": "^6.1.2", "@ssddanbrown/codemirror-lang-smarty": "^1.0.0", "@ssddanbrown/codemirror-lang-twig": "^1.0.0", - "clipboard": "^2.0.11", "codemirror": "^6.0.1", "dropzone": "^5.9.3", "markdown-it": "^13.0.1", diff --git a/readme.md b/readme.md index 28822dd8e..f56b2c2bd 100644 --- a/readme.md +++ b/readme.md @@ -134,7 +134,6 @@ Note: This is not an exhaustive list of all libraries and projects that would be * [Sortable](https://github.com/SortableJS/Sortable) - _[MIT](https://github.com/SortableJS/Sortable/blob/master/LICENSE)_ * [Google Material Icons](https://github.com/google/material-design-icons) - _[Apache-2.0](https://github.com/google/material-design-icons/blob/master/LICENSE)_ * [Dropzone.js](http://www.dropzonejs.com/) - _[MIT](https://github.com/dropzone/dropzone/blob/main/LICENSE)_ -* [clipboard.js](https://clipboardjs.com/) - _[MIT](https://github.com/zenorocha/clipboard.js/blob/master/LICENSE)_ * [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists) - _[MIT](https://github.com/markdown-it/markdown-it/blob/master/LICENSE) and [ISC](https://github.com/revin/markdown-it-task-lists/blob/master/LICENSE)_ * [Dompdf](https://github.com/dompdf/dompdf) - _[LGPL v2.1](https://github.com/dompdf/dompdf/blob/master/LICENSE.LGPL)_ * [BarryVD/Dompdf](https://github.com/barryvdh/laravel-dompdf) - _[MIT](https://github.com/barryvdh/laravel-dompdf/blob/master/LICENSE)_ diff --git a/resources/js/code/index.mjs b/resources/js/code/index.mjs index 3fe4a6d86..dbed1c1e6 100644 --- a/resources/js/code/index.mjs +++ b/resources/js/code/index.mjs @@ -1,5 +1,5 @@ -import {EditorView, keymap} from "@codemirror/view" -import Clipboard from "clipboard/dist/clipboard.min"; +import {EditorView, keymap} from "@codemirror/view"; +import {copyTextToClipboard} from "../services/clipboard.js" // Modes import {viewer, editor} from "./setups.js"; @@ -57,28 +57,23 @@ function highlightElem(elem) { /** * Add a button to a CodeMirror instance which copies the contents to the clipboard upon click. - * @param cmInstance + * @param {EditorView} editorView */ -function addCopyIcon(cmInstance) { - // TODO - // const copyIcon = ``; - // const copyButton = document.createElement('div'); - // copyButton.classList.add('CodeMirror-copy'); - // copyButton.innerHTML = copyIcon; - // cmInstance.display.wrapper.appendChild(copyButton); - // - // const clipboard = new Clipboard(copyButton, { - // text: function(trigger) { - // return cmInstance.getValue() - // } - // }); - // - // clipboard.on('success', event => { - // copyButton.classList.add('success'); - // setTimeout(() => { - // copyButton.classList.remove('success'); - // }, 240); - // }); +function addCopyIcon(editorView) { + const copyIcon = ``; + const copyButton = document.createElement('button'); + copyButton.setAttribute('type', 'button') + copyButton.classList.add('cm-copy-button'); + copyButton.innerHTML = copyIcon; + editorView.dom.appendChild(copyButton); + + copyButton.addEventListener('click', event => { + copyTextToClipboard(editorView.state.doc.toString()); + copyButton.classList.add('success'); + setTimeout(() => { + copyButton.classList.remove('success'); + }, 240); + }); } /** @@ -187,11 +182,7 @@ export function updateLayout(cmInstance) { */ export function markdownEditor(elem, onChange, domEventHandlers, keyBindings) { const content = elem.textContent; - - // TODO - Change to pass something else that's useful, probably extension array? - // window.$events.emitPublic(elem, 'editor-markdown-cm::pre-init', {config}); - - const ev = createView({ + const config = { parent: elem.parentNode, doc: content, extensions: [ @@ -202,8 +193,13 @@ export function markdownEditor(elem, onChange, domEventHandlers, keyBindings) { EditorView.domEventHandlers(domEventHandlers), keymap.of(keyBindings), ], - }); + }; + // Emit a pre-event public event to allow tweaking of the configure before view creation. + window.$events.emitPublic(elem, 'editor-markdown-cm::pre-init', {cmEditorViewConfig: config}); + + // Create editor view, hide original input + const ev = createView(config); elem.style.display = 'none'; return ev; diff --git a/resources/js/code/setups.js b/resources/js/code/setups.js index 00366ee5e..b061bb3fe 100644 --- a/resources/js/code/setups.js +++ b/resources/js/code/setups.js @@ -45,5 +45,6 @@ export function editor(language) { ...historyKeymap, ]), getLanguageExtension(language, ''), + EditorView.lineWrapping, ]; } \ No newline at end of file diff --git a/resources/js/components/markdown-editor.js b/resources/js/components/markdown-editor.js index 922916701..9d687c83c 100644 --- a/resources/js/components/markdown-editor.js +++ b/resources/js/components/markdown-editor.js @@ -1,4 +1,3 @@ -import {debounce} from "../services/util"; import {Component} from "./component"; import {init as initEditor} from "../markdown/editor"; @@ -45,8 +44,7 @@ export class MarkdownEditor extends Component { window.$events.emitPublic(this.elem, 'editor-markdown::setup', { markdownIt: this.editor.markdown.getRenderer(), displayEl: this.display, - // TODO - change to codeMirrorView? - // codeMirrorInstance: this.editor.cm, + cmEditorView: this.editor.cm, }); } @@ -81,12 +79,6 @@ export class MarkdownEditor extends Component { toolbarLabel.closest('.markdown-editor-wrap').classList.add('active'); }); - // Refresh CodeMirror on container resize - // TODO - // const resizeDebounced = debounce(() => this.editor.cm.refresh(), 100, false); - // const observer = new ResizeObserver(resizeDebounced); - // observer.observe(this.elem); - this.handleDividerDrag(); } @@ -104,8 +96,6 @@ export class MarkdownEditor extends Component { window.removeEventListener('pointerup', upListener); this.display.style.pointerEvents = null; document.body.style.userSelect = null; - // TODO - // this.editor.cm.refresh(); }; this.display.style.pointerEvents = 'none'; diff --git a/resources/js/components/pointer.js b/resources/js/components/pointer.js index d884dc721..a60525cb4 100644 --- a/resources/js/components/pointer.js +++ b/resources/js/components/pointer.js @@ -1,12 +1,14 @@ import * as DOM from "../services/dom"; -import Clipboard from "clipboard/dist/clipboard.min"; import {Component} from "./component"; +import {copyTextToClipboard} from "../services/clipboard"; export class Pointer extends Component { setup() { this.container = this.$el; + this.input = this.$refs.input; + this.button = this.$refs.button; this.pageId = this.$opts.pageId; // Instance variables @@ -16,15 +18,17 @@ export class Pointer extends Component { this.pointerSectionId = ''; this.setupListeners(); - - // Set up clipboard - new Clipboard(this.container.querySelector('button')); } setupListeners() { + // Copy on copy button click + this.button.addEventListener('click', event => { + copyTextToClipboard(this.input.value); + }); + // Select all contents on input click - DOM.onChildEvent(this.container, 'input', 'click', (event, input) => { - input.select(); + this.input.addEventListener('click', event => { + this.input.select(); event.stopPropagation(); }); @@ -112,7 +116,7 @@ export class Pointer extends Component { inputText = window.location.protocol + "//" + window.location.host + inputText; } - this.container.querySelector('input').value = inputText; + this.input.value = inputText; // Update anchor if present const editAnchor = this.container.querySelector('#pointer-edit'); diff --git a/resources/js/markdown/codemirror.js b/resources/js/markdown/codemirror.js index dbf1925c0..55ea485e3 100644 --- a/resources/js/markdown/codemirror.js +++ b/resources/js/markdown/codemirror.js @@ -64,11 +64,10 @@ export async function init(editor) { domEventHandlers, provideKeyBindings(editor), ); - window.cm = cm; - // Will force to remain as ltr for now due to issues when HTML is in editor. - // TODO - // cm.setOption('direction', 'ltr'); + // Add editor view to window for easy access/debugging. + // Not part of official API/Docs + window.mdEditorView = cm; return cm; } \ No newline at end of file diff --git a/resources/js/services/clipboard.js b/resources/js/services/clipboard.js index da921e515..6e59270a9 100644 --- a/resources/js/services/clipboard.js +++ b/resources/js/services/clipboard.js @@ -1,5 +1,5 @@ -class Clipboard { +export class Clipboard { /** * Constructor @@ -51,4 +51,8 @@ class Clipboard { } } +export function copyTextToClipboard(text) { + return navigator.clipboard.writeText(text); +} + export default Clipboard; \ No newline at end of file diff --git a/resources/sass/_codemirror.scss b/resources/sass/_codemirror.scss index 1dee39cd2..aa3729606 100644 --- a/resources/sass/_codemirror.scss +++ b/resources/sass/_codemirror.scss @@ -450,7 +450,7 @@ html.dark-mode .CodeMirror pre { /** * Custom Copy Button */ -.CodeMirror-copy { +.cm-copy-button { position: absolute; top: -1px; right: -1px; @@ -478,7 +478,7 @@ html.dark-mode .CodeMirror pre { } } } -.CodeMirror:hover .CodeMirror-copy { +.cm-editor:hover .cm-copy-button { user-select: all; opacity: 1; pointer-events: all; diff --git a/resources/views/pages/parts/markdown-editor.blade.php b/resources/views/pages/parts/markdown-editor.blade.php index fd8a20a04..c488f0e11 100644 --- a/resources/views/pages/parts/markdown-editor.blade.php +++ b/resources/views/pages/parts/markdown-editor.blade.php @@ -30,7 +30,7 @@ -
    +