diff --git a/package-lock.json b/package-lock.json index 0472e83a..efb567ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,11 @@ "@codemirror/lang-html": "^6.4.2", "@codemirror/lang-javascript": "^6.1.4", "@codemirror/lang-json": "^6.0.1", + "@codemirror/language": "^6.6.0", "@codemirror/search": "^6.2.3", + "@lezer/generator": "^1.2.2", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.3", "@radix-ui/react-dropdown-menu": "^2.0.2", "@radix-ui/react-icons": "^1.2.0", "@radix-ui/react-popover": "1.0.3", @@ -21,6 +25,7 @@ "classnames": "^2.3.2", "codemirror": "^6.0.1", "framer-motion": "^9.0.4", + "parse-json": "^6.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-helmet-async": "^1.3.0", @@ -29,6 +34,7 @@ "devDependencies": { "@tauri-apps/cli": "^1.2.2", "@types/node": "^18.7.10", + "@types/parse-json": "^4.0.0", "@types/react": "^18.0.15", "@types/react-dom": "^18.0.6", "@typescript-eslint/eslint-plugin": "^5.52.0", @@ -67,7 +73,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "dev": true, "dependencies": { "@babel/highlight": "^7.18.6" }, @@ -272,7 +277,6 @@ "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -304,7 +308,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", @@ -1067,6 +1070,18 @@ "@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==", + "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", @@ -2043,6 +2058,12 @@ "integrity": "sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==", "dev": true }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -2457,7 +2478,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -2786,7 +2806,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -2872,7 +2891,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -2880,8 +2898,7 @@ "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/concat-map": { "version": "0.0.1", @@ -3209,6 +3226,14 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.21.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz", @@ -3366,7 +3391,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "engines": { "node": ">=0.8.0" } @@ -4275,7 +4299,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "engines": { "node": ">=4" } @@ -4438,6 +4461,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, "node_modules/is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -4772,6 +4800,11 @@ "node": ">=4" } }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4846,6 +4879,14 @@ "node": ">=10" } }, + "node_modules/lines-and-columns": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz", + "integrity": "sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5200,6 +5241,23 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-6.0.2.tgz", + "integrity": "sha512-SA5aMiaIjXkAiBrW/yPgLgQAQg42f7K3ACO+2l/zOvtQBwX58DMUsFJXelW2fx3yMBmWOVkR6j1MGsdSbCA4UA==", + "dependencies": { + "@babel/code-frame": "^7.16.0", + "error-ex": "^1.3.2", + "json-parse-even-better-errors": "^2.3.1", + "lines-and-columns": "^2.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6035,7 +6093,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -6672,7 +6729,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "dev": true, "requires": { "@babel/highlight": "^7.18.6" } @@ -6826,8 +6882,7 @@ "@babel/helper-validator-identifier": { "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "dev": true + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" }, "@babel/helper-validator-option": { "version": "7.18.6", @@ -6850,7 +6905,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", @@ -7343,6 +7397,15 @@ "@lezer/lr": "^1.0.0" } }, + "@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==", + "requires": { + "@lezer/common": "^1.0.2", + "@lezer/lr": "^1.3.0" + } + }, "@lezer/highlight": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.3.tgz", @@ -8012,6 +8075,12 @@ "integrity": "sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==", "dev": true }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, "@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -8292,7 +8361,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -8515,7 +8583,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -8583,7 +8650,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -8591,8 +8657,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "concat-map": { "version": "0.0.1", @@ -8839,6 +8904,14 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, "es-abstract": { "version": "1.21.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz", @@ -8967,8 +9040,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" }, "eslint": { "version": "8.34.0", @@ -9652,8 +9724,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" }, "has-property-descriptors": { "version": "1.0.0", @@ -9768,6 +9839,11 @@ "is-typed-array": "^1.1.10" } }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, "is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -9993,6 +10069,11 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -10052,6 +10133,11 @@ "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", "dev": true }, + "lines-and-columns": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz", + "integrity": "sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==" + }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -10310,6 +10396,17 @@ "callsites": "^3.0.0" } }, + "parse-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-6.0.2.tgz", + "integrity": "sha512-SA5aMiaIjXkAiBrW/yPgLgQAQg42f7K3ACO+2l/zOvtQBwX58DMUsFJXelW2fx3yMBmWOVkR6j1MGsdSbCA4UA==", + "requires": { + "@babel/code-frame": "^7.16.0", + "error-ex": "^1.3.2", + "json-parse-even-better-errors": "^2.3.1", + "lines-and-columns": "^2.0.2" + } + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -10867,7 +10964,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } diff --git a/package.json b/package.json index abfe86a7..6e207cf4 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,11 @@ "@codemirror/lang-html": "^6.4.2", "@codemirror/lang-javascript": "^6.1.4", "@codemirror/lang-json": "^6.0.1", + "@codemirror/language": "^6.6.0", "@codemirror/search": "^6.2.3", + "@lezer/generator": "^1.2.2", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.3", "@radix-ui/react-dropdown-menu": "^2.0.2", "@radix-ui/react-icons": "^1.2.0", "@radix-ui/react-popover": "1.0.3", @@ -25,6 +29,7 @@ "classnames": "^2.3.2", "codemirror": "^6.0.1", "framer-motion": "^9.0.4", + "parse-json": "^6.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-helmet-async": "^1.3.0", @@ -33,20 +38,21 @@ "devDependencies": { "@tauri-apps/cli": "^1.2.2", "@types/node": "^18.7.10", + "@types/parse-json": "^4.0.0", "@types/react": "^18.0.15", "@types/react-dom": "^18.0.6", + "@typescript-eslint/eslint-plugin": "^5.52.0", + "@typescript-eslint/parser": "^5.52.0", "@vitejs/plugin-react": "^3.0.0", + "autoprefixer": "^10.4.13", + "concurrently": "^7.6.0", "eslint": "^8.34.0", "eslint-config-prettier": "^8.6.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.32.2", - "@typescript-eslint/eslint-plugin": "^5.52.0", - "@typescript-eslint/parser": "^5.52.0", - "autoprefixer": "^10.4.13", - "prettier": "^2.8.4", - "concurrently": "^7.6.0", "postcss": "^8.4.21", + "prettier": "^2.8.4", "tailwindcss": "^3.2.7", "typescript": "^4.6.4", "vite": "^4.0.0", diff --git a/src-web/App.tsx b/src-web/App.tsx index db54215e..d59a6f70 100644 --- a/src-web/App.tsx +++ b/src-web/App.tsx @@ -54,6 +54,7 @@ function App() { /> updateRequest.mutate({ body })} diff --git a/src-web/components/Editor/Editor.css b/src-web/components/Editor/Editor.css index ba45126e..653e39ff 100644 --- a/src-web/components/Editor/Editor.css +++ b/src-web/components/Editor/Editor.css @@ -11,10 +11,22 @@ top: 0; bottom: 0; font-size: 0.9rem; + font-family: monospace; +} + +.cm-editor .cm-tooltip { + color: gray; +} + +.cm-editor .placeholder-widget { + background-color: hsl(var(--color-blue-400)); + padding: 0.05em 0.3em; + border-radius: 0.2em; + color: white; + cursor: pointer; } .cm-editor .cm-scroller { - border: 1px solid hsl(var(--color-gray-50)); border-radius: var(--border-radius-lg); background-color: hsl(var(--color-gray-50)); } @@ -24,7 +36,7 @@ } .cm-editor.cm-focused .cm-scroller { - border-color: hsl(var(--color-blue-400)/0.4); + box-shadow: 0 0 0 1px hsl(var(--color-blue-400)/0.4); } .cm-editor .cm-line { @@ -77,10 +89,6 @@ color: hsl(var(--color-gray-300)); } -.cm-editor .cm-content { - padding-top: 0.4rem; -} - .cm-editor .cm-foldPlaceholder { background-color: hsl(var(--color-gray-100)); border: 1px solid hsl(var(--color-gray-200)); @@ -102,7 +110,7 @@ } .cm-editor .cm-cursor { - border-left: 2px solid red; + border-left: 2px solid hsl(var(--color-gray-900)); } .cm-editor .cm-selectionBackground { @@ -112,3 +120,16 @@ .cm-editor.cm-focused .cm-selectionBackground { background-color: hsl(var(--color-gray-200)); } + +/* --> Add padding to container. For some reason, using padding on both adds an extra + * 1px offset so we need to use a combination of padding and margin. + */ +.cm-editor .cm-gutters { + padding-top: 0.2em; +} + +.cm-editor .cm-content { + margin-top: 0.2em; +} + +/* <-- */ diff --git a/src-web/components/Editor/Editor.tsx b/src-web/components/Editor/Editor.tsx index b4760248..efb0429a 100644 --- a/src-web/components/Editor/Editor.tsx +++ b/src-web/components/Editor/Editor.tsx @@ -6,14 +6,15 @@ import { EditorState } from '@codemirror/state'; interface Props { contentType: string; + useTemplating?: boolean; defaultValue?: string | null; onChange?: (value: string) => void; } -export default function Editor({ contentType, defaultValue, onChange }: Props) { +export default function Editor({ contentType, useTemplating, defaultValue, onChange }: Props) { const ref = useRef(null); const extensions = useMemo(() => { - const ext = syntaxExtension(contentType); + const ext = syntaxExtension({ contentType, useTemplating }); return [ ...baseExtensions, ...(ext ? [ext] : []), @@ -28,13 +29,18 @@ export default function Editor({ contentType, defaultValue, onChange }: Props) { useEffect(() => { if (ref.current === null) return; - const view = new EditorView({ - state: EditorState.create({ - doc: defaultValue ?? '', - extensions: extensions, - }), - parent: ref.current, - }); + let view: EditorView; + try { + view = new EditorView({ + state: EditorState.create({ + doc: defaultValue ?? '', + extensions: extensions, + }), + parent: ref.current, + }); + } catch (e) { + console.log(e); + } return () => view?.destroy(); }, [ref.current]); diff --git a/src-web/components/Editor/extensions.ts b/src-web/components/Editor/extensions.ts index 64145e0a..43dd5bd5 100644 --- a/src-web/components/Editor/extensions.ts +++ b/src-web/components/Editor/extensions.ts @@ -1,13 +1,18 @@ +import { parser as twigParser } from './twig/twig'; import { bracketMatching, - defaultHighlightStyle, foldGutter, + foldInside, foldKeymap, + foldNodeProp, HighlightStyle, + indentNodeProp, indentOnInput, LanguageSupport, + LRLanguage, syntaxHighlighting, } from '@codemirror/language'; +import { lintKeymap } from '@codemirror/lint'; import { crosshairCursor, drawSelection, @@ -19,6 +24,12 @@ import { lineNumbers, rectangularSelection, } from '@codemirror/view'; +import { html } from '@codemirror/lang-html'; +import { parseMixed } from '@lezer/common'; +import { EditorState } from '@codemirror/state'; +import { json } from '@codemirror/lang-json'; +import { javascript } from '@codemirror/lang-javascript'; +import { tags as t } from '@lezer/highlight'; import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'; import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'; import { @@ -27,35 +38,86 @@ import { closeBracketsKeymap, completionKeymap, } from '@codemirror/autocomplete'; -import { lintKeymap } from '@codemirror/lint'; -import { EditorState } from '@codemirror/state'; -import { json } from '@codemirror/lang-json'; -import { javascript } from '@codemirror/lang-javascript'; -import { html } from '@codemirror/lang-html'; -import { tags } from '@lezer/highlight'; +import { placeholders } from './widgets'; export const myHighlightStyle = HighlightStyle.define([ { - tag: [tags.documentMeta, tags.blockComment, tags.lineComment, tags.docComment, tags.comment], + tag: [t.documentMeta, t.blockComment, t.lineComment, t.docComment, t.comment], color: '#757b93', }, - { tag: tags.name, color: '#4699de' }, - { tag: tags.variableName, color: '#31c434' }, - { tag: tags.bool, color: '#e864f6' }, - { tag: tags.attributeName, color: '#8f68ff' }, - { tag: tags.attributeValue, color: '#ff964b' }, - { tag: [tags.keyword, tags.string], color: '#e8b045' }, - { tag: tags.comment, color: '#cec4cc', fontStyle: 'italic' }, + { tag: [t.name], color: '#4699de' }, + { tag: [t.variableName], color: '#31c434' }, + { tag: [t.bool], color: '#e864f6' }, + { tag: [t.attributeName], color: '#8f68ff' }, + { tag: [t.attributeValue], color: '#ff964b' }, + { tag: [t.string], color: '#e8b045' }, + { tag: [t.keyword, t.meta], color: '#45e8a4' }, + { tag: [t.comment], color: '#cec4cc', fontStyle: 'italic' }, ]); -const syntaxExtensions: Record = { - 'application/json': json(), - 'application/javascript': javascript(), - 'text/html': html(), +// export const defaultHighlightStyle = HighlightStyle.define([ +// { tag: t.meta, color: '#404740' }, +// { tag: t.link, textDecoration: 'underline' }, +// { tag: t.heading, textDecoration: 'underline', fontWeight: 'bold' }, +// { tag: t.emphasis, fontStyle: 'italic' }, +// { tag: t.strong, fontWeight: 'bold' }, +// { tag: t.strikethrough, textDecoration: 'line-through' }, +// { tag: t.keyword, color: '#708' }, +// { tag: [t.atom, t.bool, t.url, t.contentSeparator, t.labelName], color: '#219' }, +// { tag: [t.literal, t.inserted], color: '#164' }, +// { tag: [t.string, t.deleted], color: '#a11' }, +// { tag: [t.regexp, t.escape, t.special(t.string)], color: '#e40' }, +// { tag: t.definition(t.variableName), color: '#00f' }, +// { tag: t.local(t.variableName), color: '#30a' }, +// { tag: [t.typeName, t.namespace], color: '#085' }, +// { tag: t.className, color: '#167' }, +// { tag: [t.special(t.variableName), t.macroName], color: '#256' }, +// { tag: t.definition(t.propertyName), color: '#00c' }, +// { tag: t.comment, color: '#940' }, +// { tag: t.invalid, color: '#f00' }, +// ]); + +const syntaxExtensions: Record = { + 'application/json': { base: json(), ext: [] }, + 'application/javascript': { base: javascript(), ext: [] }, + 'text/html': { base: html(), ext: [] }, }; -export function syntaxExtension(contentType: string): LanguageSupport | undefined { - return syntaxExtensions[contentType]; +export function syntaxExtension({ + contentType, + useTemplating, +}: { + contentType: string; + useTemplating?: boolean; +}) { + const { base, ext } = syntaxExtensions[contentType] ?? { base: json(), ext: [] }; + if (!useTemplating) { + return [base]; + } + + const mixedTwigParser = twigParser.configure({ + props: [ + // Add basic folding/indent metadata + foldNodeProp.add({ Conditional: foldInside }), + indentNodeProp.add({ + Conditional: (cx) => { + const closed = /^\s*\{% endif/.test(cx.textAfter); + return cx.lineIndent(cx.node.from) + (closed ? 0 : cx.unit); + }, + }), + ], + wrap: parseMixed((node) => { + return node.type.isTop + ? { + parser: base.language.parser, + overlay: (node) => node.type.name == 'Text', + } + : null; + }), + }); + + const twigLanguage = LRLanguage.define({ parser: mixedTwigParser }); + return [twigLanguage, placeholders, base.support, ...ext]; } export const baseExtensions = [ @@ -78,7 +140,6 @@ export const baseExtensions = [ dropCursor(), EditorState.allowMultipleSelections.of(true), indentOnInput(), - syntaxHighlighting(defaultHighlightStyle, { fallback: true }), bracketMatching(), closeBrackets(), autocompletion(), diff --git a/src-web/components/Editor/twig/twig-highlight.ts b/src-web/components/Editor/twig/twig-highlight.ts new file mode 100644 index 00000000..365d706c --- /dev/null +++ b/src-web/components/Editor/twig/twig-highlight.ts @@ -0,0 +1,7 @@ +import { styleTags, tags as t } from '@lezer/highlight'; + +export const twigHighlight = styleTags({ + 'if endif': t.controlKeyword, + '{{ }} {% %}': t.meta, + DirectiveContent: t.variableName, +}); diff --git a/src-web/components/Editor/twig/twig.grammar b/src-web/components/Editor/twig/twig.grammar new file mode 100644 index 00000000..1d1cc104 --- /dev/null +++ b/src-web/components/Editor/twig/twig.grammar @@ -0,0 +1,28 @@ +// Very crude grammar for a subset of Twig templating syntax + +@top Template { (directive | Text)* } + +directive { + // Insert | + // Conditional { ConditionalOpen (directive | Text)* ConditionalClose } + Insert +} + +@skip {space} { + Insert { "{{" DirectiveContent "}}" } + // ConditionalOpen { "{%" kw<"if"> DirectiveContent "%}" } + // ConditionalClose { "{%" kw<"endif"> "%}" } +} + +kw { @specialize[@name={word}] } + +@tokens { + Identifier { @asciiLetter+ } + Text { ![{] Text? | "{" (@eof | ![%{] Text?) } + space { @whitespace+ } + DirectiveContent { ![%}] DirectiveContent? | $[%}] (@eof | ![}] DirectiveContent?) } + @precedence { space DirectiveContent } + "{{" "}}" // "{%" "%}" +} + +@external propSource twigHighlight from "./twig-highlight.ts" diff --git a/src-web/components/Editor/twig/twig.terms.ts b/src-web/components/Editor/twig/twig.terms.ts new file mode 100644 index 00000000..fddbc22f --- /dev/null +++ b/src-web/components/Editor/twig/twig.terms.ts @@ -0,0 +1,6 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +export const + Template = 1, + Insert = 2, + DirectiveContent = 4, + Text = 6 diff --git a/src-web/components/Editor/twig/twig.ts b/src-web/components/Editor/twig/twig.ts new file mode 100644 index 00000000..fc6ca183 --- /dev/null +++ b/src-web/components/Editor/twig/twig.ts @@ -0,0 +1,19 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +import { LRParser } from '@lezer/lr'; +import { twigHighlight } from './twig-highlight'; +export const parser = LRParser.deserialize({ + version: 14, + states: "zQVOPOOO_QQO'#C^OOOO'#Cc'#CcQVOPOOOdQQO,58xOOOO-E6a-E6aOOOO1G.d1G.d", + stateData: 'l~OYOS~ORPOUQO~OSSO~OTUO~OYS~', + goto: 'cWPPXPPPP]TQORQRORTR', + nodeNames: '⚠ Template Insert {{ DirectiveContent }} Text', + maxTerm: 10, + propSources: [twigHighlight], + skippedNodes: [0], + repeatNodeCount: 1, + tokenData: + ",gRRmOX!|X^'u^p!|pq'uqu!|uv#pv#o!|#o#p)y#p#q!|#q#r+_#r#y!|#y#z'u#z$f!|$f$g'u$g#BY!|#BY#BZ'u#BZ$IS!|$IS$I_'u$I_$I|!|$I|$JO'u$JO$JT!|$JT$JU'u$JU$KV!|$KV$KW'u$KW&FU!|&FU&FV'u&FV;'S!|;'S;=`&f<%lO!|R#TXUPSQOu!|uv#pv#o!|#o#p$b#p#q!|#q#r#p#r;'S!|;'S;=`&f<%lO!|R#uXUPO#o!|#o#p$b#p#q!|#q#r&q#r;'S!|;'S;=`&f<%l~!|~O!|~~&aR$gZSQOu!|uv%Yv#o!|#o#p%o#p#q!|#q#r#p#r;'S!|;'S;=`&f<%l~!|~O!|~~&lQ%]UO#q%o#r;'S%o;'S;=`&Z<%l~%o~O%o~~&aQ%tVSQOu%ouv%Yv#q%o#q#r%Y#r;'S%o;'S;=`&Z<%lO%oQ&^P;=`<%l%oQ&fOSQR&iP;=`<%l!|P&qOUPP&vTUPO#o&q#o#p'V#p;'S&q;'S;=`'o<%lO&qP'YVOu&qv#o&q#p;'S&q;'S;=`'o<%l~&q~O&q~~&lP'rP;=`<%l&qR(OmUPYQSQOX!|X^'u^p!|pq'uqu!|uv#pv#o!|#o#p$b#p#q!|#q#r#p#r#y!|#y#z'u#z$f!|$f$g'u$g#BY!|#BY#BZ'u#BZ$IS!|$IS$I_'u$I_$I|!|$I|$JO'u$JO$JT!|$JT$JU'u$JU$KV!|$KV$KW'u$KW&FU!|&FU&FV'u&FV;'S!|;'S;=`&f<%lO!|R*OZSQOu!|uv%Yv#o!|#o#p*q#p#q!|#q#r#p#r;'S!|;'S;=`&f<%l~!|~O!|~~&lR*xVRPSQOu%ouv%Yv#q%o#q#r%Y#r;'S%o;'S;=`&Z<%lO%oR+dXUPO#o!|#o#p$b#p#q!|#q#r,P#r;'S!|;'S;=`&f<%l~!|~O!|~~&aR,WTTQUPO#o&q#o#p'V#p;'S&q;'S;=`'o<%lO&q", + tokenizers: [0, 1], + topRules: { Template: [0, 1] }, + tokenPrec: 25, +}); diff --git a/src-web/components/Editor/widgets.ts b/src-web/components/Editor/widgets.ts new file mode 100644 index 00000000..61b9e547 --- /dev/null +++ b/src-web/components/Editor/widgets.ts @@ -0,0 +1,84 @@ +import { + Decoration, + DecorationSet, + EditorView, + MatchDecorator, + ViewPlugin, + ViewUpdate, + WidgetType, +} from '@codemirror/view'; + +class PlaceholderWidget extends WidgetType { + constructor(readonly name: string) { + super(); + } + eq(other: PlaceholderWidget) { + return this.name == other.name; + } + toDOM() { + const elt = document.createElement('span'); + elt.className = 'placeholder-widget'; + elt.textContent = this.name; + return elt; + } + ignoreEvent() { + return false; + } +} + +/** + * This is a custom MatchDecorator that will not decorate a match if the selection is inside it + */ +class BetterMatchDecorator extends MatchDecorator { + updateDeco(update: ViewUpdate, deco: DecorationSet): DecorationSet { + if (!update.startState.selection.eq(update.state.selection)) { + return super.createDeco(update.view); + } else { + return super.updateDeco(update, deco); + } + } +} + +const placeholderMatcher = new BetterMatchDecorator({ + regexp: /\{\{\s*([^}\s]+)\s*}}/g, + decoration(match, view, matchStartPos) { + const matchEndPos = matchStartPos + match[0].length - 1; + + // Don't decorate if the cursor is inside the match + for (const r of view.state.selection.ranges) { + if (r.from > matchStartPos && r.to <= matchEndPos) return null; + } + + const groupMatch = match[1]; + if (groupMatch == null) { + // Should never happen, but make TS happy + console.warn('Group match was empty', match); + return null; + } + + return Decoration.replace({ + inclusive: true, + widget: new PlaceholderWidget(groupMatch), + }); + }, +}); + +export const placeholders = ViewPlugin.fromClass( + class { + placeholders: DecorationSet; + constructor(view: EditorView) { + this.placeholders = placeholderMatcher.createDeco(view); + } + update(update: ViewUpdate) { + console.log('VIEW UPDATE', update); + this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders); + } + }, + { + decorations: (instance) => instance.placeholders, + provide: (plugin) => + EditorView.atomicRanges.of((view) => { + return view.plugin(plugin)?.placeholders || Decoration.none; + }), + }, +);