diff --git a/package-lock.json b/package-lock.json index b7af9e6e76..aa86e0653e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8257,6 +8257,12 @@ "@babel/types": "^7.3.0" } }, + "@types/blueimp-md5": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/@types/blueimp-md5/-/blueimp-md5-2.18.0.tgz", + "integrity": "sha512-f4A+++lGZGJvVSgeyMkqA7BEf2BVQli6F+qEykKb49c5ieWQBkfpn6CP5c1IZr2Yi2Ofl6Fj+v0e1fN18Z8Cnw==", + "dev": true + }, "@types/estree": { "version": "0.0.51", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", @@ -9236,7 +9242,7 @@ "app-root-dir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/app-root-dir/-/app-root-dir-1.0.2.tgz", - "integrity": "sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g==", + "integrity": "sha1-OBh+wt6nV3//Az/8sSFyaS/24Rg=", "dev": true }, "append-buffer": { @@ -9342,7 +9348,7 @@ "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", "dev": true }, "array-ify": { @@ -9427,7 +9433,7 @@ "array-union": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", "dev": true, "requires": { "array-uniq": "^1.0.1" @@ -9436,7 +9442,7 @@ "array-uniq": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", "dev": true }, "array-unique": { @@ -10423,7 +10429,7 @@ "babel-plugin-add-react-displayname": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/babel-plugin-add-react-displayname/-/babel-plugin-add-react-displayname-0.0.5.tgz", - "integrity": "sha512-LY3+Y0XVDYcShHHorshrDbt4KFWL4bSeniCtl4SYZbask+Syngk1uMPCeN9+nSiZo6zX5s0RTq/J9Pnaaf/KHw==", + "integrity": "sha1-M51M3be2X9YtHfnbn+BN4TQSK9U=", "dev": true }, "babel-plugin-apply-mdx-type-prop": { @@ -10784,6 +10790,11 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "dev": true }, + "blueimp-md5": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", + "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==" + }, "bn.js": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", @@ -10830,7 +10841,7 @@ "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "dev": true }, "boxen": { @@ -11738,7 +11749,7 @@ "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", "dev": true }, "debug": { @@ -11753,7 +11764,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true } } @@ -11810,7 +11821,7 @@ "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true }, "constants-browserify": { @@ -11951,7 +11962,7 @@ "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", "dev": true }, "copy-concurrently": { @@ -12559,7 +12570,7 @@ "css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", "dev": true }, "cssesc": { @@ -12989,7 +13000,7 @@ "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true }, "depd": { @@ -13187,7 +13198,7 @@ "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "dev": true } } @@ -13373,7 +13384,7 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "dev": true }, "electron-to-chromium": { @@ -13425,7 +13436,7 @@ "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", "dev": true }, "end-of-stream": { @@ -13700,7 +13711,7 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", "dev": true }, "escape-string-regexp": { @@ -14264,7 +14275,7 @@ "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", "dev": true }, "events": { @@ -14675,7 +14686,7 @@ "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", "dev": true, "requires": { "is-glob": "^3.1.0", @@ -14685,7 +14696,7 @@ "is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", "dev": true, "requires": { "is-extglob": "^2.1.0" @@ -15230,7 +15241,7 @@ "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", "dev": true }, "from2": { @@ -15672,7 +15683,7 @@ "glob-to-regexp": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", - "integrity": "sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig==", + "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", "dev": true }, "glob-watcher": { @@ -16428,7 +16439,7 @@ "has-glob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-glob/-/has-glob-1.0.0.tgz", - "integrity": "sha512-D+8A457fBShSEI3tFCj65PAbT++5sKiFtdCdOam0gnfBgw9D277OERk+HM9qYJXmdVLZ/znez10SqHN0BBQ50g==", + "integrity": "sha1-mqqe7b/7G6OZCnsAEPtnjuAIEgc=", "dev": true, "requires": { "is-glob": "^3.0.0" @@ -16437,7 +16448,7 @@ "is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", "dev": true, "requires": { "is-extglob": "^2.1.0" @@ -16478,7 +16489,7 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true }, "has-value": { @@ -17442,7 +17453,7 @@ "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true }, "is-string": { @@ -17529,7 +17540,7 @@ "is-window": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-window/-/is-window-1.0.2.tgz", - "integrity": "sha512-uj00kdXyZb9t9RcAUAwMZAnkBUwdYGhYlt7djMXhfyhUCzwNba50tIiBKR7q0l7tdoBtFVw/3JmLY6fI3rmZmg==", + "integrity": "sha1-LIlspT25feRdPDMTOmXYyfVjSA0=", "dev": true }, "is-windows": { @@ -20521,7 +20532,7 @@ "js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", - "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", + "integrity": "sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8=", "dev": true }, "js-tokens": { @@ -20912,7 +20923,7 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", "dev": true }, "lodash.get": { @@ -20942,7 +20953,7 @@ "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", "dev": true }, "loose-envify": { @@ -20984,7 +20995,7 @@ "lz-string": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", - "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", + "integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==", "dev": true }, "make-dir": { @@ -21043,7 +21054,7 @@ "map-or-similar": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz", - "integrity": "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==", + "integrity": "sha1-beJlMXSt+12e3DPGnT6Sobdvrwg=", "dev": true }, "map-visit": { @@ -21156,13 +21167,13 @@ "mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", "dev": true }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, "memfs": { @@ -21182,7 +21193,7 @@ "memoizerific": { "version": "1.11.3", "resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz", - "integrity": "sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==", + "integrity": "sha1-fIekZGREwy11Q4VwkF8tvRsagFo=", "dev": true, "requires": { "map-or-similar": "^1.5.0" @@ -21282,7 +21293,7 @@ "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", "dev": true }, "merge-stream": { @@ -21298,7 +21309,7 @@ "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", "dev": true }, "microevent.ts": { @@ -21466,7 +21477,7 @@ "min-document": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", "dev": true, "requires": { "dom-walk": "^0.1.0" @@ -21754,7 +21765,7 @@ "node-dir": { "version": "0.1.17", "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", - "integrity": "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==", + "integrity": "sha1-X1Zl2TNRM1yqvvjxxVRRbPXx5OU=", "dev": true, "requires": { "minimatch": "^3.0.2" @@ -21872,7 +21883,7 @@ "normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", "dev": true }, "normalize-selector": { @@ -21966,7 +21977,7 @@ "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "dev": true, "requires": { "path-key": "^2.0.0" @@ -21996,7 +22007,7 @@ "num2fraction": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", "dev": true }, "number-is-nan": { @@ -22542,7 +22553,7 @@ "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "dev": true }, "p-limit": { @@ -22787,7 +22798,7 @@ "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", "dev": true }, "path-type": { @@ -24272,7 +24283,7 @@ "relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", "dev": true }, "remark-external-links": { @@ -24455,13 +24466,13 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { "ansi-regex": "^2.0.0" @@ -24774,7 +24785,7 @@ "normalize-path": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "dev": true, "requires": { "remove-trailing-separator": "^1.0.1" @@ -24935,7 +24946,7 @@ "serve-favicon": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/serve-favicon/-/serve-favicon-2.5.0.tgz", - "integrity": "sha512-FMW2RvqNr03x+C0WxTyu6sOv21oOjkq5j8tjquWccwa6ScNyGFOGJVpuS1NmTVGBAHS07xnSKotgf2ehQmf9iA==", + "integrity": "sha1-k10kDN/g9YBTB/3+ln2IlCosvPA=", "dev": true, "requires": { "etag": "~1.8.1", @@ -25839,7 +25850,7 @@ "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true }, "strip-final-newline": { @@ -26504,7 +26515,7 @@ "toggle-selection": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" }, "toidentifier": { "version": "1.0.1", @@ -26534,13 +26545,13 @@ "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", "dev": true }, "trim": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", - "integrity": "sha512-YzQV+TZg4AxpKxaTHK3c3D+kRDCGVEE7LemdlQZoQXn0iennk10RsIoY6ikzAqJTc9Xjl9C1/waHom/J86ziAQ==", + "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", "dev": true }, "trim-newlines": { @@ -27034,7 +27045,7 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", "dev": true }, "unset-value": { @@ -27219,7 +27230,7 @@ "utila": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", "dev": true }, "utility-types": { @@ -27230,7 +27241,7 @@ "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", "dev": true }, "uuid": { @@ -27242,7 +27253,7 @@ "uuid-browser": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/uuid-browser/-/uuid-browser-3.1.0.tgz", - "integrity": "sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg==", + "integrity": "sha1-DwWkCu90+eWVHiDvv0SxGHHlZBA=", "dev": true }, "v8-compile-cache": { @@ -27298,7 +27309,7 @@ "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", "dev": true }, "vfile": { @@ -27723,7 +27734,7 @@ "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", "dev": true }, "webpack": { @@ -27874,7 +27885,7 @@ "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", "dev": true, "requires": { "tr46": "~0.0.3", @@ -27950,7 +27961,7 @@ "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", "dev": true }, "worker-farm": { diff --git a/package.json b/package.json index 48e7a0263d..0ffe0b140c 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "@gravity-ui/i18n": "^1.0.0", "@popperjs/core": "2.11.6", "bem-cn-lite": "4.0.0", + "blueimp-md5": "^2.19.0", "focus-trap": "7.4.0", "lodash": "4.17.21", "react-copy-to-clipboard": "5.1.0", @@ -104,6 +105,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.4.3", + "@types/blueimp-md5": "^2.18.0", "@types/jest": "^27.4.0", "@types/lodash": "^4.14.177", "@types/react": "^18.0.26", diff --git a/src/components/index.ts b/src/components/index.ts index 77a72b045c..179b016cdc 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -49,6 +49,7 @@ export * from './Tooltip'; export * from './User'; export * from './UserAvatar'; +export * from './utils/class-transform'; export * from './utils/event-broker'; export {getComponentName} from './utils/getComponentName'; export * from './utils/withEventBrokerDomHandlers'; @@ -64,3 +65,4 @@ export * from './utils/useListNavigation'; export * from './utils/useForkRef'; export * from './utils/setRef'; export {useOnFocusOutside} from './utils/useOnFocusOutside'; +export * from './utils/xpath'; diff --git a/src/components/utils/__tests__/class-transform.test.ts b/src/components/utils/__tests__/class-transform.test.ts new file mode 100644 index 0000000000..2641f507b0 --- /dev/null +++ b/src/components/utils/__tests__/class-transform.test.ts @@ -0,0 +1,63 @@ +import {formatClass, parseClass, ElementClass} from '../class-transform'; + +describe('class-transform', () => { + function checkParseFormat(strClass: string, objClass: ElementClass) { + expect(parseClass(strClass)).toEqual(objClass); + expect(formatClass(objClass)).toEqual(strClass); + } + + it('should parse and format block class', () => { + checkParseFormat('block', { + block: 'block', + }); + }); + + it('should parse and format block__element class', () => { + checkParseFormat('block__element', { + block: 'block', + element: 'element', + }); + }); + + it('should parse and format block__element_mod class', () => { + checkParseFormat('block__element_mod', { + block: 'block', + element: 'element', + mod: { + key: 'mod', + value: true, + }, + }); + }); + + it('should parse and format block__element_mod_value class', () => { + checkParseFormat('block__element_mod_value', { + block: 'block', + element: 'element', + mod: { + key: 'mod', + value: 'value', + }, + }); + }); + + it('should parse and format block_mod class', () => { + checkParseFormat('block_mod', { + block: 'block', + mod: { + key: 'mod', + value: true, + }, + }); + }); + + it('should parse and format block_mod_value class', () => { + checkParseFormat('block_mod_value', { + block: 'block', + mod: { + key: 'mod', + value: 'value', + }, + }); + }); +}); diff --git a/src/components/utils/__tests__/xpath.test.tsx b/src/components/utils/__tests__/xpath.test.tsx new file mode 100644 index 0000000000..3bcf88749f --- /dev/null +++ b/src/components/utils/__tests__/xpath.test.tsx @@ -0,0 +1,313 @@ +import React from 'react'; +import {render, screen} from '@testing-library/react'; +import {getXpath, withoutClassMods} from '../xpath'; + +class XpathBuilder { + readonly xpath: string; + + constructor(xpath = '/html/body') { + this.xpath = xpath; + } + + append(params?: {tag?: string; className?: string; id?: string}) { + const {tag, className, id} = { + tag: 'div', + ...params, + }; + + let newXpath = `${this.xpath}/${tag}`; + if (id) { + newXpath += `[@id='${id}']`; + } else if (className) { + newXpath += `[@class='${className}']`; + } + return new XpathBuilder(newXpath); + } +} + +describe('getXpath', () => { + const rootBuilder = new XpathBuilder().append(); + const clickText = 'Click me!'; + + describe('no option', () => { + it('should get xpath', () => { + const onClick = jest.fn(); + render(
{clickText}
); + screen.getByText(clickText).click(); + + const {xpath, hash} = getXpath(onClick.mock.calls[0][0]); + expect(xpath).toBe(rootBuilder.append().xpath); + expect(hash).toMatchInlineSnapshot(`"ae152984744905d6e135619e32e8a06e"`); + }); + + it('should get xpath with class name', () => { + const onClick = jest.fn(); + render( +
+ {clickText} +
, + ); + screen.getByText(clickText).click(); + + const {xpath, hash} = getXpath(onClick.mock.calls[0][0]); + expect(xpath).toBe(rootBuilder.append({className: 'target'}).xpath); + expect(hash).toMatchInlineSnapshot(`"af0aa4559887e5d354f9e3da443f4ec7"`); + }); + + it('should get xpath with multiply class names', () => { + const onClick = jest.fn(); + render( +
+ {clickText} +
, + ); + screen.getByText(clickText).click(); + + const {xpath, hash} = getXpath(onClick.mock.calls[0][0]); + expect(xpath).toBe( + rootBuilder.append({className: 'target target_important or-maybe__not_important'}) + .xpath, + ); + expect(hash).toMatchInlineSnapshot(`"71133dcb198e2a619bd0f83f4b5f37a8"`); + }); + + it('should get xpath with id', () => { + const onClick = jest.fn(); + render( +
+ {clickText} +
, + ); + screen.getByText(clickText).click(); + + const {xpath, hash} = getXpath(onClick.mock.calls[0][0]); + expect(xpath).toBe(rootBuilder.append({id: 'target'}).xpath); + expect(hash).toMatchInlineSnapshot(`"ab5d7778edde59b97b55ea2ff0fe2b28"`); + }); + + it('should get xpath with only id', () => { + const onClick = jest.fn(); + render( +
+ {clickText} +
, + ); + screen.getByText(clickText).click(); + + const {xpath, hash} = getXpath(onClick.mock.calls[0][0]); + expect(xpath).toBe(rootBuilder.append({id: 'id'}).xpath); + expect(hash).toMatchInlineSnapshot(`"aad885cd5dfa17a63e475c43e23b572d"`); + }); + + it('should get xpath from deep tag', () => { + const onClick = jest.fn(); + render( +
+
+ {clickText} +
+
, + ); + screen.getByText(clickText).click(); + + const {xpath, hash} = getXpath(onClick.mock.calls[0][0]); + expect(xpath).toBe( + rootBuilder.append({tag: 'main', className: 'main'}).append({id: 'target'}).xpath, + ); + expect(hash).toMatchInlineSnapshot(`"52eca834366d0e6e0f1e707a540f5475"`); + }); + + it('should ignore same level nodes', () => { + const onClick = jest.fn(); + render( +
+ Hello +
world
+
{clickText}
+
!
+ (css is awesome) +
, + ); + screen.getByText(clickText).click(); + + const {xpath, hash} = getXpath(onClick.mock.calls[0][0]); + expect(xpath).toBe(rootBuilder.append({tag: 'main'}).append().xpath); + expect(hash).toMatchInlineSnapshot(`"65f32734376f55e3648401f011cc75c5"`); + }); + + it('should ignore subnodes', () => { + const onClick = jest.fn(); + render( +
+ {clickText} + Hello +
world
+
!
+ (css is awesome) +
, + ); + screen.getByText(new RegExp(`^${clickText}`)).click(); + + const {xpath, hash} = getXpath(onClick.mock.calls[0][0]); + expect(xpath).toBe(rootBuilder.append().xpath); + expect(hash).toMatchInlineSnapshot(`"ae152984744905d6e135619e32e8a06e"`); + }); + }); + + describe('options', () => { + it('should ignore id', () => { + const onClick = jest.fn(); + render( +
+ {clickText} +
, + ); + screen.getByText(clickText).click(); + + const {xpath, hash} = getXpath(onClick.mock.calls[0][0], {withoutId: true}); + expect(xpath).toBe(rootBuilder.append().xpath); + expect(hash).toMatchInlineSnapshot(`"ae152984744905d6e135619e32e8a06e"`); + }); + + it('should ignore id 2', () => { + const onClick = jest.fn(); + render( +
+ {clickText} +
, + ); + screen.getByText(clickText).click(); + + const {xpath, hash} = getXpath(onClick.mock.calls[0][0], {withoutId: true}); + expect(xpath).toBe(rootBuilder.append({className: 'class-name'}).xpath); + expect(hash).toMatchInlineSnapshot(`"bfdf511bca38e29bdc45c5fc556ce9a7"`); + }); + + it('should transform class names', () => { + const onClick = jest.fn(); + render( +
+
+
+ {clickText} +
+
+
, + ); + screen.getByText(clickText).click(); + + const {xpath, hash} = getXpath(onClick.mock.calls[0][0], { + classConverter: ({block}) => ({ + block, + element: 'converted', + }), + }); + expect(xpath).toBe( + rootBuilder + .append() + .append({className: 'container__converted'}) + .append({className: 'target__converted target-2__converted'}).xpath, + ); + expect(hash).toMatchInlineSnapshot(`"9f0dc952032536615fd5217d02f47f09"`); + }); + + it('should ignore class name convert if id exist', () => { + const onClick = jest.fn(); + render( +
+ {clickText} +
, + ); + screen.getByText(clickText).click(); + + const {xpath, hash} = getXpath(onClick.mock.calls[0][0], { + classConverter: ({block}) => ({block}), + }); + expect(xpath).toBe(rootBuilder.append({id: 'id'}).xpath); + expect(hash).toMatchInlineSnapshot(`"aad885cd5dfa17a63e475c43e23b572d"`); + }); + + it('should not ignore class name convert if withoutId', () => { + const onClick = jest.fn(); + render( +
+ {clickText} +
, + ); + screen.getByText(clickText).click(); + + const {xpath, hash} = getXpath(onClick.mock.calls[0][0], { + classConverter: ({block}) => ({block}), + withoutId: true, + }); + expect(xpath).toBe(rootBuilder.append({className: 'class'}).xpath); + expect(hash).toMatchInlineSnapshot(`"5296eda04d18e2ab7e41f1a2e293a4bf"`); + }); + + it('should remove mods correctly', () => { + const onClick = jest.fn(); + render( +
+ {clickText} +
, + ); + screen.getByText(clickText).click(); + + const {xpath, hash} = getXpath(onClick.mock.calls[0][0], { + classConverter: withoutClassMods(), + withoutId: true, + }); + expect(xpath).toBe(rootBuilder.append({className: 'class class__name'}).xpath); + expect(hash).toMatchInlineSnapshot(`"d7c4a0991ef83687ec9be6a2e342d6e5"`); + }); + + it('should remove mods correctly 2', () => { + const onClick = jest.fn(); + render( +
+ {clickText} +
, + ); + screen.getByText(clickText).click(); + + const {xpath, hash} = getXpath(onClick.mock.calls[0][0], { + classConverter: withoutClassMods((objClass) => ({ + ...objClass, + mod: {key: 'no-mod', value: true}, + })), + withoutId: true, + }); + expect(xpath).toBe( + rootBuilder.append({className: 'class_no-mod class__name_no-mod'}).xpath, + ); + expect(hash).toMatchInlineSnapshot(`"e1cc78e4024420bc11aed6dbc612e97c"`); + }); + + it('should get tags', () => { + const onClick = jest.fn(); + render( +
+ {clickText} +
, + ); + screen.getByText(clickText).click(); + + const {xpath, hash} = getXpath(onClick.mock.calls[0][0], { + classConverter: ({tag, block}) => ({ + block, + element: tag, + }), + }); + expect(xpath).toBe(rootBuilder.append({className: 'class__div'}).xpath); + expect(hash).toMatchInlineSnapshot(`"bf5138ce0f335973b588b4c3029e1335"`); + }); + }); +}); diff --git a/src/components/utils/class-transform.ts b/src/components/utils/class-transform.ts new file mode 100644 index 0000000000..6e714550e9 --- /dev/null +++ b/src/components/utils/class-transform.ts @@ -0,0 +1,48 @@ +export interface ElementClass { + block: string; + element?: string; + mod?: { + key: string; + value: string | boolean; + }; +} + +export function parseClass(strClass: string): ElementClass { + const split = strClass.split('_').filter((str) => str); + if (strClass.includes('__')) { + return { + block: split[0], + element: split[1], + mod: split[2] + ? { + key: split[2], + value: split[3] ? split[3] : true, + } + : undefined, + }; + } + + return { + block: split[0], + mod: split[1] + ? { + key: split[1], + value: split[2] ? split[2] : true, + } + : undefined, + }; +} + +export function formatClass(objClass: ElementClass): string { + let result = objClass.block; + if (objClass.element) { + result = `${result}__${objClass.element}`; + } + if (objClass.mod?.value) { + result = `${result}_${objClass.mod.key}`; + if (typeof objClass.mod.value === 'string') { + result = `${result}_${objClass.mod.value}`; + } + } + return result; +} diff --git a/src/components/utils/xpath.ts b/src/components/utils/xpath.ts new file mode 100644 index 0000000000..a7f4dba34c --- /dev/null +++ b/src/components/utils/xpath.ts @@ -0,0 +1,77 @@ +import React from 'react'; +import md5 from 'blueimp-md5'; + +import {ElementClass, formatClass, parseClass} from './class-transform'; + +export interface ElementClassWithInfo extends ElementClass { + tag: string; +} + +export type XpathClassConverter = ( + parsedClass: ElementClassWithInfo, + strClass: string, +) => ElementClass | undefined; + +export interface XpathOptions { + /** Function for converting and filtering classes */ + classConverter?: XpathClassConverter; + /** Flag for managing replaces from tag[@class='...'] to tag[@id='...'] if id is exist */ + withoutId?: boolean; +} + +type InternalXpathOptions = Required; + +export function withoutClassMods( + converter: XpathClassConverter = (arg) => arg, +): XpathClassConverter { + return (parsedClass, strClass) => + parsedClass.mod ? undefined : converter(parsedClass, strClass); +} + +function isElement(node: Node): node is Element { + return node.nodeType === Node.ELEMENT_NODE; +} + +function getXpathByNode(node: Node | null, options: InternalXpathOptions): string { + if (!node || !isElement(node)) { + return ''; + } + const tag = node.tagName.toLowerCase(); + + let token = `/${tag}`; + if (node.id && !options.withoutId) { + token += `[@id='${node.id}']`; + } else { + const classes: string[] = []; + node.classList.forEach((className) => { + const currentClass = options.classConverter({...parseClass(className), tag}, className); + if (currentClass) { + classes.push(formatClass(currentClass)); + } + }); + if (classes.length) { + token += `[@class='${classes.join(' ')}']`; + } + } + + return getXpathByNode(node.parentElement, options) + token; +} + +const defaultXpathOptions: InternalXpathOptions = { + classConverter: (arg) => arg, + withoutId: false, +}; + +export function getXpath(event: React.SyntheticEvent, options?: XpathOptions) { + const internalOptions = { + ...defaultXpathOptions, + ...(options || {}), + }; + + const xpath = getXpathByNode(event.currentTarget || event.target, internalOptions); + + return { + xpath, + hash: md5(xpath), + }; +}