diff --git a/package.json b/package.json index 867424c..2405ef3 100644 --- a/package.json +++ b/package.json @@ -134,6 +134,7 @@ "@types/ws": "^8.5.10", "@cordisjs/plugin-loader": "^1.0.0-rc.5", "cordis": "^4.0.0-rc.7", + "jsdom": "^29.1.1", "lightningcss": "^1.32.0", "react": "^18.2.0", "react-dom": "18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 580ca56..365a32e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -168,6 +168,9 @@ importers: cordis: specifier: ^4.0.0-rc.7 version: 4.0.0-rc.7(@cordisjs/plugin-loader@1.0.0-rc.5) + jsdom: + specifier: ^29.1.1 + version: 29.1.1 lightningcss: specifier: ^1.32.0 version: 1.33.0 @@ -185,7 +188,7 @@ importers: version: 5.6.2 vitest: specifier: ^4.1.8 - version: 4.1.10(@types/node@24.13.3)(vite@8.2.1(@types/node@24.13.3)) + version: 4.1.10(@types/node@24.13.3)(jsdom@29.1.1)(vite@8.2.1(@types/node@24.13.3)) packages: @@ -197,10 +200,29 @@ packages: pdfjs-dist: optional: true + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@babel/runtime@7.29.7': resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + '@codemirror/autocomplete@6.20.3': resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} @@ -276,6 +298,42 @@ packages: node-addon-require-builtin: optional: true + '@csstools/color-helpers@6.1.0': + resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.3.0': + resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.10': + resolution: {integrity: sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.7': + resolution: {integrity: sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@deepseek-ai/cordis-plugin-include@1.0.5-rc.1': resolution: {integrity: sha512-fwDm2DvCUawaP7o2xx9kPQ46FU8krYkB51iQxOIXwZFNRCamfGkdYwIEe4ycEnXoEpjm4P/gA9pZovbJJNqnAw==} peerDependencies: @@ -779,6 +837,15 @@ packages: '@deepseek-ai/schemastery@3.18.1-rc.1': resolution: {integrity: sha512-gilia4mY6S/j6oFYsn4x2Ve9K6QfvzTgqJntkty0wFsKjTYHQ6e2WDvXLMw48t75WRBhpLt+ZpFm3GHmTWkNMw==} + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@flatten-js/interval-tree@1.1.3': resolution: {integrity: sha512-xhFWUBoHJFF77cJO1D6REjdgJEMRf2Y2Z+eKEPav8evGKcLSnj1ud5pLXQSbGuxF3VSvT1rWhMfVpXEKJLTL+A==} @@ -2152,6 +2219,9 @@ packages: async-lock@1.4.1: resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + cac@7.0.0: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} @@ -2241,9 +2311,17 @@ packages: crelt@1.0.7: resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2301,6 +2379,10 @@ packages: resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} engines: {node: '>=14'} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + es-module-lexer@2.3.1: resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} @@ -2367,6 +2449,10 @@ packages: hookable@6.1.1: resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} @@ -2387,6 +2473,9 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -2397,6 +2486,15 @@ packages: resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true + jsdom@29.1.1: + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} @@ -2506,6 +2604,10 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -2551,6 +2653,9 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -2691,6 +2796,9 @@ packages: pako@1.0.2: resolution: {integrity: sha512-qCaDHl8pk5JSbz5k1Bi5zTZf430DUuV13pmcFEHHan4fW++WIwm2HXUirpzfciBRyFUN+eeZW11bfIM/Y4Zeog==} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -2718,6 +2826,10 @@ packages: resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} engines: {node: '>=12.0.0'} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + quansync@1.0.0: resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} @@ -2795,6 +2907,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -2828,6 +2944,10 @@ packages: safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} @@ -2884,6 +3004,9 @@ packages: style-mod@4.1.3: resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tailwind-merge@2.6.0: resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} @@ -2902,6 +3025,21 @@ packages: resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} + tldts-core@7.4.10: + resolution: {integrity: sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==} + + tldts@7.4.10: + resolution: {integrity: sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==} + hasBin: true + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -2963,6 +3101,10 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici@7.29.0: + resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} + engines: {node: '>=20.18.1'} + unicode-regex@4.2.0: resolution: {integrity: sha512-fEYz7CCnvHDAdrb8OYAP7qlQCWzXBO5cHXQ3XI+HoZaBpiAwyC6b2nixMGl91yrDYEIRm7NDskgTvnLZ7mqrKQ==} engines: {node: '>=16'} @@ -3117,6 +3259,22 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -3151,6 +3309,13 @@ packages: engines: {node: '>=0.8'} hasBin: true + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xterm@5.3.0: resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==} deprecated: This package is now deprecated. Move to @xterm/xterm instead. @@ -3207,8 +3372,32 @@ snapshots: echarts: 6.1.0 jszip: 3.10.1 + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + '@babel/runtime@7.29.7': {} + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + '@codemirror/autocomplete@6.20.3': dependencies: '@codemirror/language': 6.12.4 @@ -3376,6 +3565,30 @@ snapshots: cordis: 4.0.0-rc.7(@cordisjs/plugin-loader@1.0.0-rc.5) cosmokit: 1.8.1 + '@csstools/color-helpers@6.1.0': {} + + '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.1.0 + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.7(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + '@deepseek-ai/cordis-plugin-include@1.0.5-rc.1(@deepseek-ai/cordis-plugin-loader@1.0.1-rc.1)(@deepseek-ai/cordis@4.0.1-rc.1)': dependencies: '@deepseek-ai/cordis': 4.0.1-rc.1(@deepseek-ai/cordis-plugin-include@1.0.5-rc.1)(@deepseek-ai/cordis-plugin-loader@1.0.1-rc.1) @@ -3961,6 +4174,8 @@ snapshots: '@deepseek-ai/cosmokit': 1.8.2-rc.1 '@standard-schema/spec': 1.1.0 + '@exodus/bytes@1.15.1': {} + '@flatten-js/interval-tree@1.1.3': {} '@floating-ui/core@1.8.0': @@ -6121,6 +6336,10 @@ snapshots: async-lock@1.4.1: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + cac@7.0.0: {} ccount@2.0.1: {} @@ -6186,8 +6405,20 @@ snapshots: crelt@1.0.7: {} + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + csstype@3.2.3: {} + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + debug@4.4.3: dependencies: ms: 2.1.3 @@ -6230,6 +6461,8 @@ snapshots: empathic@2.0.1: {} + entities@8.0.0: {} + es-module-lexer@2.3.1: {} escalade@3.2.0: {} @@ -6287,6 +6520,12 @@ snapshots: hookable@6.1.1: {} + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' + html-void-elements@3.0.0: {} immediate@3.0.6: {} @@ -6299,6 +6538,8 @@ snapshots: is-fullwidth-code-point@3.0.0: {} + is-potential-custom-element-name@1.0.1: {} + isarray@1.0.0: {} js-tokens@3.0.0: {} @@ -6307,6 +6548,32 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@29.1.1: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.7(css-tree@3.2.1) + '@exodus/bytes': 1.15.1 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + undici: 7.29.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + jszip@3.10.1: dependencies: lie: 3.3.0 @@ -6393,6 +6660,8 @@ snapshots: dependencies: js-tokens: 3.0.0 + lru-cache@11.5.2: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -6525,6 +6794,8 @@ snapshots: dependencies: '@types/mdast': 4.0.4 + mdn-data@2.27.1: {} + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.3.0 @@ -6766,6 +7037,10 @@ snapshots: pako@1.0.2: {} + parse5@8.0.1: + dependencies: + entities: 8.0.0 + pathe@2.0.3: {} picocolors@1.1.1: {} @@ -6802,6 +7077,8 @@ snapshots: '@types/node': 24.13.3 long: 5.3.2 + punycode@2.3.1: {} + quansync@1.0.0: {} quickselect@3.0.0: {} @@ -6882,6 +7159,8 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + resolve-pkg-maps@1.0.0: {} rolldown-plugin-dts@0.27.14(rolldown@1.2.3)(typescript@5.6.2): @@ -6924,6 +7203,10 @@ snapshots: safe-buffer@5.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.23.0: dependencies: loose-envify: 1.4.0 @@ -6986,6 +7269,8 @@ snapshots: style-mod@4.1.3: {} + symbol-tree@3.2.4: {} + tailwind-merge@2.6.0: {} tinybench@2.9.0: {} @@ -6999,6 +7284,20 @@ snapshots: tinyrainbow@3.1.1: {} + tldts-core@7.4.10: {} + + tldts@7.4.10: + dependencies: + tldts-core: 7.4.10 + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.10 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} trigram-utils@2.0.1: @@ -7046,6 +7345,8 @@ snapshots: undici-types@7.18.2: {} + undici@7.29.0: {} + unicode-regex@4.2.0: dependencies: regexp-util: 2.0.3 @@ -7126,7 +7427,7 @@ snapshots: '@types/node': 24.13.3 fsevents: 2.3.3 - vitest@4.1.10(@types/node@24.13.3)(vite@8.2.1(@types/node@24.13.3)): + vitest@4.1.10(@types/node@24.13.3)(jsdom@29.1.1)(vite@8.2.1(@types/node@24.13.3)): dependencies: '@vitest/expect': 4.1.10 '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@24.13.3)) @@ -7150,11 +7451,28 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.13.3 + jsdom: 29.1.1 transitivePeerDependencies: - msw w3c-keyname@2.2.8: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -7182,6 +7500,10 @@ snapshots: wmf: 1.0.2 word: 0.3.0 + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + xterm@5.3.0: {} y18n@5.0.8: {} diff --git a/src/client/ime-guard.ts b/src/client/ime-guard.ts new file mode 100644 index 0000000..1ee5b40 --- /dev/null +++ b/src/client/ime-guard.ts @@ -0,0 +1,47 @@ +/** + * IME-composition key guard. + * + * While a Chinese/Japanese/Korean input method is composing (the user is + * picking a candidate from the IME window), every pressed key BELONGS to the + * input method: arrows move the candidate highlight, Enter/Space confirm the + * composition, Escape cancels it. Page code must not process those keys — + * a component that does (a number stepper calling preventDefault() on + * ArrowUp/ArrowDown, a submit handler reacting to Enter, ...) silently + * breaks the IME: candidates stop responding, the composition gets torn + * apart, and only bare letters come out. + * + * This guard enforces that rule at the document boundary: a capture-phase + * keydown/keyup listener that stops the event from propagating further + * whenever a composition is in progress. Because it runs in the capture + * phase on `document` — the outermost node — it fires BEFORE React's + * delegated handlers (attached at the root container) and before any native + * target/bubble listener, so an inlined third-party component (e.g. the + * Univer office UI bundled into this plugin) can never intercept + * composition keys. The browser's native IME processing is untouched: + * stopPropagation only silences page JS, not the default action. + * + * The composition signal follows the DSH core convention (InputBar's IME + * guard, issue #535): `isComposing` for modern engines, keyCode 229 as the + * legacy signal engines emit without isComposing. + */ + +/** The pure decision: is this keyboard event part of an IME composition? */ +export function isImeComposition(event: { isComposing: boolean; keyCode: number }): boolean { + return event.isComposing || event.keyCode === 229 +} + +/** + * Register the document-level capture guard. Returns the disposer + * (HMR-safe; call through `ctx.effect`). + */ +export function registerImeGuard(): () => void { + const onKey = (event: KeyboardEvent): void => { + if (isImeComposition(event)) event.stopPropagation() + } + document.addEventListener('keydown', onKey, true) + document.addEventListener('keyup', onKey, true) + return () => { + document.removeEventListener('keydown', onKey, true) + document.removeEventListener('keyup', onKey, true) + } +} diff --git a/src/client/index.tsx b/src/client/index.tsx index 52c0d49..240d537 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -17,6 +17,7 @@ import { registerBuiltins } from './builtins/index.ts' import { Sidebar } from './Sidebar.tsx' import { registerOpenPathInterception, registerTurnTailInterception } from './intercept.tsx' import { registerLinkInterception } from './link-intercept.ts' +import { registerImeGuard } from './ime-guard.ts' import { loadPrefs } from './prefs.ts' import { SideCardSection } from './SideCardSection.tsx' import { api } from './api.ts' @@ -183,6 +184,25 @@ export function apply(ctx: Context): void { 'dsh-better-sidebar: link interception', ) + // The IME guard: composition keys (candidate arrows, confirm, cancel) + // belong to the input method, never to page JS. Inlined third-party UI + // (Univer's office controls) has shipped unguarded keydown handlers that + // hijack ArrowUp/ArrowDown and break Chinese input (#562 regression); the + // document-capture guard neutralizes the whole class before React or any + // native listener sees the event. Registered as early as possible so no + // other capture-phase listener can win the ordering race. + ctx.effect( + () => { + try { + return registerImeGuard() + } catch (error) { + fail('ime guard', error) + return undefined + } + }, + 'dsh-better-sidebar: IME composition guard', + ) + // The "Side card" settings section: appears in the DSH Settings shell // once the shell's declaration is on the ledger (slots.inject waits for // it); the section reads/writes the prefs through the plugin's own diff --git a/tests/ime-guard.spec.ts b/tests/ime-guard.spec.ts new file mode 100644 index 0000000..43758e2 --- /dev/null +++ b/tests/ime-guard.spec.ts @@ -0,0 +1,187 @@ +// @vitest-environment jsdom +/** + * IME-composition guard tests. + * + * The guard registers document CAPTURE-phase keydown/keyup listeners that + * stopPropagation() while an input method is composing (isComposing or the + * legacy keyCode 229). These tests pin: + * + * 1. the pure decision (`isImeComposition`); + * 2. the native-listener path — a bubble listener on `document` and a + * target-phase listener on the input must NOT see composition keys, but + * must see every other key; + * 3. the React-synthetic path — a React `onKeyDown` handler (delegated at + * the root container) must not fire for composition keys, proving the + * document-capture listener wins the ordering race against React's + * delegation (the mechanism that inlined third-party UI, e.g. Univer's + * InputNumber, uses to hijack ArrowUp/ArrowDown); + * 4. the disposer restores normal flow (HMR-safe). + * + * Events are dispatched on a deep element (an `` in `document.body`) + * exactly like the browser does, so capture-phase blocking behaves like in + * production — dispatching on `document` itself would run all listeners in + * the target phase where stopPropagation does not stop same-node listeners. + */ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { createElement } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { act } from 'react-dom/test-utils' +import { isImeComposition, registerImeGuard } from '../src/client/ime-guard.ts' + +/** Build a KeyboardEvent the way jsdom allows: keyCode/isComposing via defineProperty. */ +function keyEvent( + type: 'keydown' | 'keyup', + init: { key: string; isComposing?: boolean; keyCode?: number; bubbles?: boolean }, +): KeyboardEvent { + const event = new KeyboardEvent(type, { key: init.key, bubbles: init.bubbles ?? true, cancelable: true }) + if (init.isComposing !== undefined) { + Object.defineProperty(event, 'isComposing', { value: init.isComposing }) + } + if (init.keyCode !== undefined) { + Object.defineProperty(event, 'keyCode', { value: init.keyCode }) + } + return event +} + +describe('isImeComposition', () => { + it('treats isComposing as composition', () => { + expect(isImeComposition({ isComposing: true, keyCode: 0 })).toBe(true) + }) + + it('treats keyCode 229 as composition (legacy engines without isComposing)', () => { + expect(isImeComposition({ isComposing: false, keyCode: 229 })).toBe(true) + }) + + it('treats both signals together as composition', () => { + expect(isImeComposition({ isComposing: true, keyCode: 229 })).toBe(true) + }) + + it('lets ordinary keys through', () => { + expect(isImeComposition({ isComposing: false, keyCode: 40 })).toBe(false) + expect(isImeComposition({ isComposing: false, keyCode: 0 })).toBe(false) + }) +}) + +describe('registerImeGuard — native listeners', () => { + let input: HTMLInputElement + let dispose: (() => void) | undefined + const seen: string[] = [] + + const onDocumentBubble = (event: Event): void => { + seen.push(`document:${event.type}`) + } + const onInputTarget = (event: Event): void => { + seen.push(`input:${event.type}`) + } + + beforeEach(() => { + input = document.createElement('input') + document.body.appendChild(input) + seen.length = 0 + document.addEventListener('keydown', onDocumentBubble) + document.addEventListener('keyup', onDocumentBubble) + input.addEventListener('keydown', onInputTarget) + input.addEventListener('keyup', onInputTarget) + }) + + afterEach(() => { + dispose?.() + dispose = undefined + document.removeEventListener('keydown', onDocumentBubble) + document.removeEventListener('keyup', onDocumentBubble) + input.removeEventListener('keydown', onInputTarget) + input.removeEventListener('keyup', onInputTarget) + input.remove() + }) + + it('lets ordinary keys reach document bubble and input target listeners', () => { + dispose = registerImeGuard() + input.dispatchEvent(keyEvent('keydown', { key: 'ArrowDown' })) + input.dispatchEvent(keyEvent('keyup', { key: 'ArrowDown' })) + expect(seen).toEqual(['input:keydown', 'document:keydown', 'input:keyup', 'document:keyup']) + }) + + it('blocks composition keys (isComposing) from every downstream listener', () => { + dispose = registerImeGuard() + input.dispatchEvent(keyEvent('keydown', { key: 'ArrowDown', isComposing: true })) + input.dispatchEvent(keyEvent('keyup', { key: 'ArrowDown', isComposing: true })) + expect(seen).toEqual([]) + }) + + it('blocks composition keys signalled by keyCode 229 only', () => { + dispose = registerImeGuard() + input.dispatchEvent(keyEvent('keydown', { key: 'ArrowDown', keyCode: 229 })) + expect(seen).toEqual([]) + }) + + it('still blocks after the composition signal is gone (guard is per-event)', () => { + dispose = registerImeGuard() + input.dispatchEvent(keyEvent('keydown', { key: 'ArrowDown', isComposing: true })) + input.dispatchEvent(keyEvent('keydown', { key: 'ArrowDown', isComposing: false })) + expect(seen).toEqual(['input:keydown', 'document:keydown']) + }) + + it('disposer restores normal flow', () => { + dispose = registerImeGuard() + input.dispatchEvent(keyEvent('keydown', { key: 'ArrowDown', isComposing: true })) + expect(seen).toEqual([]) + dispose() + dispose = undefined + input.dispatchEvent(keyEvent('keydown', { key: 'ArrowDown', isComposing: true })) + expect(seen).toEqual(['input:keydown', 'document:keydown']) + }) +}) + +describe('registerImeGuard — React synthetic path', () => { + let container: HTMLDivElement + let root: Root + let input: HTMLInputElement + let dispose: (() => void) | undefined + const seen: string[] = [] + + const onReactKeyDown = (): void => { + seen.push('react:keydown') + } + + beforeEach(() => { + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + act(() => { + root.render(createElement('input', { 'data-testid': 'ime-target', onKeyDown: onReactKeyDown })) + }) + input = container.querySelector('input') as HTMLInputElement + seen.length = 0 + }) + + afterEach(() => { + dispose?.() + dispose = undefined + act(() => { root.unmount() }) + container.remove() + }) + + it('baseline: React onKeyDown receives composition keys when the guard is absent', () => { + input.dispatchEvent(keyEvent('keydown', { key: 'ArrowDown', isComposing: true })) + expect(seen).toEqual(['react:keydown']) + }) + + it('guard blocks React synthetic onKeyDown during composition (capture beats delegation)', () => { + dispose = registerImeGuard() + input.dispatchEvent(keyEvent('keydown', { key: 'ArrowDown', isComposing: true })) + expect(seen).toEqual([]) + // Ordinary keys still flow through React after the guard is in place. + input.dispatchEvent(keyEvent('keydown', { key: 'ArrowDown', isComposing: false })) + expect(seen).toEqual(['react:keydown']) + }) + + it('disposer restores the React synthetic path', () => { + dispose = registerImeGuard() + input.dispatchEvent(keyEvent('keydown', { key: 'ArrowDown', isComposing: true })) + expect(seen).toEqual([]) + dispose() + dispose = undefined + input.dispatchEvent(keyEvent('keydown', { key: 'ArrowDown', isComposing: true })) + expect(seen).toEqual(['react:keydown']) + }) +})