From 03191a219f4d0751309bf3b2298d77d8d1bfbc10 Mon Sep 17 00:00:00 2001 From: landaiqing Date: Sat, 28 Mar 2026 22:35:09 +0800 Subject: [PATCH 01/26] :bug: Fixed the issue of scroll wheel modifying font size and the problem of automatic language detection failing --- frontend/package-lock.json | 205 +++++++++--------- .../toolbar/BlockLanguageSelector.vue | 4 +- frontend/src/stores/editorStore.ts | 10 +- .../editor/extensions/codeblock/copyPaste.ts | 113 +++++++--- .../codeblock/lang-detect/autodetect.ts | 13 +- .../editor/extensions/codeblock/parser.ts | 8 +- .../editor/extensions/codeblock/types.ts | 2 +- version.txt | 2 +- 8 files changed, 216 insertions(+), 141 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5be1c9d4..44b11da2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -78,7 +78,7 @@ "eslint-plugin-vue": "^10.6.2", "globals": "^16.5.0", "typescript": "^5.9.3", - "typescript-eslint": "^8.50.1", + "typescript-eslint": "^8.51.0", "unplugin-vue-components": "^30.0.0", "vite": "npm:rolldown-vite@latest", "vite-plugin-node-polyfills": "^0.24.0", @@ -142,7 +142,6 @@ "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz", "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/types": "^7.28.5" }, @@ -610,9 +609,9 @@ } }, "node_modules/@codemirror/view": { - "version": "6.39.6", - "resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.39.6.tgz", - "integrity": "sha512-/N+SoP5NndJjkGInp3BwlUa3KQKD6bDo0TV6ep37ueAdQ7BVu/PqlZNywmgjCq0MQoZadZd8T+MZucSr7fktyQ==", + "version": "6.39.8", + "resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.39.8.tgz", + "integrity": "sha512-1rASYd9Z/mE3tkbC9wInRlCNyCkSn+nLsiQKZhEDUUJiUfs/5FHDpCUDaQpoTIaNGeDc6/bhaEAyLmeEucEFPw==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.5.0", @@ -1341,13 +1340,13 @@ } }, "node_modules/@intlify/core-base": { - "version": "11.2.7", - "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.2.7.tgz", - "integrity": "sha512-+Ra9I/LAzXDnmv/IrTO03WMCiLya7pHRmGJvNl9fKwx/W4REJ0xaMk2PxCRqnxcBsX443amEMdebQ3R1geiuIw==", + "version": "11.2.8", + "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.2.8.tgz", + "integrity": "sha512-nBq6Y1tVkjIUsLsdOjDSJj4AsjvD0UG3zsg9Fyc+OivwlA/oMHSKooUy9tpKj0HqZ+NWFifweHavdljlBLTwdA==", "license": "MIT", "dependencies": { - "@intlify/message-compiler": "11.2.7", - "@intlify/shared": "11.2.7" + "@intlify/message-compiler": "11.2.8", + "@intlify/shared": "11.2.8" }, "engines": { "node": ">= 16" @@ -1357,12 +1356,12 @@ } }, "node_modules/@intlify/message-compiler": { - "version": "11.2.7", - "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.2.7.tgz", - "integrity": "sha512-TFamC+GzJAotAFwUNvbtRVBgvuSn2nCwKNresmPUHv3IIVMmXJt7QQJj/DORI1h8hs46ZF6L0Fs2xBohSOE4iQ==", + "version": "11.2.8", + "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.2.8.tgz", + "integrity": "sha512-A5n33doOjmHsBtCN421386cG1tWp5rpOjOYPNsnpjIJbQ4POF0QY2ezhZR9kr0boKwaHjbOifvyQvHj2UTrDFQ==", "license": "MIT", "dependencies": { - "@intlify/shared": "11.2.7", + "@intlify/shared": "11.2.8", "source-map-js": "^1.0.2" }, "engines": { @@ -1373,9 +1372,9 @@ } }, "node_modules/@intlify/shared": { - "version": "11.2.7", - "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-11.2.7.tgz", - "integrity": "sha512-uvlkvc/0uQ4FDlHQZccpUnmcOwNcaI3i+69ck2YJ+GqM35AoVbuS63b+YfirV4G0SZh64Ij2UMcFRMmB4nr95w==", + "version": "11.2.8", + "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-11.2.8.tgz", + "integrity": "sha512-l6e4NZyUgv8VyXXH4DbuucFOBmxLF56C/mqh2tvApbzl2Hrhi1aTDcuv5TKdxzfHYmpO3UB0Cz04fgDT9vszfw==", "license": "MIT", "engines": { "node": ">= 16" @@ -2469,21 +2468,21 @@ "license": "MIT" }, "node_modules/@toml-tools/lexer": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/@toml-tools/lexer/-/lexer-1.0.0.tgz", - "integrity": "sha512-rVoOC9FibF2CICwCBWQnYcjAEOmLCJExer178K2AsY0Nk9FjJNVoVJuR5UAtuq42BZOajvH+ainf6Gj2GpCnXQ==", + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@toml-tools/lexer/-/lexer-1.0.1.tgz", + "integrity": "sha512-jn2fl8m/9QPcUD507Hbt2W3TVMKzF5HEY8xKIxqY2r2dTG2udeCKlo2ejJ5k/RSOJsWNIuw+Ir/nxW5PItUApA==", "license": "MIT", "dependencies": { "chevrotain": "^11.0.1" } }, "node_modules/@toml-tools/parser": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/@toml-tools/parser/-/parser-1.0.0.tgz", - "integrity": "sha512-j8cd3A3ccLHppGoWI69urbiVJslrpwI6sZ61ySDUPxM/FTkQWRx/JkkF8aipnl0Ds0feWXyjyvmWzn70mIohYg==", + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@toml-tools/parser/-/parser-1.0.1.tgz", + "integrity": "sha512-W+YdnB8KDgKjIqhoArEXjiTTPnKSXVvI/B+raHfou9+sip3rxhzVsELn46GG7dZyNHyu9pS+gYgYrdF9c5AQDg==", "license": "MIT", "dependencies": { - "@toml-tools/lexer": "^1.0.0", + "@toml-tools/lexer": "^1.0.1", "chevrotain": "^11.0.1" } }, @@ -2865,23 +2864,25 @@ "resolved": "https://registry.npmmirror.com/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.1.tgz", - "integrity": "sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz", + "integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.50.1", - "@typescript-eslint/type-utils": "8.50.1", - "@typescript-eslint/utils": "8.50.1", - "@typescript-eslint/visitor-keys": "8.50.1", + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/type-utils": "8.51.0", + "@typescript-eslint/utils": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2891,7 +2892,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.50.1", + "@typescript-eslint/parser": "^8.51.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -2907,16 +2908,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.50.1.tgz", - "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.51.0.tgz", + "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.50.1", - "@typescript-eslint/types": "8.50.1", - "@typescript-eslint/typescript-estree": "8.50.1", - "@typescript-eslint/visitor-keys": "8.50.1", + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4" }, "engines": { @@ -2932,14 +2933,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.50.1.tgz", - "integrity": "sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.51.0.tgz", + "integrity": "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.50.1", - "@typescript-eslint/types": "^8.50.1", + "@typescript-eslint/tsconfig-utils": "^8.51.0", + "@typescript-eslint/types": "^8.51.0", "debug": "^4.3.4" }, "engines": { @@ -2954,14 +2955,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.50.1.tgz", - "integrity": "sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz", + "integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.1", - "@typescript-eslint/visitor-keys": "8.50.1" + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2972,9 +2973,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.1.tgz", - "integrity": "sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz", + "integrity": "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==", "dev": true, "license": "MIT", "engines": { @@ -2989,17 +2990,17 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.50.1.tgz", - "integrity": "sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz", + "integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.1", - "@typescript-eslint/typescript-estree": "8.50.1", - "@typescript-eslint/utils": "8.50.1", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/utils": "8.51.0", "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3014,9 +3015,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.50.1.tgz", - "integrity": "sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.51.0.tgz", + "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==", "dev": true, "license": "MIT", "engines": { @@ -3028,21 +3029,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.1.tgz", - "integrity": "sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz", + "integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.50.1", - "@typescript-eslint/tsconfig-utils": "8.50.1", - "@typescript-eslint/types": "8.50.1", - "@typescript-eslint/visitor-keys": "8.50.1", + "@typescript-eslint/project-service": "8.51.0", + "@typescript-eslint/tsconfig-utils": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3082,16 +3083,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.50.1.tgz", - "integrity": "sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.51.0.tgz", + "integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.50.1", - "@typescript-eslint/types": "8.50.1", - "@typescript-eslint/typescript-estree": "8.50.1" + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3106,13 +3107,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.1.tgz", - "integrity": "sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz", + "integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/types": "8.51.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -5733,6 +5734,8 @@ "integrity": "sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@types/node": "^20.0.0", "@types/whatwg-mimetype": "^3.0.2", @@ -5748,6 +5751,8 @@ "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -5757,7 +5762,9 @@ "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/has-flag": { "version": "4.0.0", @@ -8160,9 +8167,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -8231,16 +8238,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.50.1", - "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.50.1.tgz", - "integrity": "sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==", + "version": "8.51.0", + "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.51.0.tgz", + "integrity": "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.50.1", - "@typescript-eslint/parser": "8.50.1", - "@typescript-eslint/typescript-estree": "8.50.1", - "@typescript-eslint/utils": "8.50.1" + "@typescript-eslint/eslint-plugin": "8.51.0", + "@typescript-eslint/parser": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/utils": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8946,13 +8953,13 @@ } }, "node_modules/vue-i18n": { - "version": "11.2.7", - "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.2.7.tgz", - "integrity": "sha512-LPv8bAY5OA0UvFEXl4vBQOBqJzRrlExy92tWgRuwW7tbykHf7CH71G2Y4TM2OwGcIS4+hyqKHS2EVBqaYwPY9Q==", + "version": "11.2.8", + "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.2.8.tgz", + "integrity": "sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==", "license": "MIT", "dependencies": { - "@intlify/core-base": "11.2.7", - "@intlify/shared": "11.2.7", + "@intlify/core-base": "11.2.8", + "@intlify/shared": "11.2.8", "@vue/devtools-api": "^6.5.0" }, "engines": { @@ -9041,6 +9048,8 @@ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=12" } diff --git a/frontend/src/components/toolbar/BlockLanguageSelector.vue b/frontend/src/components/toolbar/BlockLanguageSelector.vue index 071fdc39..2ad3bb2e 100644 --- a/frontend/src/components/toolbar/BlockLanguageSelector.vue +++ b/frontend/src/components/toolbar/BlockLanguageSelector.vue @@ -183,7 +183,7 @@ const selectLanguage = (languageId: SupportedLanguage) => { try { const view = editorStore.currentEditor; const state = view.state; - const dispatch = view.dispatch; + const dispatch = view.dispatch.bind(view); const [targetLanguage, autoDetect] = languageId === 'auto' ? ['text', true] @@ -510,4 +510,4 @@ const scrollToCurrentLanguage = () => { background-color: var(--text-muted); } } - \ No newline at end of file + diff --git a/frontend/src/stores/editorStore.ts b/frontend/src/stores/editorStore.ts index 10678fca..fd95a1f7 100644 --- a/frontend/src/stores/editorStore.ts +++ b/frontend/src/stores/editorStore.ts @@ -92,8 +92,14 @@ export const useEditorStore = defineStore('editor', () => { // 滚轮缩放扩展 const wheelZoomExtension = createWheelZoomExtension({ - increaseFontSize: () => configStore.increaseFontSizeLocal(), - decreaseFontSize: () => configStore.decreaseFontSizeLocal(), + increaseFontSize: () => { + configStore.increaseFontSizeLocal(); + applyFontSettings(); + }, + decreaseFontSize: () => { + configStore.decreaseFontSizeLocal(); + applyFontSettings(); + }, onSave: () => configStore.saveFontSize(), saveDelay: 1000 }); diff --git a/frontend/src/views/editor/extensions/codeblock/copyPaste.ts b/frontend/src/views/editor/extensions/codeblock/copyPaste.ts index 0f51a62c..4a90db89 100644 --- a/frontend/src/views/editor/extensions/codeblock/copyPaste.ts +++ b/frontend/src/views/editor/extensions/codeblock/copyPaste.ts @@ -8,6 +8,7 @@ import { EditorView } from "@codemirror/view"; import { Command } from "@codemirror/view"; import { LANGUAGES } from "./lang-parser/languages"; import { USER_EVENTS, codeBlockEvent, CONTENT_EDIT } from "./annotation"; +import * as runtime from "@wailsio/runtime"; /** * 构建块分隔符正则表达式 @@ -62,29 +63,37 @@ function copiedRange(state: EditorState, forCut: boolean = false) { */ export const codeBlockCopyCut = EditorView.domEventHandlers({ copy(event, view) { + event.preventDefault(); + let { text } = copiedRange(view.state); - // 将块分隔符替换为双换行符 text = text.replaceAll(blockSeparatorRegex, "\n\n"); - const data = event.clipboardData; - if (data) { - event.preventDefault(); - data.clearData(); - data.setData("text/plain", text); - } + // 优先使用 Wails 原生剪贴板 API + runtime.Clipboard.SetText(text).catch(() => { + // 降级方案:使用浏览器剪贴板 + const data = event.clipboardData; + if (data) { + data.clearData(); + data.setData("text/plain", text); + } + }); }, cut(event, view) { + event.preventDefault(); + let { text, ranges } = copiedRange(view.state, true); - // 将块分隔符替换为双换行符 text = text.replaceAll(blockSeparatorRegex, "\n\n"); - const data = event.clipboardData; - if (data) { - event.preventDefault(); - data.clearData(); - data.setData("text/plain", text); - } + // 优先使用 Wails 原生剪贴板 API + runtime.Clipboard.SetText(text).catch(() => { + // 降级方案:使用浏览器剪贴板 + const data = event.clipboardData; + if (data) { + data.clearData(); + data.setData("text/plain", text); + } + }); if (!view.state.readOnly) { view.dispatch({ @@ -94,20 +103,52 @@ export const codeBlockCopyCut = EditorView.domEventHandlers({ annotations: [codeBlockEvent.of(CONTENT_EDIT)], }); } + }, + + paste(event, view) { + if (view.state.readOnly) { + return false; + } + + event.preventDefault(); + + // 使用 Wails 原生剪贴板 API + runtime.Clipboard.Text() + .then(text => { + if (text) { + doPaste(view, text); + } + }) + .catch(error => { + console.error('[Clipboard] Failed to read from system clipboard:', error); + + const data = event.clipboardData; + if (data) { + const text = data.getData("text/plain"); + if (text) { + doPaste(view, text); + } + } + }); + + return true; } }); /** - * 复制和剪切的通用函数 + * 复制和剪切的通用函数 - 使用 Wails 原生剪贴板 API */ const copyCut = (view: EditorView, cut: boolean): boolean => { let { text, ranges } = copiedRange(view.state, cut); - // 将块分隔符替换为双换行符 text = text.replaceAll(blockSeparatorRegex, "\n\n"); - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(text); - } + runtime.Clipboard.SetText(text).catch(err => { + console.error('[Clipboard] Failed to write to system clipboard:', err); + + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text); + } + }); if (cut && !view.state.readOnly) { view.dispatch({ @@ -166,20 +207,32 @@ export const cutCommand: Command = (view) => { }; /** - * 粘贴命令 + * 粘贴命令 - 使用 Wails 原生剪贴板 API */ export const pasteCommand: Command = (view) => { - if (navigator.clipboard && navigator.clipboard.readText) { - navigator.clipboard.readText() - .then(text => { + // 使用 Wails 原生剪贴板 API,正确处理系统编码 + runtime.Clipboard.Text() + .then(text => { + if (text) { doPaste(view, text); - }) - .catch(err => { - console.error('Failed to read from clipboard:', err); - }); - } else { - console.warn('The clipboard API is not available, please use your browser\'s native paste feature'); - } + } + }) + .catch(err => { + console.error('[Clipboard] Failed to read from system clipboard:', err); + + if (navigator.clipboard && navigator.clipboard.readText) { + navigator.clipboard.readText() + .then(text => { + if (text) { + doPaste(view, text); + } + }) + .catch(fallbackErr => { + console.error('[Clipboard] Fallback also failed:', fallbackErr); + }); + } + }); + return true; }; diff --git a/frontend/src/views/editor/extensions/codeblock/lang-detect/autodetect.ts b/frontend/src/views/editor/extensions/codeblock/lang-detect/autodetect.ts index 5f0bc0ec..e4a722fa 100644 --- a/frontend/src/views/editor/extensions/codeblock/lang-detect/autodetect.ts +++ b/frontend/src/views/editor/extensions/codeblock/lang-detect/autodetect.ts @@ -68,13 +68,20 @@ function createDetectionMap(): Map { LANGUAGES.forEach(lang => { if (lang.detectIds) { lang.detectIds.forEach(detectId => { - map.set(detectId, lang.token); + // 保留首个映射,避免重复 detectId 覆盖更基础的语言,例如 js -> ts。 + if (!map.has(detectId)) { + map.set(detectId, lang.token); + } }); } }); return map; } +function createWorkerUrl(): URL { + return new URL(`${import.meta.env.BASE_URL}langdetect-worker.js`, window.location.href); +} + /** * 检测ID到语言token的映射表 */ @@ -131,7 +138,7 @@ class LanguageDetectionWorker { */ private initWorker(): void { try { - this.worker = new Worker('/langdetect-worker.js'); + this.worker = new Worker(createWorkerUrl()); this.worker.onmessage = (event) => { const response: WorkerResponse = event.data; const request = this.pendingRequests.get(response.idx); @@ -316,4 +323,4 @@ export async function detectLanguages(contents: string[]): Promise { if (langChild.type.id === BlockLanguage) { diff --git a/frontend/src/views/editor/extensions/codeblock/types.ts b/frontend/src/views/editor/extensions/codeblock/types.ts index 47c49896..f8b1eadd 100644 --- a/frontend/src/views/editor/extensions/codeblock/types.ts +++ b/frontend/src/views/editor/extensions/codeblock/types.ts @@ -87,7 +87,7 @@ export interface EditorOptions { // 分隔符格式常量 -export const DELIMITER_REGEX = /^\n∞∞∞([a-zA-Z0-9_-]+)(-a)?\n/gm; +export const DELIMITER_REGEX = /^\n∞∞∞([a-zA-Z0-9_]+)(-a)?\n/gm; export const DELIMITER_PREFIX = '\n∞∞∞'; export const DELIMITER_SUFFIX = '\n'; export const AUTO_DETECT_SUFFIX = '-a'; diff --git a/version.txt b/version.txt index ead97297..c0ad9452 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -VERSION=1.5.6 +VERSION=1.5.7 From 34c8f2a18598825470135df615e952da750764e6 Mon Sep 17 00:00:00 2001 From: landaiqing Date: Sun, 29 Mar 2026 00:25:23 +0800 Subject: [PATCH 02/26] :bug: Fixed java prettier invalidation --- .../wails/v3/pkg/services/dock/dockservice.ts | 8 + frontend/package-lock.json | 3729 ++++++++--------- frontend/package.json | 84 +- .../common/prettier/plugins/clang/index.ts | 26 +- .../prettier/plugins/java/comments.d.ts | 17 - .../common/prettier/plugins/java/comments.js | 199 - .../common/prettier/plugins/java/index.d.ts | 563 --- .../src/common/prettier/plugins/java/index.js | 29 - .../common/prettier/plugins/java/options.d.ts | 43 - .../common/prettier/plugins/java/options.js | 284 -- .../common/prettier/plugins/java/parser.d.ts | 9 - .../common/prettier/plugins/java/parser.js | 24 - .../common/prettier/plugins/java/printer.d.ts | 18 - .../common/prettier/plugins/java/printer.js | 40 - .../plugins/java/printers/arrays.d.ts | 9 - .../prettier/plugins/java/printers/arrays.js | 9 - .../java/printers/blocks-and-statements.d.ts | 117 - .../java/printers/blocks-and-statements.js | 337 -- .../plugins/java/printers/classes.d.ts | 157 - .../prettier/plugins/java/printers/classes.js | 446 -- .../plugins/java/printers/expressions.d.ts | 134 - .../plugins/java/printers/expressions.js | 598 --- .../plugins/java/printers/helpers.d.ts | 71 - .../prettier/plugins/java/printers/helpers.js | 239 -- .../prettier/plugins/java/printers/index.d.ts | 2 - .../prettier/plugins/java/printers/index.js | 13 - .../plugins/java/printers/interfaces.d.ts | 62 - .../plugins/java/printers/interfaces.js | 157 - .../java/printers/lexical-structure.d.ts | 14 - .../java/printers/lexical-structure.js | 29 - .../prettier/plugins/java/printers/names.d.ts | 12 - .../prettier/plugins/java/printers/names.js | 11 - .../java/printers/packages-and-modules.d.ts | 46 - .../java/printers/packages-and-modules.js | 169 - .../printers/types-values-and-variables.d.ts | 46 - .../printers/types-values-and-variables.js | 90 - .../codeblock/lang-parser/languages.ts | 11 +- go.mod | 61 +- go.sum | 144 +- 39 files changed, 1914 insertions(+), 6143 deletions(-) delete mode 100644 frontend/src/common/prettier/plugins/java/comments.d.ts delete mode 100644 frontend/src/common/prettier/plugins/java/comments.js delete mode 100644 frontend/src/common/prettier/plugins/java/index.d.ts delete mode 100644 frontend/src/common/prettier/plugins/java/index.js delete mode 100644 frontend/src/common/prettier/plugins/java/options.d.ts delete mode 100644 frontend/src/common/prettier/plugins/java/options.js delete mode 100644 frontend/src/common/prettier/plugins/java/parser.d.ts delete mode 100644 frontend/src/common/prettier/plugins/java/parser.js delete mode 100644 frontend/src/common/prettier/plugins/java/printer.d.ts delete mode 100644 frontend/src/common/prettier/plugins/java/printer.js delete mode 100644 frontend/src/common/prettier/plugins/java/printers/arrays.d.ts delete mode 100644 frontend/src/common/prettier/plugins/java/printers/arrays.js delete mode 100644 frontend/src/common/prettier/plugins/java/printers/blocks-and-statements.d.ts delete mode 100644 frontend/src/common/prettier/plugins/java/printers/blocks-and-statements.js delete mode 100644 frontend/src/common/prettier/plugins/java/printers/classes.d.ts delete mode 100644 frontend/src/common/prettier/plugins/java/printers/classes.js delete mode 100644 frontend/src/common/prettier/plugins/java/printers/expressions.d.ts delete mode 100644 frontend/src/common/prettier/plugins/java/printers/expressions.js delete mode 100644 frontend/src/common/prettier/plugins/java/printers/helpers.d.ts delete mode 100644 frontend/src/common/prettier/plugins/java/printers/helpers.js delete mode 100644 frontend/src/common/prettier/plugins/java/printers/index.d.ts delete mode 100644 frontend/src/common/prettier/plugins/java/printers/index.js delete mode 100644 frontend/src/common/prettier/plugins/java/printers/interfaces.d.ts delete mode 100644 frontend/src/common/prettier/plugins/java/printers/interfaces.js delete mode 100644 frontend/src/common/prettier/plugins/java/printers/lexical-structure.d.ts delete mode 100644 frontend/src/common/prettier/plugins/java/printers/lexical-structure.js delete mode 100644 frontend/src/common/prettier/plugins/java/printers/names.d.ts delete mode 100644 frontend/src/common/prettier/plugins/java/printers/names.js delete mode 100644 frontend/src/common/prettier/plugins/java/printers/packages-and-modules.d.ts delete mode 100644 frontend/src/common/prettier/plugins/java/printers/packages-and-modules.js delete mode 100644 frontend/src/common/prettier/plugins/java/printers/types-values-and-variables.d.ts delete mode 100644 frontend/src/common/prettier/plugins/java/printers/types-values-and-variables.js diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/dock/dockservice.ts b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/dock/dockservice.ts index 732e56fa..58ceb2fd 100644 --- a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/dock/dockservice.ts +++ b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/dock/dockservice.ts @@ -18,6 +18,14 @@ import * as application$0 from "../../application/models.js"; // @ts-ignore: Unused imports import * as $models from "./models.js"; +/** + * GetBadge returns the badge label on the application icon. + */ +export function GetBadge(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(1150236961) as any; + return $resultPromise; +} + /** * HideAppIcon hides the app icon in the dock/taskbar. */ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 44b11da2..ed84bb3b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,19 +8,19 @@ "name": "frontend", "version": "0.0.0", "dependencies": { - "@codemirror/autocomplete": "^6.20.0", - "@codemirror/commands": "^6.10.1", + "@codemirror/autocomplete": "^6.20.1", + "@codemirror/commands": "^6.10.3", "@codemirror/lang-angular": "^0.1.4", "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-go": "^6.0.1", "@codemirror/lang-html": "^6.4.11", "@codemirror/lang-java": "^6.0.2", - "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-json": "^6.0.2", "@codemirror/lang-less": "^6.0.2", "@codemirror/lang-lezer": "^6.0.2", - "@codemirror/lang-liquid": "^6.3.1", + "@codemirror/lang-liquid": "^6.3.2", "@codemirror/lang-markdown": "^6.5.0", "@codemirror/lang-php": "^6.0.2", "@codemirror/lang-python": "^6.2.1", @@ -29,63 +29,62 @@ "@codemirror/lang-sql": "^6.10.0", "@codemirror/lang-vue": "^0.1.3", "@codemirror/lang-wast": "^6.0.2", - "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "^6.12.1", + "@codemirror/lang-yaml": "^6.1.3", + "@codemirror/language": "^6.12.3", "@codemirror/language-data": "^6.5.2", "@codemirror/legacy-modes": "^6.5.2", - "@codemirror/lint": "^6.9.2", - "@codemirror/search": "^6.5.11", - "@codemirror/state": "^6.5.3", - "@codemirror/view": "^6.39.8", + "@codemirror/lint": "^6.9.5", + "@codemirror/search": "^6.6.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.40.0", "@cospaia/prettier-plugin-clojure": "^0.0.2", "@lezer/highlight": "^1.2.3", - "@lezer/lr": "^1.4.5", + "@lezer/lr": "^1.4.8", "@prettier/plugin-xml": "^3.4.2", "@replit/codemirror-lang-svelte": "^6.0.0", "@toml-tools/lexer": "^1.0.1", "@toml-tools/parser": "^1.0.1", - "@types/katex": "^0.16.7", - "@zumer/snapdom": "^2.0.1", + "@types/katex": "^0.16.8", + "@zumer/snapdom": "^2.7.0", "codemirror": "^6.0.2", - "codemirror-lang-elixir": "^4.0.0", - "colors-named": "^1.0.4", - "colors-named-hex": "^1.0.3", + "codemirror-lang-elixir": "^4.0.1", + "colors-named": "^1.0.5", + "colors-named-hex": "^1.0.4", "groovy-beautify": "^0.0.17", "hsl-matcher": "^1.2.4", - "java-parser": "^3.0.1", - "katex": "^0.16.27", - "linguist-languages": "^9.1.11", - "marked": "^17.0.1", - "mermaid": "^11.12.2", - "php-parser": "^3.2.5", + "katex": "^0.16.44", + "linguist-languages": "^9.3.1", + "marked": "^17.0.5", + "mermaid": "^11.13.0", + "php-parser": "^3.5.0", "pinia": "^3.0.4", "pinia-plugin-persistedstate": "^4.7.1", - "prettier": "^3.7.4", - "sass": "^1.97.1", - "vue": "^3.5.26", - "vue-i18n": "^11.2.8", + "prettier": "^3.8.1", + "sass": "^1.98.0", + "vue": "^3.5.31", + "vue-i18n": "^11.3.0", "vue-pick-colors": "^1.8.0", - "vue-router": "^4.6.4" + "vue-router": "^5.0.4" }, "devDependencies": { - "@eslint/js": "^9.39.2", + "@eslint/js": "^10.0.1", "@lezer/generator": "^1.8.0", - "@types/node": "^25.0.3", - "@vitejs/plugin-vue": "^6.0.3", - "@wailsio/runtime": "^3.0.0-alpha.77", + "@types/node": "^25.5.0", + "@vitejs/plugin-vue": "^6.0.5", + "@wailsio/runtime": "^3.0.0-alpha.79", "cross-env": "^10.1.0", - "eslint": "^9.39.2", - "eslint-plugin-vue": "^10.6.2", - "globals": "^16.5.0", + "eslint": "^10.1.0", + "eslint-plugin-vue": "^10.8.0", + "globals": "^17.4.0", "typescript": "^5.9.3", - "typescript-eslint": "^8.51.0", - "unplugin-vue-components": "^30.0.0", - "vite": "npm:rolldown-vite@latest", - "vite-plugin-node-polyfills": "^0.24.0", + "typescript-eslint": "^8.57.2", + "unplugin-vue-components": "^32.0.0", + "vite": "^7.3.1", + "vite-plugin-node-polyfills": "^0.25.0", "vitepress": "^2.0.0-alpha.12", - "vitest": "^4.0.16", - "vue-eslint-parser": "^10.2.0", - "vue-tsc": "^3.2.1" + "vitest": "^4.1.2", + "vue-eslint-parser": "^10.4.0", + "vue-tsc": "^3.2.6" } }, "node_modules/@antfu/install-pkg": { @@ -101,22 +100,20 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@antfu/install-pkg/node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, "engines": { - "node": ">=18" - } - }, - "node_modules/@antfu/utils": { - "version": "9.3.0", - "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-9.3.0.tgz", - "integrity": "sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" + "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { @@ -138,12 +135,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.2", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -153,9 +150,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -166,54 +163,54 @@ } }, "node_modules/@braintree/sanitize-url": { - "version": "7.1.1", - "resolved": "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", - "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==", + "version": "7.1.2", + "resolved": "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", "license": "MIT" }, "node_modules/@chevrotain/cst-dts-gen": { - "version": "11.0.3", - "resolved": "https://registry.npmmirror.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", - "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "version": "11.2.0", + "resolved": "https://registry.npmmirror.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.2.0.tgz", + "integrity": "sha512-ssJFvn/UXhQQeICw3SR/fZPmYVj+JM2mP+Lx7bZ51cOeHaMWOKp3AUMuyM3QR82aFFXTfcAp67P5GpPjGmbZWQ==", "license": "Apache-2.0", "dependencies": { - "@chevrotain/gast": "11.0.3", - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" + "@chevrotain/gast": "11.2.0", + "@chevrotain/types": "11.2.0", + "lodash-es": "4.17.23" } }, "node_modules/@chevrotain/gast": { - "version": "11.0.3", - "resolved": "https://registry.npmmirror.com/@chevrotain/gast/-/gast-11.0.3.tgz", - "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "version": "11.2.0", + "resolved": "https://registry.npmmirror.com/@chevrotain/gast/-/gast-11.2.0.tgz", + "integrity": "sha512-c+KoD6eSI1xjAZZoNUW+V0l13UEn+a4ShmUrjIKs1BeEWCji0Kwhmqn5FSx1K4BhWL7IQKlV7wLR4r8lLArORQ==", "license": "Apache-2.0", "dependencies": { - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" + "@chevrotain/types": "11.2.0", + "lodash-es": "4.17.23" } }, "node_modules/@chevrotain/regexp-to-ast": { - "version": "11.0.3", - "resolved": "https://registry.npmmirror.com/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", - "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "version": "11.2.0", + "resolved": "https://registry.npmmirror.com/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.2.0.tgz", + "integrity": "sha512-lG73pBFqbXODTbXhdZwv0oyUaI+3Irm+uOv5/W79lI3g5hasYaJnVJOm3H2NkhA0Ef4XLBU4Scr7TJDJwgFkAw==", "license": "Apache-2.0" }, "node_modules/@chevrotain/types": { - "version": "11.0.3", - "resolved": "https://registry.npmmirror.com/@chevrotain/types/-/types-11.0.3.tgz", - "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "version": "11.2.0", + "resolved": "https://registry.npmmirror.com/@chevrotain/types/-/types-11.2.0.tgz", + "integrity": "sha512-vBMSj/lz/LqolbGQEHB0tlpW5BnljHVtp+kzjQfQU+5BtGMTuZCPVgaAjtKvQYXnHb/8i/02Kii00y0tsuwfsw==", "license": "Apache-2.0" }, "node_modules/@chevrotain/utils": { - "version": "11.0.3", - "resolved": "https://registry.npmmirror.com/@chevrotain/utils/-/utils-11.0.3.tgz", - "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "version": "11.2.0", + "resolved": "https://registry.npmmirror.com/@chevrotain/utils/-/utils-11.2.0.tgz", + "integrity": "sha512-+7whECg4yNWHottjvr2To2BRxL4XJVjIyyv5J4+bJ0iMOVU8j/8n1qPDLZS/90W/BObDR8VNL46lFbzY/Hosmw==", "license": "Apache-2.0" }, "node_modules/@codemirror/autocomplete": { - "version": "6.20.0", - "resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", - "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==", + "version": "6.20.1", + "resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz", + "integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -223,13 +220,13 @@ } }, "node_modules/@codemirror/commands": { - "version": "6.10.1", - "resolved": "https://registry.npmmirror.com/@codemirror/commands/-/commands-6.10.1.tgz", - "integrity": "sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==", + "version": "6.10.3", + "resolved": "https://registry.npmmirror.com/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.4.0", + "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } @@ -312,9 +309,9 @@ } }, "node_modules/@codemirror/lang-javascript": { - "version": "6.2.4", - "resolved": "https://registry.npmmirror.com/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", - "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", + "version": "6.2.5", + "resolved": "https://registry.npmmirror.com/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", + "integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -375,9 +372,9 @@ } }, "node_modules/@codemirror/lang-liquid": { - "version": "6.3.1", - "resolved": "https://registry.npmmirror.com/@codemirror/lang-liquid/-/lang-liquid-6.3.1.tgz", - "integrity": "sha512-S/jE/D7iij2Pu70AC65ME6AYWxOOcX20cSJvaPgY5w7m2sfxsArAcUAuUgm/CZCVmqoi9KiOlS7gj/gyLipABw==", + "version": "6.3.2", + "resolved": "https://registry.npmmirror.com/@codemirror/lang-liquid/-/lang-liquid-6.3.2.tgz", + "integrity": "sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -509,9 +506,9 @@ } }, "node_modules/@codemirror/lang-yaml": { - "version": "6.1.2", - "resolved": "https://registry.npmmirror.com/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz", - "integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==", + "version": "6.1.3", + "resolved": "https://registry.npmmirror.com/@codemirror/lang-yaml/-/lang-yaml-6.1.3.tgz", + "integrity": "sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -524,9 +521,9 @@ } }, "node_modules/@codemirror/language": { - "version": "6.12.1", - "resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.12.1.tgz", - "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==", + "version": "6.12.3", + "resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.12.3.tgz", + "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -578,9 +575,9 @@ } }, "node_modules/@codemirror/lint": { - "version": "6.9.2", - "resolved": "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.9.2.tgz", - "integrity": "sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==", + "version": "6.9.5", + "resolved": "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.9.5.tgz", + "integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -589,32 +586,32 @@ } }, "node_modules/@codemirror/search": { - "version": "6.5.11", - "resolved": "https://registry.npmmirror.com/@codemirror/search/-/search-6.5.11.tgz", - "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", + "version": "6.6.0", + "resolved": "https://registry.npmmirror.com/@codemirror/search/-/search-6.6.0.tgz", + "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", + "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "node_modules/@codemirror/state": { - "version": "6.5.3", - "resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.3.tgz", - "integrity": "sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==", + "version": "6.6.0", + "resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", "license": "MIT", "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "node_modules/@codemirror/view": { - "version": "6.39.8", - "resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.39.8.tgz", - "integrity": "sha512-1rASYd9Z/mE3tkbC9wInRlCNyCkSn+nLsiQKZhEDUUJiUfs/5FHDpCUDaQpoTIaNGeDc6/bhaEAyLmeEucEFPw==", + "version": "6.40.0", + "resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.40.0.tgz", + "integrity": "sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg==", "license": "MIT", "dependencies": { - "@codemirror/state": "^6.5.0", + "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" @@ -627,16 +624,23 @@ "license": "MIT" }, "node_modules/@docsearch/css": { - "version": "4.2.0", - "resolved": "https://registry.npmmirror.com/@docsearch/css/-/css-4.2.0.tgz", - "integrity": "sha512-65KU9Fw5fGsPPPlgIghonMcndyx1bszzrDQYLfierN+Ha29yotMHzVS94bPkZS6On9LS8dE4qmW4P/fGjtCf/g==", + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/@docsearch/css/-/css-4.6.2.tgz", + "integrity": "sha512-fH/cn8BjEEdM2nJdjNMHIvOVYupG6AIDtFVDgIZrNzdCSj4KXr9kd+hsehqsNGYjpUjObeKYKvgy/IwCb1jZYQ==", "dev": true, "license": "MIT" }, "node_modules/@docsearch/js": { - "version": "4.2.0", - "resolved": "https://registry.npmmirror.com/@docsearch/js/-/js-4.2.0.tgz", - "integrity": "sha512-KBHVPO29QiGUFJYeAqxW0oXtGf/aghNmRrIRPT4/28JAefqoCkNn/ZM/jeQ7fHjl0KNM6C+KlLVYjwyz6lNZnA==", + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/@docsearch/js/-/js-4.6.2.tgz", + "integrity": "sha512-qj1yoxl3y4GKoK7+VM6fq/rQqPnvUmg3IKzJ9x0VzN14QVzdB/SG/J6VfV1BWT5RcPUFxIcVwoY1fwHM2fSRRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/sidepanel-js": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/@docsearch/sidepanel-js/-/sidepanel-js-4.6.2.tgz", + "integrity": "sha512-Pni85AP/GwRj7fFg8cBJp0U04tzbueBvWSd3gysgnOsVnQVSZwSYncfErUScLE1CAtR+qocPDFjmYR9AMRNJtQ==", "dev": true, "license": "MIT" }, @@ -648,9 +652,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", - "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", "cpu": [ "ppc64" ], @@ -665,9 +669,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.2.tgz", - "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", "cpu": [ "arm" ], @@ -682,9 +686,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", - "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", "cpu": [ "arm64" ], @@ -699,9 +703,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.2.tgz", - "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", "cpu": [ "x64" ], @@ -716,9 +720,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", - "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", "cpu": [ "arm64" ], @@ -733,9 +737,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", - "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", "cpu": [ "x64" ], @@ -750,9 +754,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", - "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", "cpu": [ "arm64" ], @@ -767,9 +771,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", - "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", "cpu": [ "x64" ], @@ -784,9 +788,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", - "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", "cpu": [ "arm" ], @@ -801,9 +805,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", - "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", "cpu": [ "arm64" ], @@ -818,9 +822,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", - "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", "cpu": [ "ia32" ], @@ -835,9 +839,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", - "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", "cpu": [ "loong64" ], @@ -852,9 +856,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", - "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", "cpu": [ "mips64el" ], @@ -869,9 +873,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", - "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", "cpu": [ "ppc64" ], @@ -886,9 +890,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", - "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", "cpu": [ "riscv64" ], @@ -903,9 +907,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", - "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", "cpu": [ "s390x" ], @@ -920,9 +924,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", - "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", "cpu": [ "x64" ], @@ -937,9 +941,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", - "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", "cpu": [ "arm64" ], @@ -954,9 +958,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", - "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", "cpu": [ "x64" ], @@ -971,9 +975,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", - "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", "cpu": [ "arm64" ], @@ -988,9 +992,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", - "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", "cpu": [ "x64" ], @@ -1004,10 +1008,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", - "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", "cpu": [ "x64" ], @@ -1022,9 +1043,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", - "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", "cpu": [ "arm64" ], @@ -1039,9 +1060,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", - "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", "cpu": [ "ia32" ], @@ -1056,9 +1077,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", - "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", "cpu": [ "x64" ], @@ -1073,9 +1094,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1105,9 +1126,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -1115,118 +1136,89 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.23.3", + "resolved": "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.7", + "@eslint/object-schema": "^3.0.3", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^10.2.4" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "version": "0.5.3", + "resolved": "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0" + "@eslint/core": "^1.1.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmmirror.com/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "10.0.1", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0", + "@eslint/core": "^1.1.1", "levn": "^0.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@humanfs/core": { @@ -1240,33 +1232,19 @@ } }, "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "version": "0.16.7", + "resolved": "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1282,9 +1260,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1296,9 +1274,9 @@ } }, "node_modules/@iconify-json/simple-icons": { - "version": "1.2.56", - "resolved": "https://registry.npmmirror.com/@iconify-json/simple-icons/-/simple-icons-1.2.56.tgz", - "integrity": "sha512-oAvxOzgSjfvdj/Jsi3S7HDUCxO8/n2j8e1w1e/FktHUAXiWjNX00n3Tu3AP+n1ayKrypcUDXCzxn+0ENMl6ouw==", + "version": "1.2.75", + "resolved": "https://registry.npmmirror.com/@iconify-json/simple-icons/-/simple-icons-1.2.75.tgz", + "integrity": "sha512-KvcCUbvcBWb0sbqLIxHoY8z5/piXY08wcY9gfMhF+ph3AfzGMaSmZFkUY71HSXAljQngXkgs4bdKdekO0HQWvg==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -1312,41 +1290,41 @@ "license": "MIT" }, "node_modules/@iconify/utils": { - "version": "3.0.2", - "resolved": "https://registry.npmmirror.com/@iconify/utils/-/utils-3.0.2.tgz", - "integrity": "sha512-EfJS0rLfVuRuJRn4psJHtK2A9TqVnkxPpHY6lYHiB9+8eSuudsxbwMiavocG45ujOo6FJ+CIRlRnlOGinzkaGQ==", + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@iconify/utils/-/utils-3.1.0.tgz", + "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", "license": "MIT", "dependencies": { "@antfu/install-pkg": "^1.1.0", - "@antfu/utils": "^9.2.0", "@iconify/types": "^2.0.0", - "debug": "^4.4.1", - "globals": "^15.15.0", - "kolorist": "^1.8.0", - "local-pkg": "^1.1.1", - "mlly": "^1.7.4" + "mlly": "^1.8.0" } }, - "node_modules/@iconify/utils/node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmmirror.com/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "node_modules/@intlify/core-base": { + "version": "11.3.0", + "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.3.0.tgz", + "integrity": "sha512-NNX5jIwF4TJBe7RtSKDMOA6JD9mp2mRcBHAwt2X+Q8PvnZub0yj5YYXlFu2AcESdgQpEv/5Yx2uOCV/yh7YkZg==", "license": "MIT", + "dependencies": { + "@intlify/devtools-types": "11.3.0", + "@intlify/message-compiler": "11.3.0", + "@intlify/shared": "11.3.0" + }, "engines": { - "node": ">=18" + "node": ">= 16" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/kazupon" } }, - "node_modules/@intlify/core-base": { - "version": "11.2.8", - "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.2.8.tgz", - "integrity": "sha512-nBq6Y1tVkjIUsLsdOjDSJj4AsjvD0UG3zsg9Fyc+OivwlA/oMHSKooUy9tpKj0HqZ+NWFifweHavdljlBLTwdA==", + "node_modules/@intlify/devtools-types": { + "version": "11.3.0", + "resolved": "https://registry.npmmirror.com/@intlify/devtools-types/-/devtools-types-11.3.0.tgz", + "integrity": "sha512-G9CNL4WpANWVdUjubOIIS7/D2j/0j+1KJmhBJxHilWNKr9mmt3IjFV3Hq4JoBP23uOoC5ynxz/FHZ42M+YxfGw==", "license": "MIT", "dependencies": { - "@intlify/message-compiler": "11.2.8", - "@intlify/shared": "11.2.8" + "@intlify/core-base": "11.3.0", + "@intlify/shared": "11.3.0" }, "engines": { "node": ">= 16" @@ -1356,12 +1334,12 @@ } }, "node_modules/@intlify/message-compiler": { - "version": "11.2.8", - "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.2.8.tgz", - "integrity": "sha512-A5n33doOjmHsBtCN421386cG1tWp5rpOjOYPNsnpjIJbQ4POF0QY2ezhZR9kr0boKwaHjbOifvyQvHj2UTrDFQ==", + "version": "11.3.0", + "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.3.0.tgz", + "integrity": "sha512-RAJp3TMsqohg/Wa7bVF3cChRhecSYBLrTCQSj7j0UtWVFLP+6iEJoE2zb7GU5fp+fmG5kCbUdzhmlAUCWXiUJw==", "license": "MIT", "dependencies": { - "@intlify/shared": "11.2.8", + "@intlify/shared": "11.3.0", "source-map-js": "^1.0.2" }, "engines": { @@ -1372,9 +1350,9 @@ } }, "node_modules/@intlify/shared": { - "version": "11.2.8", - "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-11.2.8.tgz", - "integrity": "sha512-l6e4NZyUgv8VyXXH4DbuucFOBmxLF56C/mqh2tvApbzl2Hrhi1aTDcuv5TKdxzfHYmpO3UB0Cz04fgDT9vszfw==", + "version": "11.3.0", + "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-11.3.0.tgz", + "integrity": "sha512-LC6P/uay7rXL5zZ5+5iRJfLs/iUN8apu9tm8YqQVmW3Uq3X4A0dOFUIDuAmB7gAC29wTHOS3EiN/IosNSz0eNQ==", "license": "MIT", "engines": { "node": ">= 16" @@ -1387,7 +1365,6 @@ "version": "0.3.13", "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1398,7 +1375,6 @@ "version": "2.3.5", "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1409,7 +1385,6 @@ "version": "3.1.2", "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1425,7 +1400,6 @@ "version": "0.3.31", "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1433,15 +1407,15 @@ } }, "node_modules/@lezer/common": { - "version": "1.5.0", - "resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.5.0.tgz", - "integrity": "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==", + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.5.1.tgz", + "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", "license": "MIT" }, "node_modules/@lezer/cpp": { - "version": "1.1.3", - "resolved": "https://registry.npmmirror.com/@lezer/cpp/-/cpp-1.1.3.tgz", - "integrity": "sha512-ykYvuFQKGsRi6IcE+/hCSGUhb/I4WPjd3ELhEblm2wS2cOznDFzO+ubK2c+ioysOnlZ3EduV+MVQFCPzAIoY3w==", + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@lezer/cpp/-/cpp-1.1.5.tgz", + "integrity": "sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -1450,14 +1424,14 @@ } }, "node_modules/@lezer/css": { - "version": "1.1.11", - "resolved": "https://registry.npmmirror.com/@lezer/css/-/css-1.1.11.tgz", - "integrity": "sha512-FuAnusbLBl1SEAtfN8NdShxYJiESKw9LAFysfea1T96jD3ydBn12oYjaSG1a04BQRIUd93/0D8e5CV1cUMkmQg==", + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/@lezer/css/-/css-1.3.3.tgz", + "integrity": "sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" + "@lezer/lr": "^1.3.0" } }, "node_modules/@lezer/generator": { @@ -1475,14 +1449,14 @@ } }, "node_modules/@lezer/go": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/@lezer/go/-/go-1.0.0.tgz", - "integrity": "sha512-co9JfT3QqX1YkrMmourYw2Z8meGC50Ko4d54QEcQbEYpvdUvN4yb0NBZdn/9ertgvjsySxHsKzH3lbm3vqJ4Jw==", + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@lezer/go/-/go-1.0.1.tgz", + "integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" + "@lezer/lr": "^1.3.0" } }, "node_modules/@lezer/highlight": { @@ -1495,9 +1469,9 @@ } }, "node_modules/@lezer/html": { - "version": "1.3.12", - "resolved": "https://registry.npmmirror.com/@lezer/html/-/html-1.3.12.tgz", - "integrity": "sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw==", + "version": "1.3.13", + "resolved": "https://registry.npmmirror.com/@lezer/html/-/html-1.3.13.tgz", + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -1517,9 +1491,9 @@ } }, "node_modules/@lezer/javascript": { - "version": "1.5.1", - "resolved": "https://registry.npmmirror.com/@lezer/javascript/-/javascript-1.5.1.tgz", - "integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==", + "version": "1.5.4", + "resolved": "https://registry.npmmirror.com/@lezer/javascript/-/javascript-1.5.4.tgz", + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -1549,28 +1523,28 @@ } }, "node_modules/@lezer/lr": { - "version": "1.4.5", - "resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.5.tgz", - "integrity": "sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==", + "version": "1.4.8", + "resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" } }, "node_modules/@lezer/markdown": { - "version": "1.4.2", - "resolved": "https://registry.npmmirror.com/@lezer/markdown/-/markdown-1.4.2.tgz", - "integrity": "sha512-iYewCigG/517D0xJPQd7RGaCjZAFwROiH8T9h7OTtz0bRVtkxzFhGBFJ9JGKgBBs4uuo1cvxzyQ5iKhDLMcLUQ==", + "version": "1.6.3", + "resolved": "https://registry.npmmirror.com/@lezer/markdown/-/markdown-1.6.3.tgz", + "integrity": "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.0.0", + "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0" } }, "node_modules/@lezer/php": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/@lezer/php/-/php-1.0.2.tgz", - "integrity": "sha512-GN7BnqtGRpFyeoKSEqxvGvhJQiI4zkgmYnDk/JIyc7H7Ifc1tkPnUn/R2R8meH3h/aBf5rzjvU8ZQoyiNDtDrA==", + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/@lezer/php/-/php-1.0.5.tgz", + "integrity": "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -1601,9 +1575,9 @@ } }, "node_modules/@lezer/sass": { - "version": "1.0.7", - "resolved": "https://registry.npmmirror.com/@lezer/sass/-/sass-1.0.7.tgz", - "integrity": "sha512-8HLlOkuX/SMHOggI2DAsXUw38TuURe+3eQ5hiuk9QmYOUyC55B1dYEIMkav5A4IELVaW4e1T4P9WRiI5ka4mdw==", + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@lezer/sass/-/sass-1.1.0.tgz", + "integrity": "sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -1623,9 +1597,9 @@ } }, "node_modules/@lezer/yaml": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/@lezer/yaml/-/yaml-1.0.3.tgz", - "integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==", + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/@lezer/yaml/-/yaml-1.0.4.tgz", + "integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -1640,26 +1614,26 @@ "license": "MIT" }, "node_modules/@mermaid-js/parser": { - "version": "0.6.3", - "resolved": "https://registry.npmmirror.com/@mermaid-js/parser/-/parser-0.6.3.tgz", - "integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==", + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@mermaid-js/parser/-/parser-1.0.1.tgz", + "integrity": "sha512-opmV19kN1JsK0T6HhhokHpcVkqKpF+x2pPDKKM2ThHtZAB5F4PROopk0amuVYK5qMrIA4erzpNm8gmPNJgMDxQ==", "license": "MIT", "dependencies": { - "langium": "3.3.1" + "langium": "^4.0.0" } }, "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.3", "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">= 10.0.0" @@ -1669,25 +1643,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", "cpu": [ "arm64" ], @@ -1705,9 +1679,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", "cpu": [ "arm64" ], @@ -1725,9 +1699,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", "cpu": [ "x64" ], @@ -1745,9 +1719,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", "cpu": [ "x64" ], @@ -1765,9 +1739,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", "cpu": [ "arm" ], @@ -1785,9 +1759,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", "cpu": [ "arm" ], @@ -1805,9 +1779,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", "cpu": [ "arm64" ], @@ -1825,9 +1799,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", "cpu": [ "arm64" ], @@ -1845,9 +1819,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", "cpu": [ "x64" ], @@ -1865,9 +1839,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", "cpu": [ "x64" ], @@ -1885,9 +1859,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", "cpu": [ "arm64" ], @@ -1905,9 +1879,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", "cpu": [ "ia32" ], @@ -1925,9 +1899,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", "cpu": [ "x64" ], @@ -1986,9 +1960,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", - "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", - "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", "dev": true, "license": "MIT" }, @@ -2038,23 +2012,10 @@ } } }, - "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", - "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", "cpu": [ "arm" ], @@ -2066,9 +2027,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", - "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", "cpu": [ "arm64" ], @@ -2080,9 +2041,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", - "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", "cpu": [ "arm64" ], @@ -2094,9 +2055,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", - "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", "cpu": [ "x64" ], @@ -2108,9 +2069,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", - "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", "cpu": [ "arm64" ], @@ -2122,9 +2083,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", - "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", "cpu": [ "x64" ], @@ -2136,9 +2097,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", - "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", "cpu": [ "arm" ], @@ -2150,9 +2111,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", - "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", "cpu": [ "arm" ], @@ -2164,9 +2125,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", - "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", "cpu": [ "arm64" ], @@ -2178,9 +2139,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", - "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", "cpu": [ "arm64" ], @@ -2191,10 +2152,24 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", - "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", "cpu": [ "loong64" ], @@ -2206,9 +2181,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", - "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", "cpu": [ "ppc64" ], @@ -2220,9 +2209,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", - "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", "cpu": [ "riscv64" ], @@ -2234,9 +2223,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", - "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", "cpu": [ "riscv64" ], @@ -2248,9 +2237,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", - "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", "cpu": [ "s390x" ], @@ -2262,9 +2251,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", - "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", "cpu": [ "x64" ], @@ -2276,9 +2265,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", - "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", "cpu": [ "x64" ], @@ -2289,10 +2278,38 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", - "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", "cpu": [ "arm64" ], @@ -2304,9 +2321,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", - "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", "cpu": [ "ia32" ], @@ -2317,10 +2334,24 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", - "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", "cpu": [ "x64" ], @@ -2332,120 +2363,76 @@ ] }, "node_modules/@shikijs/core": { - "version": "3.14.0", - "resolved": "https://registry.npmmirror.com/@shikijs/core/-/core-3.14.0.tgz", - "integrity": "sha512-qRSeuP5vlYHCNUIrpEBQFO7vSkR7jn7Kv+5X3FO/zBKVDGQbcnlScD3XhkrHi/R8Ltz0kEjvFR9Szp/XMRbFMw==", + "version": "3.23.0", + "resolved": "https://registry.npmmirror.com/@shikijs/core/-/core-3.23.0.tgz", + "integrity": "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.14.0", + "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "node_modules/@shikijs/engine-javascript": { - "version": "3.15.0", - "resolved": "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-3.15.0.tgz", - "integrity": "sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.15.0", - "@shikijs/vscode-textmate": "^10.0.2", - "oniguruma-to-es": "^4.3.3" - } - }, - "node_modules/@shikijs/engine-javascript/node_modules/@shikijs/types": { - "version": "3.15.0", - "resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-3.15.0.tgz", - "integrity": "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==", + "version": "3.23.0", + "resolved": "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz", + "integrity": "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==", "dev": true, "license": "MIT", "dependencies": { + "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "oniguruma-to-es": "^4.3.4" } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "3.15.0", - "resolved": "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.15.0.tgz", - "integrity": "sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA==", + "version": "3.23.0", + "resolved": "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", + "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.15.0", + "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, - "node_modules/@shikijs/engine-oniguruma/node_modules/@shikijs/types": { - "version": "3.15.0", - "resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-3.15.0.tgz", - "integrity": "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" - } - }, "node_modules/@shikijs/langs": { - "version": "3.15.0", - "resolved": "https://registry.npmmirror.com/@shikijs/langs/-/langs-3.15.0.tgz", - "integrity": "sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.15.0" - } - }, - "node_modules/@shikijs/langs/node_modules/@shikijs/types": { - "version": "3.15.0", - "resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-3.15.0.tgz", - "integrity": "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==", + "version": "3.23.0", + "resolved": "https://registry.npmmirror.com/@shikijs/langs/-/langs-3.23.0.tgz", + "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@shikijs/types": "3.23.0" } }, "node_modules/@shikijs/themes": { - "version": "3.15.0", - "resolved": "https://registry.npmmirror.com/@shikijs/themes/-/themes-3.15.0.tgz", - "integrity": "sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.15.0" - } - }, - "node_modules/@shikijs/themes/node_modules/@shikijs/types": { - "version": "3.15.0", - "resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-3.15.0.tgz", - "integrity": "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==", + "version": "3.23.0", + "resolved": "https://registry.npmmirror.com/@shikijs/themes/-/themes-3.23.0.tgz", + "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@shikijs/types": "3.23.0" } }, "node_modules/@shikijs/transformers": { - "version": "3.14.0", - "resolved": "https://registry.npmmirror.com/@shikijs/transformers/-/transformers-3.14.0.tgz", - "integrity": "sha512-i67zQnY9wLMMnKasonVW1L9fKneSLZDj1ePsA4o0AZWU4uUobmJY9baRDa36z+a9/g0aG76/2tybQvm4hrwxIQ==", + "version": "3.23.0", + "resolved": "https://registry.npmmirror.com/@shikijs/transformers/-/transformers-3.23.0.tgz", + "integrity": "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/core": "3.14.0", - "@shikijs/types": "3.14.0" + "@shikijs/core": "3.23.0", + "@shikijs/types": "3.23.0" } }, "node_modules/@shikijs/types": { - "version": "3.14.0", - "resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-3.14.0.tgz", - "integrity": "sha512-bQGgC6vrY8U/9ObG1Z/vTro+uclbjjD/uG58RvfxKZVD5p9Yc1ka3tVyEFy7BNJLzxuWyHH5NWynP9zZZS59eQ==", + "version": "3.23.0", + "resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2705,9 +2692,9 @@ "license": "MIT" }, "node_modules/@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "version": "3.1.8", + "resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -2757,6 +2744,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", @@ -2788,9 +2782,9 @@ "license": "MIT" }, "node_modules/@types/katex": { - "version": "0.16.7", - "resolved": "https://registry.npmmirror.com/@types/katex/-/katex-0.16.7.tgz", - "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmmirror.com/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", "license": "MIT" }, "node_modules/@types/linkify-it": { @@ -2829,13 +2823,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "25.5.0", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/trusted-types": { @@ -2859,30 +2853,21 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/whatwg-mimetype": { - "version": "3.0.2", - "resolved": "https://registry.npmmirror.com/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", - "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.51.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz", - "integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==", + "version": "8.57.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.51.0", - "@typescript-eslint/type-utils": "8.51.0", - "@typescript-eslint/utils": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.2.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2892,8 +2877,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.51.0", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.57.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -2908,17 +2893,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.51.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.51.0.tgz", - "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", + "version": "8.57.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.51.0", - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/typescript-estree": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2928,20 +2913,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.51.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.51.0.tgz", - "integrity": "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==", + "version": "8.57.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.51.0", - "@typescript-eslint/types": "^8.51.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2955,14 +2940,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.51.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz", - "integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==", + "version": "8.57.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.0" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2973,9 +2958,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.51.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz", - "integrity": "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==", + "version": "8.57.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", "dev": true, "license": "MIT", "engines": { @@ -2990,17 +2975,17 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.51.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz", - "integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==", + "version": "8.57.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/typescript-estree": "8.51.0", - "@typescript-eslint/utils": "8.51.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.2.0" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3010,14 +2995,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.51.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.51.0.tgz", - "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==", + "version": "8.57.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", "dev": true, "license": "MIT", "engines": { @@ -3029,21 +3014,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.51.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz", - "integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==", + "version": "8.57.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.51.0", - "@typescript-eslint/tsconfig-utils": "8.51.0", - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.0", - "debug": "^4.3.4", - "minimatch": "^9.0.4", - "semver": "^7.6.0", + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.2.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3056,43 +3041,17 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/utils": { - "version": "8.51.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.51.0.tgz", - "integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==", + "version": "8.57.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.51.0", - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/typescript-estree": "8.51.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3102,19 +3061,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.51.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz", - "integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==", + "version": "8.57.2", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.51.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.57.2", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3131,49 +3090,59 @@ "dev": true, "license": "ISC" }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/@vitejs/plugin-vue": { - "version": "6.0.3", - "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz", - "integrity": "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==", + "version": "6.0.5", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz", + "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.53" + "@rolldown/pluginutils": "1.0.0-rc.2" }, "engines": { "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.2.25" } }, "node_modules/@vitest/expect": { - "version": "4.0.16", - "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-4.0.16.tgz", - "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.16", - "@vitest/utils": "4.0.16", - "chai": "^6.2.1", - "tinyrainbow": "^3.0.3" + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "4.0.16", - "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.0.16.tgz", - "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.16", + "@vitest/spy": "4.1.2", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -3182,7 +3151,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -3204,26 +3173,26 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.16", - "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-4.0.16.tgz", - "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.0.16", - "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-4.0.16.tgz", - "integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==", + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.16", + "@vitest/utils": "4.1.2", "pathe": "^2.0.3" }, "funding": { @@ -3231,13 +3200,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.16", - "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-4.0.16.tgz", - "integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==", + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.16", + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -3246,9 +3216,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.16", - "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-4.0.16.tgz", - "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==", + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", "dev": true, "license": "MIT", "funding": { @@ -3256,114 +3226,142 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.16", - "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-4.0.16.tgz", - "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.16", - "tinyrainbow": "^3.0.3" + "@vitest/pretty-format": "4.1.2", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@volar/language-core": { - "version": "2.4.27", - "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.27.tgz", - "integrity": "sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==", + "version": "2.4.28", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", "dev": true, "license": "MIT", "dependencies": { - "@volar/source-map": "2.4.27" + "@volar/source-map": "2.4.28" } }, "node_modules/@volar/source-map": { - "version": "2.4.27", - "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.27.tgz", - "integrity": "sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg==", + "version": "2.4.28", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", "dev": true, "license": "MIT" }, "node_modules/@volar/typescript": { - "version": "2.4.27", - "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.27.tgz", - "integrity": "sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==", + "version": "2.4.28", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.27", + "@volar/language-core": "2.4.28", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, - "node_modules/@vue/compiler-core": { - "version": "3.5.26", - "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.26.tgz", - "integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==", + "node_modules/@vue-macros/common": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@vue-macros/common/-/common-3.1.2.tgz", + "integrity": "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/shared": "3.5.26", - "entities": "^7.0.0", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.26", - "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz", - "integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==", - "license": "MIT", + "@vue/compiler-sfc": "^3.5.22", + "ast-kit": "^2.1.2", + "local-pkg": "^1.1.2", + "magic-string-ast": "^1.0.2", + "unplugin-utils": "^0.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/vue-macros" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.2.25" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.31", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.31.tgz", + "integrity": "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==", + "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.26", - "@vue/shared": "3.5.26" + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.31", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.31", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz", + "integrity": "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.31", + "@vue/shared": "3.5.31" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.26", - "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz", - "integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==", + "version": "3.5.31", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.31.tgz", + "integrity": "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/compiler-core": "3.5.26", - "@vue/compiler-dom": "3.5.26", - "@vue/compiler-ssr": "3.5.26", - "@vue/shared": "3.5.26", + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.31", + "@vue/compiler-dom": "3.5.31", + "@vue/compiler-ssr": "3.5.31", + "@vue/shared": "3.5.31", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.6", + "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.26", - "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz", - "integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==", + "version": "3.5.31", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.31.tgz", + "integrity": "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.26", - "@vue/shared": "3.5.26" + "@vue/compiler-dom": "3.5.31", + "@vue/shared": "3.5.31" } }, "node_modules/@vue/devtools-api": { - "version": "7.7.8", - "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.8.tgz", - "integrity": "sha512-BtFcAmDbtXGwurWUFf8ogIbgZyR+rcVES1TSNEI8Em80fD8Anu+qTRN1Fc3J6vdRHlVM3fzPV1qIo+B4AiqGzw==", + "version": "7.7.9", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", "license": "MIT", "dependencies": { - "@vue/devtools-kit": "^7.7.8" + "@vue/devtools-kit": "^7.7.9" } }, "node_modules/@vue/devtools-kit": { - "version": "7.7.8", - "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.8.tgz", - "integrity": "sha512-4Y8op+AoxOJhB9fpcEF6d5vcJXWKgHxC3B0ytUB8zz15KbP9g9WgVzral05xluxi2fOeAy6t140rdQ943GcLRQ==", + "version": "7.7.9", + "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", "license": "MIT", "dependencies": { - "@vue/devtools-shared": "^7.7.8", + "@vue/devtools-shared": "^7.7.9", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", @@ -3373,22 +3371,22 @@ } }, "node_modules/@vue/devtools-shared": { - "version": "7.7.8", - "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.8.tgz", - "integrity": "sha512-XHpO3jC5nOgYr40M9p8Z4mmKfTvUxKyRcUnpBAYg11pE78eaRFBKb0kG5yKLroMuJeeNH9LWmKp2zMU5LUc7CA==", + "version": "7.7.9", + "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", "license": "MIT", "dependencies": { "rfdc": "^1.4.1" } }, "node_modules/@vue/language-core": { - "version": "3.2.1", - "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.2.1.tgz", - "integrity": "sha512-g6oSenpnGMtpxHGAwKuu7HJJkNZpemK/zg3vZzZbJ6cnnXq1ssxuNrXSsAHYM3NvH8p4IkTw+NLmuxyeYz4r8A==", + "version": "3.2.6", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.2.6.tgz", + "integrity": "sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.27", + "@volar/language-core": "2.4.28", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.0.0", @@ -3397,79 +3395,66 @@ "picomatch": "^4.0.2" } }, - "node_modules/@vue/language-core/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@vue/reactivity": { - "version": "3.5.26", - "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.26.tgz", - "integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==", + "version": "3.5.31", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.31.tgz", + "integrity": "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.26" + "@vue/shared": "3.5.31" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.26", - "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.26.tgz", - "integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==", + "version": "3.5.31", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.31.tgz", + "integrity": "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.26", - "@vue/shared": "3.5.26" + "@vue/reactivity": "3.5.31", + "@vue/shared": "3.5.31" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.26", - "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz", - "integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==", + "version": "3.5.31", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.31.tgz", + "integrity": "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.26", - "@vue/runtime-core": "3.5.26", - "@vue/shared": "3.5.26", + "@vue/reactivity": "3.5.31", + "@vue/runtime-core": "3.5.31", + "@vue/shared": "3.5.31", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.26", - "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.26.tgz", - "integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==", + "version": "3.5.31", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.31.tgz", + "integrity": "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.26", - "@vue/shared": "3.5.26" + "@vue/compiler-ssr": "3.5.31", + "@vue/shared": "3.5.31" }, "peerDependencies": { - "vue": "3.5.26" + "vue": "3.5.31" } }, "node_modules/@vue/shared": { - "version": "3.5.26", - "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.26.tgz", - "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==", + "version": "3.5.31", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.31.tgz", + "integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==", "license": "MIT" }, "node_modules/@vueuse/core": { - "version": "13.9.0", - "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-13.9.0.tgz", - "integrity": "sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==", + "version": "14.2.1", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-14.2.1.tgz", + "integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==", "dev": true, "license": "MIT", "dependencies": { "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "13.9.0", - "@vueuse/shared": "13.9.0" + "@vueuse/metadata": "14.2.1", + "@vueuse/shared": "14.2.1" }, "funding": { "url": "https://github.com/sponsors/antfu" @@ -3479,14 +3464,14 @@ } }, "node_modules/@vueuse/integrations": { - "version": "13.9.0", - "resolved": "https://registry.npmmirror.com/@vueuse/integrations/-/integrations-13.9.0.tgz", - "integrity": "sha512-SDobKBbPIOe0cVL7QxMzGkuUGHvWTdihi9zOrrWaWUgFKe15cwEcwfWmgrcNzjT6kHnNmWuTajPHoIzUjYNYYQ==", + "version": "14.2.1", + "resolved": "https://registry.npmmirror.com/@vueuse/integrations/-/integrations-14.2.1.tgz", + "integrity": "sha512-2LIUpBi/67PoXJGqSDQUF0pgQWpNHh7beiA+KG2AbybcNm+pTGWT6oPGlBgUoDWmYwfeQqM/uzOHqcILpKL7nA==", "dev": true, "license": "MIT", "dependencies": { - "@vueuse/core": "13.9.0", - "@vueuse/shared": "13.9.0" + "@vueuse/core": "14.2.1", + "@vueuse/shared": "14.2.1" }, "funding": { "url": "https://github.com/sponsors/antfu" @@ -3496,7 +3481,7 @@ "axios": "^1", "change-case": "^5", "drauu": "^0.4", - "focus-trap": "^7", + "focus-trap": "^7 || ^8", "fuse.js": "^7", "idb-keyval": "^6", "jwt-decode": "^4", @@ -3546,9 +3531,9 @@ } }, "node_modules/@vueuse/metadata": { - "version": "13.9.0", - "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-13.9.0.tgz", - "integrity": "sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==", + "version": "14.2.1", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-14.2.1.tgz", + "integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==", "dev": true, "license": "MIT", "funding": { @@ -3556,9 +3541,9 @@ } }, "node_modules/@vueuse/shared": { - "version": "13.9.0", - "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-13.9.0.tgz", - "integrity": "sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==", + "version": "14.2.1", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-14.2.1.tgz", + "integrity": "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==", "dev": true, "license": "MIT", "funding": { @@ -3569,9 +3554,9 @@ } }, "node_modules/@wailsio/runtime": { - "version": "3.0.0-alpha.77", - "resolved": "https://registry.npmmirror.com/@wailsio/runtime/-/runtime-3.0.0-alpha.77.tgz", - "integrity": "sha512-DMWjT8VFCk8O818mnw2dbrgZilOf1TzmGGp5lemZyGej7g+SSqAhMFOHp9eCiGQ32EbxmGOdTO4aNZVA00j9Nw==", + "version": "3.0.0-alpha.79", + "resolved": "https://registry.npmmirror.com/@wailsio/runtime/-/runtime-3.0.0-alpha.79.tgz", + "integrity": "sha512-NITzxKmJsMEruc39L166lbPJVECxzcbdqpHVqOOF7Cu/7Zqk/e3B/gNpkUjhNyo5rVb3V1wpS8oEgLUmpu1cwA==", "dev": true, "license": "MIT" }, @@ -3594,15 +3579,18 @@ } }, "node_modules/@zumer/snapdom": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/@zumer/snapdom/-/snapdom-2.0.1.tgz", - "integrity": "sha512-78/qbYl2FTv4H6qaXcNfAujfIOSzdvs83NW63VbyC9QA3sqNPfPvhn4xYMO6Gy11hXwJUEhd0z65yKiNzDwy9w==", - "license": "MIT" + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/@zumer/snapdom/-/snapdom-2.7.0.tgz", + "integrity": "sha512-ZiELKzDszeFOazPQ/ExXzgtdoW9jADVjDjInr5XDAlVdCx0RbNsFiG7RLyM48XnA7EyCA9yTvmXSc3ElDrTRqA==", + "license": "MIT", + "workspaces": [ + "packages/*" + ] }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3622,9 +3610,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -3639,35 +3627,12 @@ } }, "node_modules/alien-signals": { - "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-3.1.1.tgz", - "integrity": "sha512-ogkIWbVrLwKtHY6oOAXaYkAxP+cTH7V5FZ5+Tm4NZFd8VDZ6uNMDrfzqctTZ42eTMCSR3ne3otpcxmqSnFfPYA==", + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-3.1.2.tgz", + "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==", "dev": true, "license": "MIT" }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, "node_modules/asn1.js": { "version": "4.10.1", "resolved": "https://registry.npmmirror.com/asn1.js/-/asn1.js-4.10.1.tgz", @@ -3681,9 +3646,9 @@ } }, "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -3711,6 +3676,38 @@ "node": ">=12" } }, + "node_modules/ast-kit": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/ast-kit/-/ast-kit-2.2.0.tgz", + "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/ast-walker-scope": { + "version": "0.8.3", + "resolved": "https://registry.npmmirror.com/ast-walker-scope/-/ast-walker-scope-0.8.3.tgz", + "integrity": "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.4", + "ast-kit": "^2.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3728,11 +3725,14 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/base64-js": { "version": "1.5.1", @@ -3756,18 +3756,18 @@ "license": "MIT" }, "node_modules/birpc": { - "version": "2.6.1", - "resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.6.1.tgz", - "integrity": "sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==", + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/bn.js": { - "version": "5.2.2", - "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-5.2.2.tgz", - "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "version": "5.2.3", + "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", "dev": true, "license": "MIT" }, @@ -3779,27 +3779,16 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "5.0.5", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "optional": true, - "dependencies": { - "fill-range": "^7.1.1" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" } }, "node_modules/brorand": { @@ -3875,25 +3864,24 @@ } }, "node_modules/browserify-sign": { - "version": "4.2.3", - "resolved": "https://registry.npmmirror.com/browserify-sign/-/browserify-sign-4.2.3.tgz", - "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/browserify-sign/-/browserify-sign-4.2.5.tgz", + "integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==", "dev": true, "license": "ISC", "dependencies": { - "bn.js": "^5.2.1", - "browserify-rsa": "^4.1.0", + "bn.js": "^5.2.2", + "browserify-rsa": "^4.1.1", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.5", - "hash-base": "~3.0", + "elliptic": "^6.6.1", "inherits": "^2.0.4", - "parse-asn1": "^5.1.7", + "parse-asn1": "^5.1.9", "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1" }, "engines": { - "node": ">= 0.12" + "node": ">= 0.10" } }, "node_modules/browserify-sign/node_modules/isarray": { @@ -3926,23 +3914,6 @@ "dev": true, "license": "MIT" }, - "node_modules/browserify-sign/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, "node_modules/browserify-zlib": { "version": "0.2.0", "resolved": "https://registry.npmmirror.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz", @@ -4042,16 +4013,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/ccount/-/ccount-2.0.1.tgz", @@ -4064,32 +4025,15 @@ } }, "node_modules/chai": { - "version": "6.2.1", - "resolved": "https://registry.npmmirror.com/chai/-/chai-6.2.1.tgz", - "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { "node": ">=18" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/character-entities-html4": { "version": "2.1.0", "resolved": "https://registry.npmmirror.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz", @@ -4113,17 +4057,17 @@ } }, "node_modules/chevrotain": { - "version": "11.0.3", - "resolved": "https://registry.npmmirror.com/chevrotain/-/chevrotain-11.0.3.tgz", - "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "version": "11.2.0", + "resolved": "https://registry.npmmirror.com/chevrotain/-/chevrotain-11.2.0.tgz", + "integrity": "sha512-mHCHTxM51nCklUw9RzRVc0DLjAh/SAUPM4k/zMInlTIo25ldWXOZoPt7XEIk/LwoT4lFVmJcu9g5MHtx371x3A==", "license": "Apache-2.0", "dependencies": { - "@chevrotain/cst-dts-gen": "11.0.3", - "@chevrotain/gast": "11.0.3", - "@chevrotain/regexp-to-ast": "11.0.3", - "@chevrotain/types": "11.0.3", - "@chevrotain/utils": "11.0.3", - "lodash-es": "4.17.21" + "@chevrotain/cst-dts-gen": "11.2.0", + "@chevrotain/gast": "11.2.0", + "@chevrotain/regexp-to-ast": "11.2.0", + "@chevrotain/types": "11.2.0", + "@chevrotain/utils": "11.2.0", + "lodash-es": "4.17.23" } }, "node_modules/chevrotain-allstar": { @@ -4154,14 +4098,15 @@ } }, "node_modules/cipher-base": { - "version": "1.0.6", - "resolved": "https://registry.npmmirror.com/cipher-base/-/cipher-base-1.0.6.tgz", - "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.4", - "safe-buffer": "^5.2.1" + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" }, "engines": { "node": ">= 0.10" @@ -4183,38 +4128,19 @@ } }, "node_modules/codemirror-lang-elixir": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/codemirror-lang-elixir/-/codemirror-lang-elixir-4.0.0.tgz", - "integrity": "sha512-mzFesxo/t6KOxwnkqVd34R/q7yk+sMtHh6vUKGAvjwHmpL7bERHB+vQAsmU/nqrndkwVeJEHWGw/z/ybfdiudA==", + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/codemirror-lang-elixir/-/codemirror-lang-elixir-4.0.1.tgz", + "integrity": "sha512-z6W/XB4b7TZrp9EZYBGVq93vQfvKbff+1iM8YZaVErL0dguBAeLmVRlEv1NuDZHOP1qjJ3NwyibkUkNWn7q9VQ==", + "license": "Apache-2.0", "dependencies": { "@codemirror/language": "^6.0.0", "lezer-elixir": "^1.0.0" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/colors-named": { - "version": "1.0.4", - "resolved": "https://registry.npmmirror.com/colors-named/-/colors-named-1.0.4.tgz", - "integrity": "sha512-R254qrKSxFJNa7QmM7vrRaz5Hygr7MIaNbXcIx7WfmlYJ9OjZQ+aczGlnKS8lLtNT0GM9aGZ4EcmNXrh5ttv6g==", + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/colors-named/-/colors-named-1.0.5.tgz", + "integrity": "sha512-xaspf9oddAOqP2LYNOgp8E3BwAzugrdO9J1kDNS5ySrzTgV9hrXGBt5w87ioLEr2pM4Ukt+GKedvzaLRxpv8pA==", "license": "MIT", "engines": { "node": ">=14.16" @@ -4224,9 +4150,9 @@ } }, "node_modules/colors-named-hex": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/colors-named-hex/-/colors-named-hex-1.0.3.tgz", - "integrity": "sha512-vhUoMdCdOKgD9Ni3p6uV3ET1JJCHzlcK6lN3/yl+6TUHinDE6HUFlmnvkh/NDZ2M9049Ipn3mX85qu6akRiC1g==", + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/colors-named-hex/-/colors-named-hex-1.0.4.tgz", + "integrity": "sha512-X+Enw/2fFAgDRhUac69cRO/RJvHnWDBBrP8J1sJuEU16Buiiu8PPpJP4abSo0V+fJbkfwmQITE6zKx/SBJERGw==", "license": "MIT", "engines": { "node": ">=14.16" @@ -4255,17 +4181,10 @@ "node": ">= 12" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "license": "MIT" }, "node_modules/console-browserify": { @@ -4281,16 +4200,23 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/copy-anything": { - "version": "3.0.5", - "resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-3.0.5.tgz", - "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", "license": "MIT", "dependencies": { - "is-what": "^4.1.8" + "is-what": "^5.2.0" }, "engines": { - "node": ">=12.13" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/mesqueeb" @@ -4324,9 +4250,9 @@ } }, "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -4714,9 +4640,9 @@ } }, "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", "license": "ISC", "engines": { "node": ">=12" @@ -4825,12 +4751,6 @@ "d3-path": "1" } }, - "node_modules/d3-sankey/node_modules/internmap": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/internmap/-/internmap-1.0.1.tgz", - "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", - "license": "ISC" - }, "node_modules/d3-scale": { "version": "4.0.2", "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz", @@ -4950,9 +4870,9 @@ } }, "node_modules/dagre-d3-es": { - "version": "7.0.13", - "resolved": "https://registry.npmmirror.com/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz", - "integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==", + "version": "7.0.14", + "resolved": "https://registry.npmmirror.com/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", "license": "MIT", "dependencies": { "d3": "^7.9.0", @@ -4960,15 +4880,16 @@ } }, "node_modules/dayjs": { - "version": "1.11.19", - "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "version": "1.11.20", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5032,9 +4953,9 @@ "license": "MIT" }, "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", "license": "ISC", "dependencies": { "robust-predicates": "^3.0.2" @@ -5062,16 +4983,13 @@ } }, "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, "engines": { - "node": ">=0.10" + "node": ">=8" } }, "node_modules/devlop": { @@ -5101,9 +5019,9 @@ } }, "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -5121,9 +5039,9 @@ } }, "node_modules/dompurify": { - "version": "3.3.0", - "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.3.0.tgz", - "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.3.3.tgz", + "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -5161,16 +5079,16 @@ } }, "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, "node_modules/entities": { - "version": "7.0.0", - "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.0.tgz", - "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -5200,9 +5118,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, "license": "MIT" }, @@ -5220,9 +5138,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.2", - "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.2.tgz", - "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5233,31 +5151,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.2", - "@esbuild/android-arm": "0.25.2", - "@esbuild/android-arm64": "0.25.2", - "@esbuild/android-x64": "0.25.2", - "@esbuild/darwin-arm64": "0.25.2", - "@esbuild/darwin-x64": "0.25.2", - "@esbuild/freebsd-arm64": "0.25.2", - "@esbuild/freebsd-x64": "0.25.2", - "@esbuild/linux-arm": "0.25.2", - "@esbuild/linux-arm64": "0.25.2", - "@esbuild/linux-ia32": "0.25.2", - "@esbuild/linux-loong64": "0.25.2", - "@esbuild/linux-mips64el": "0.25.2", - "@esbuild/linux-ppc64": "0.25.2", - "@esbuild/linux-riscv64": "0.25.2", - "@esbuild/linux-s390x": "0.25.2", - "@esbuild/linux-x64": "0.25.2", - "@esbuild/netbsd-arm64": "0.25.2", - "@esbuild/netbsd-x64": "0.25.2", - "@esbuild/openbsd-arm64": "0.25.2", - "@esbuild/openbsd-x64": "0.25.2", - "@esbuild/sunos-x64": "0.25.2", - "@esbuild/win32-arm64": "0.25.2", - "@esbuild/win32-ia32": "0.25.2", - "@esbuild/win32-x64": "0.25.2" + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" } }, "node_modules/escape-string-regexp": { @@ -5274,33 +5193,30 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-10.1.0.tgz", + "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.3", + "@eslint/config-helpers": "^0.5.3", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", + "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -5310,8 +5226,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -5319,7 +5234,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" @@ -5334,9 +5249,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "10.6.2", - "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-10.6.2.tgz", - "integrity": "sha512-nA5yUs/B1KmKzvC42fyD0+l9Yd+LtEpVhWRbXuDj0e+ZURcTtyRbMDWUeJmTAh2wC6jC83raS63anNM2YT3NPw==", + "version": "10.8.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-10.8.0.tgz", + "integrity": "sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA==", "dev": true, "license": "MIT", "dependencies": { @@ -5353,7 +5268,7 @@ "peerDependencies": { "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "vue-eslint-parser": "^10.0.0" }, "peerDependenciesMeta": { @@ -5366,57 +5281,59 @@ } }, "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "version": "9.1.2", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmmirror.com/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "version": "11.2.0", + "resolved": "https://registry.npmmirror.com/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5487,9 +5404,9 @@ } }, "node_modules/expect-type": { - "version": "1.2.2", - "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.2.2.tgz", - "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5497,9 +5414,9 @@ } }, "node_modules/exsolve": { - "version": "1.0.7", - "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.7.tgz", - "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", "license": "MIT" }, "node_modules/fast-deep-equal": { @@ -5523,6 +5440,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -5536,19 +5470,6 @@ "node": ">=16.0.0" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "optional": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", @@ -5581,20 +5502,20 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, "node_modules/focus-trap": { - "version": "7.6.6", - "resolved": "https://registry.npmmirror.com/focus-trap/-/focus-trap-7.6.6.tgz", - "integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==", + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/focus-trap/-/focus-trap-8.0.1.tgz", + "integrity": "sha512-9ptSG6z51YQOstI/oN4XuVGP/03u2nh0g//qz7L6zX0i6PZiPnkcf3GenXq7N2hZnASXaMxTPpbKwdI+PFvxlw==", "dev": true, "license": "MIT", "dependencies": { - "tabbable": "^6.3.0" + "tabbable": "^6.4.0" } }, "node_modules/for-each": { @@ -5638,6 +5559,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -5691,9 +5622,9 @@ } }, "node_modules/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmmirror.com/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "version": "17.4.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", "dev": true, "license": "MIT", "engines": { @@ -5728,71 +5659,23 @@ "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", "license": "MIT" }, - "node_modules/happy-dom": { - "version": "20.0.11", - "resolved": "https://registry.npmmirror.com/happy-dom/-/happy-dom-20.0.11.tgz", - "integrity": "sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g==", + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "@types/node": "^20.0.0", - "@types/whatwg-mimetype": "^3.0.2", - "whatwg-mimetype": "^3.0.0" + "es-define-property": "^1.0.0" }, - "engines": { - "node": ">=20.0.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/happy-dom/node_modules/@types/node": { - "version": "20.19.25", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.25.tgz", - "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/happy-dom/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -5986,28 +5869,11 @@ } }, "node_modules/immutable": { - "version": "5.1.1", - "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.1.tgz", - "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==", + "version": "5.1.5", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", "license": "MIT" }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -6026,13 +5892,10 @@ "license": "ISC" }, "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" }, "node_modules/is-arguments": { "version": "1.2.0", @@ -6091,14 +5954,15 @@ } }, "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -6139,16 +6003,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", @@ -6185,12 +6039,12 @@ } }, "node_modules/is-what": { - "version": "4.1.16", - "resolved": "https://registry.npmmirror.com/is-what/-/is-what-4.1.16.tgz", - "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", "license": "MIT", "engines": { - "node": ">=12.13" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/mesqueeb" @@ -6220,40 +6074,16 @@ "node": ">=10" } }, - "node_modules/java-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmmirror.com/java-parser/-/java-parser-3.0.1.tgz", - "integrity": "sha512-sDIR7u9b7O2JViNUxiZRhnRz7URII/eE7g2B+BmGxDeS6Ex3OYAcCyz5oh0H4LQ+hL/BS8OJTz8apMy9xtGmrQ==", - "license": "Apache-2.0", - "dependencies": { - "chevrotain": "11.0.3", - "chevrotain-allstar": "0.3.1", - "lodash": "4.17.21" - } - }, - "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "dev": true, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "license": "MIT", - "optional": true, - "peer": true, "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" + "jsesc": "bin/jsesc" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=6" } }, "node_modules/json-buffer": { @@ -6277,10 +6107,22 @@ "dev": true, "license": "MIT" }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/katex": { - "version": "0.16.27", - "resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.27.tgz", - "integrity": "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==", + "version": "0.16.44", + "resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.44.tgz", + "integrity": "sha512-EkxoDTk8ufHqHlf9QxGwcxeLkWRR3iOuYfRpfORgYfqc8s13bgb+YtRY59NK5ZpRaCwq1kqA6a5lpX8C/eLphQ==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -6308,33 +6150,75 @@ "resolved": "https://registry.npmmirror.com/khroma/-/khroma-2.1.0.tgz", "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" }, - "node_modules/kolorist": { - "version": "1.8.0", - "resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz", - "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", - "license": "MIT" - }, "node_modules/langium": { - "version": "3.3.1", - "resolved": "https://registry.npmmirror.com/langium/-/langium-3.3.1.tgz", - "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/langium/-/langium-4.2.1.tgz", + "integrity": "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==", "license": "MIT", "dependencies": { - "chevrotain": "~11.0.3", - "chevrotain-allstar": "~0.3.0", + "chevrotain": "~11.1.1", + "chevrotain-allstar": "~0.3.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", - "vscode-uri": "~3.0.8" + "vscode-uri": "~3.1.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.10.0", + "npm": ">=10.2.3" } }, - "node_modules/langium/node_modules/vscode-uri": { - "version": "3.0.8", - "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.0.8.tgz", - "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", - "license": "MIT" + "node_modules/langium/node_modules/@chevrotain/cst-dts-gen": { + "version": "11.1.2", + "resolved": "https://registry.npmmirror.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.2.tgz", + "integrity": "sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.1.2", + "@chevrotain/types": "11.1.2", + "lodash-es": "4.17.23" + } + }, + "node_modules/langium/node_modules/@chevrotain/gast": { + "version": "11.1.2", + "resolved": "https://registry.npmmirror.com/@chevrotain/gast/-/gast-11.1.2.tgz", + "integrity": "sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.1.2", + "lodash-es": "4.17.23" + } + }, + "node_modules/langium/node_modules/@chevrotain/regexp-to-ast": { + "version": "11.1.2", + "resolved": "https://registry.npmmirror.com/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.1.2.tgz", + "integrity": "sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw==", + "license": "Apache-2.0" + }, + "node_modules/langium/node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmmirror.com/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "license": "Apache-2.0" + }, + "node_modules/langium/node_modules/@chevrotain/utils": { + "version": "11.1.2", + "resolved": "https://registry.npmmirror.com/@chevrotain/utils/-/utils-11.1.2.tgz", + "integrity": "sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA==", + "license": "Apache-2.0" + }, + "node_modules/langium/node_modules/chevrotain": { + "version": "11.1.2", + "resolved": "https://registry.npmmirror.com/chevrotain/-/chevrotain-11.1.2.tgz", + "integrity": "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.1.2", + "@chevrotain/gast": "11.1.2", + "@chevrotain/regexp-to-ast": "11.1.2", + "@chevrotain/types": "11.1.2", + "@chevrotain/utils": "11.1.2", + "lodash-es": "4.17.23" + } }, "node_modules/layout-base": { "version": "1.0.2", @@ -6357,18 +6241,19 @@ } }, "node_modules/lezer-elixir": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/lezer-elixir/-/lezer-elixir-1.1.2.tgz", - "integrity": "sha512-K3yPMJcNhqCL6ugr5NkgOC1g37rcOM38XZezO9lBXy0LwWFd8zdWXfmRbY829vZVk0OGCQoI02yDWp9FF2OWZA==", + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/lezer-elixir/-/lezer-elixir-1.1.3.tgz", + "integrity": "sha512-Ymc58/WhxdZS9yEOlnKbF3rdeBdFcPm4OEm26KMqA1Za9vztXi7I5qwGw1KxYmm3Nv0iDHq//EQyBwSEzKG9Mg==", + "license": "Apache-2.0", "dependencies": { "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.3.0" } }, "node_modules/linguist-languages": { - "version": "9.1.11", - "resolved": "https://registry.npmmirror.com/linguist-languages/-/linguist-languages-9.1.11.tgz", - "integrity": "sha512-z7olfPDVscn9cKsf5DF7FGLy3FLMnLhg1ZtkXfuJsXobW+LYQxP6XTyASF2vdYTrQGr1G+cqzFaa3NeXZcGIVA==", + "version": "9.3.1", + "resolved": "https://registry.npmmirror.com/linguist-languages/-/linguist-languages-9.3.1.tgz", + "integrity": "sha512-Mum2sqg3MyhgKfpulFhKZMAK/1VnV6m9vCV8YQCSqWs+pbKouKn9EqRshZjVWUaJjl6NTTDcYJk/1+C02siXEQ==", "license": "MIT" }, "node_modules/local-pkg": { @@ -6388,6 +6273,23 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/local-pkg/node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/local-pkg/node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", @@ -6404,23 +6306,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, + "version": "4.17.23", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "license": "MIT" }, "node_modules/magic-string": { @@ -6432,6 +6321,21 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magic-string-ast": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/magic-string-ast/-/magic-string-ast-1.0.3.tgz", + "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==", + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.19" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/mark.js": { "version": "8.11.1", "resolved": "https://registry.npmmirror.com/mark.js/-/mark.js-8.11.1.tgz", @@ -6440,9 +6344,9 @@ "license": "MIT" }, "node_modules/marked": { - "version": "17.0.1", - "resolved": "https://registry.npmmirror.com/marked/-/marked-17.0.1.tgz", - "integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==", + "version": "17.0.5", + "resolved": "https://registry.npmmirror.com/marked/-/marked-17.0.5.tgz", + "integrity": "sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -6475,7 +6379,7 @@ }, "node_modules/mdast-util-to-hast": { "version": "13.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "resolved": "https://registry.npmmirror.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "dev": true, "license": "MIT", @@ -6496,27 +6400,28 @@ } }, "node_modules/mermaid": { - "version": "11.12.2", - "resolved": "https://registry.npmmirror.com/mermaid/-/mermaid-11.12.2.tgz", - "integrity": "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==", + "version": "11.13.0", + "resolved": "https://registry.npmmirror.com/mermaid/-/mermaid-11.13.0.tgz", + "integrity": "sha512-fEnci+Immw6lKMFI8sqzjlATTyjLkRa6axrEgLV2yHTfv8r+h1wjFbV6xeRtd4rUV1cS4EpR9rwp3Rci7TRWDw==", "license": "MIT", "dependencies": { "@braintree/sanitize-url": "^7.1.1", - "@iconify/utils": "^3.0.1", - "@mermaid-js/parser": "^0.6.3", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.0.1", "@types/d3": "^7.4.3", - "cytoscape": "^3.29.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", - "dagre-d3-es": "7.0.13", - "dayjs": "^1.11.18", - "dompurify": "^3.2.5", - "katex": "^0.16.22", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.19", + "dompurify": "^3.3.1", + "katex": "^0.16.25", "khroma": "^2.1.0", - "lodash-es": "^4.17.21", - "marked": "^16.2.1", + "lodash-es": "^4.17.23", + "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", @@ -6629,20 +6534,6 @@ ], "license": "MIT" }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "optional": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/miller-rabin": { "version": "4.0.1", "resolved": "https://registry.npmmirror.com/miller-rabin/-/miller-rabin-4.0.1.tgz", @@ -6658,9 +6549,9 @@ } }, "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -6679,16 +6570,19 @@ "license": "MIT" }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "10.2.4", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minisearch": { @@ -6705,45 +6599,28 @@ "license": "MIT" }, "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "version": "1.8.2", + "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", "license": "MIT", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", - "ufo": "^1.6.1" - } - }, - "node_modules/mlly/node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "license": "MIT" - }, - "node_modules/mlly/node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" + "ufo": "^1.6.3" } }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/muggle-string": { "version": "0.4.1", "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -6917,14 +6794,14 @@ "license": "MIT" }, "node_modules/oniguruma-to-es": { - "version": "4.3.3", - "resolved": "https://registry.npmmirror.com/oniguruma-to-es/-/oniguruma-to-es-4.3.3.tgz", - "integrity": "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==", + "version": "4.3.5", + "resolved": "https://registry.npmmirror.com/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz", + "integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==", "dev": true, "license": "MIT", "dependencies": { "oniguruma-parser": "^0.12.1", - "regex": "^6.0.1", + "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, @@ -6986,9 +6863,9 @@ } }, "node_modules/package-manager-detector": { - "version": "1.5.0", - "resolved": "https://registry.npmmirror.com/package-manager-detector/-/package-manager-detector-1.5.0.tgz", - "integrity": "sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==", + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", "license": "MIT" }, "node_modules/pako": { @@ -6998,31 +6875,17 @@ "dev": true, "license": "(MIT AND Zlib)" }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/parse-asn1": { - "version": "5.1.7", - "resolved": "https://registry.npmmirror.com/parse-asn1/-/parse-asn1-5.1.7.tgz", - "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", + "version": "5.1.9", + "resolved": "https://registry.npmmirror.com/parse-asn1/-/parse-asn1-5.1.9.tgz", + "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", "dev": true, "license": "ISC", "dependencies": { "asn1.js": "^4.10.1", "browserify-aes": "^1.2.0", "evp_bytestokey": "^1.0.3", - "hash-base": "~3.0", - "pbkdf2": "^3.1.2", + "pbkdf2": "^3.1.5", "safe-buffer": "^5.2.1" }, "engines": { @@ -7076,55 +6939,21 @@ "license": "MIT" }, "node_modules/pbkdf2": { - "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/pbkdf2/-/pbkdf2-3.1.3.tgz", - "integrity": "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA==", + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/pbkdf2/-/pbkdf2-3.1.5.tgz", + "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", "dev": true, "license": "MIT", "dependencies": { - "create-hash": "~1.1.3", + "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "ripemd160": "=2.0.1", + "ripemd160": "^2.0.3", "safe-buffer": "^5.2.1", - "sha.js": "^2.4.11", - "to-buffer": "^1.2.0" + "sha.js": "^2.4.12", + "to-buffer": "^1.2.1" }, "engines": { - "node": ">=0.12" - } - }, - "node_modules/pbkdf2/node_modules/create-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmmirror.com/create-hash/-/create-hash-1.1.3.tgz", - "integrity": "sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "sha.js": "^2.4.0" - } - }, - "node_modules/pbkdf2/node_modules/hash-base": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/hash-base/-/hash-base-2.0.2.tgz", - "integrity": "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1" - } - }, - "node_modules/pbkdf2/node_modules/ripemd160": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/ripemd160/-/ripemd160-2.0.1.tgz", - "integrity": "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hash-base": "^2.0.0", - "inherits": "^2.0.1" + "node": ">= 0.10" } }, "node_modules/perfect-debounce": { @@ -7134,9 +6963,9 @@ "license": "MIT" }, "node_modules/php-parser": { - "version": "3.2.5", - "resolved": "https://registry.npmmirror.com/php-parser/-/php-parser-3.2.5.tgz", - "integrity": "sha512-M1ZYlALFFnESbSdmRtTQrBFUHSriHgPhgqtTF/LCbZM4h7swR5PHtUceB2Kzby5CfqcsYwBn7OXTJ0+8Sajwkw==", + "version": "3.5.0", + "resolved": "https://registry.npmmirror.com/php-parser/-/php-parser-3.5.0.tgz", + "integrity": "sha512-EHdzSckQNP86jQRCEsMYhs+YzS4BfvfxnyhvzHVhVRoRUGEMFi8f3xKfuS9xdChBazZSyvb10SZbqhYQLGBcQg==", "license": "BSD-3-Clause" }, "node_modules/picocolors": { @@ -7146,13 +6975,12 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", - "optional": true, "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -7218,14 +7046,14 @@ } }, "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", "license": "MIT", "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" } }, "node_modules/points-on-curve": { @@ -7255,9 +7083,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "funding": [ { "type": "opencollective", @@ -7283,9 +7111,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, "license": "MIT", "dependencies": { @@ -7307,9 +7135,9 @@ } }, "node_modules/prettier": { - "version": "3.7.4", - "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.7.4.tgz", - "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "version": "3.8.1", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -7365,9 +7193,9 @@ } }, "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT" }, @@ -7382,9 +7210,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.15.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -7472,9 +7300,9 @@ } }, "node_modules/regex": { - "version": "6.0.1", - "resolved": "https://registry.npmmirror.com/regex/-/regex-6.0.1.tgz", - "integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==", + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", "dev": true, "license": "MIT", "dependencies": { @@ -7505,13 +7333,13 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.11", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -7525,16 +7353,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", @@ -7542,59 +7360,113 @@ "license": "MIT" }, "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", "dev": true, "license": "MIT", "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" } }, - "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", - "license": "Unlicense" - }, - "node_modules/rollup": { - "version": "4.46.2", - "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.46.2.tgz", - "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", + "node_modules/ripemd160/node_modules/hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ripemd160/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ripemd160/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/ripemd160/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, + "node_modules/rollup": { + "version": "4.60.0", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" }, "engines": { "node": ">=18.0.0", "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.46.2", - "@rollup/rollup-android-arm64": "4.46.2", - "@rollup/rollup-darwin-arm64": "4.46.2", - "@rollup/rollup-darwin-x64": "4.46.2", - "@rollup/rollup-freebsd-arm64": "4.46.2", - "@rollup/rollup-freebsd-x64": "4.46.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", - "@rollup/rollup-linux-arm-musleabihf": "4.46.2", - "@rollup/rollup-linux-arm64-gnu": "4.46.2", - "@rollup/rollup-linux-arm64-musl": "4.46.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", - "@rollup/rollup-linux-ppc64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-musl": "4.46.2", - "@rollup/rollup-linux-s390x-gnu": "4.46.2", - "@rollup/rollup-linux-x64-gnu": "4.46.2", - "@rollup/rollup-linux-x64-musl": "4.46.2", - "@rollup/rollup-win32-arm64-msvc": "4.46.2", - "@rollup/rollup-win32-ia32-msvc": "4.46.2", - "@rollup/rollup-win32-x64-msvc": "4.46.2", + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" } }, @@ -7662,13 +7534,13 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.97.1", - "resolved": "https://registry.npmmirror.com/sass/-/sass-1.97.1.tgz", - "integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==", + "version": "1.98.0", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.98.0.tgz", + "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", "license": "MIT", "dependencies": { "chokidar": "^4.0.0", - "immutable": "^5.0.2", + "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -7681,10 +7553,16 @@ "@parcel/watcher": "^2.4.1" } }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "license": "MIT" + }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -7764,42 +7642,18 @@ } }, "node_modules/shiki": { - "version": "3.15.0", - "resolved": "https://registry.npmmirror.com/shiki/-/shiki-3.15.0.tgz", - "integrity": "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@shikijs/core": "3.15.0", - "@shikijs/engine-javascript": "3.15.0", - "@shikijs/engine-oniguruma": "3.15.0", - "@shikijs/langs": "3.15.0", - "@shikijs/themes": "3.15.0", - "@shikijs/types": "3.15.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" - } - }, - "node_modules/shiki/node_modules/@shikijs/core": { - "version": "3.15.0", - "resolved": "https://registry.npmmirror.com/@shikijs/core/-/core-3.15.0.tgz", - "integrity": "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.15.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4", - "hast-util-to-html": "^9.0.5" - } - }, - "node_modules/shiki/node_modules/@shikijs/types": { - "version": "3.15.0", - "resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-3.15.0.tgz", - "integrity": "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==", + "version": "3.23.0", + "resolved": "https://registry.npmmirror.com/shiki/-/shiki-3.23.0.tgz", + "integrity": "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==", "dev": true, "license": "MIT", "dependencies": { + "@shikijs/core": "3.23.0", + "@shikijs/engine-javascript": "3.23.0", + "@shikijs/engine-oniguruma": "3.23.0", + "@shikijs/langs": "3.23.0", + "@shikijs/themes": "3.23.0", + "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } @@ -7924,9 +7778,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", "dev": true, "license": "MIT" }, @@ -7955,15 +7809,22 @@ } }, "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", "dependencies": { - "safe-buffer": "~5.2.0" + "safe-buffer": "~5.1.0" } }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmmirror.com/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -7979,23 +7840,10 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/style-mod": { - "version": "4.1.2", - "resolved": "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.2.tgz", - "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", "license": "MIT" }, "node_modules/stylis": { @@ -8005,30 +7853,17 @@ "license": "MIT" }, "node_modules/superjson": { - "version": "2.2.2", - "resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.2.tgz", - "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "version": "2.2.6", + "resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", "license": "MIT", "dependencies": { - "copy-anything": "^3.0.2" + "copy-anything": "^4" }, "engines": { "node": ">=16" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -8043,9 +7878,9 @@ } }, "node_modules/tabbable": { - "version": "6.3.0", - "resolved": "https://registry.npmmirror.com/tabbable/-/tabbable-6.3.0.tgz", - "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", + "version": "6.4.0", + "resolved": "https://registry.npmmirror.com/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", "dev": true, "license": "MIT" }, @@ -8069,11 +7904,19 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -8086,41 +7929,10 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -8128,9 +7940,9 @@ } }, "node_modules/to-buffer": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/to-buffer/-/to-buffer-1.2.1.tgz", - "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", "dev": true, "license": "MIT", "dependencies": { @@ -8142,19 +7954,6 @@ "node": ">= 0.4" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/trim-lines/-/trim-lines-3.0.1.tgz", @@ -8167,9 +7966,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -8238,16 +8037,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.51.0", - "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.51.0.tgz", - "integrity": "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA==", + "version": "8.57.2", + "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.57.2.tgz", + "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.51.0", - "@typescript-eslint/parser": "8.51.0", - "@typescript-eslint/typescript-estree": "8.51.0", - "@typescript-eslint/utils": "8.51.0" + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8257,20 +8056,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "version": "1.6.3", + "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", "license": "MIT" }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, @@ -8317,9 +8116,9 @@ } }, "node_modules/unist-util-visit": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", "dev": true, "license": "MIT", "dependencies": { @@ -8348,98 +8147,96 @@ } }, "node_modules/unplugin": { - "version": "2.3.10", - "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-2.3.10.tgz", - "integrity": "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==", - "dev": true, + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-3.0.0.tgz", + "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" }, "engines": { - "node": ">=18.12.0" + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unplugin-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmmirror.com/unplugin-utils/-/unplugin-utils-0.3.1.tgz", + "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==", + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" } }, "node_modules/unplugin-vue-components": { - "version": "30.0.0", - "resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-30.0.0.tgz", - "integrity": "sha512-4qVE/lwCgmdPTp6h0qsRN2u642tt4boBQtcpn4wQcWZAsr8TQwq+SPT3NDu/6kBFxzo/sSEK4ioXhOOBrXc3iw==", + "version": "32.0.0", + "resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-32.0.0.tgz", + "integrity": "sha512-uLdccgS7mf3pv1bCCP20y/hm+u1eOjAmygVkh+Oa70MPkzgl1eQv1L0CwdHNM3gscO8/GDMGIET98Ja47CBbZg==", "dev": true, "license": "MIT", "dependencies": { - "chokidar": "^4.0.3", - "debug": "^4.4.3", + "chokidar": "^5.0.0", "local-pkg": "^1.1.2", - "magic-string": "^0.30.19", - "mlly": "^1.8.0", + "magic-string": "^0.30.21", + "mlly": "^1.8.2", + "obug": "^2.1.1", + "picomatch": "^4.0.3", "tinyglobby": "^0.2.15", - "unplugin": "^2.3.10", + "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1" }, "engines": { - "node": ">=14" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/sponsors/antfu" }, "peerDependencies": { - "@babel/parser": "^7.15.8", "@nuxt/kit": "^3.2.2 || ^4.0.0", - "vue": "2 || 3" + "vue": "^3.0.0" }, "peerDependenciesMeta": { - "@babel/parser": { - "optional": true - }, "@nuxt/kit": { "optional": true } } }, - "node_modules/unplugin-vue-components/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/unplugin-vue-components/node_modules/unplugin-utils": { - "version": "0.3.1", - "resolved": "https://registry.npmmirror.com/unplugin-utils/-/unplugin-utils-0.3.1.tgz", - "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==", + "node_modules/unplugin-vue-components/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", "dependencies": { - "pathe": "^2.0.3", - "picomatch": "^4.0.3" + "readdirp": "^5.0.0" }, "engines": { - "node": ">=20.19.0" + "node": ">= 20.19.0" }, "funding": { - "url": "https://github.com/sponsors/sxzz" + "url": "https://paulmillr.com/funding/" } }, - "node_modules/unplugin/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "node_modules/unplugin-vue-components/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">= 20.19.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/uri-js": { @@ -8538,13 +8335,13 @@ } }, "node_modules/vite": { - "version": "7.2.2", - "resolved": "https://registry.npmmirror.com/vite/-/vite-7.2.2.tgz", - "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", + "version": "7.3.1", + "resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -8613,14 +8410,14 @@ } }, "node_modules/vite-plugin-node-polyfills": { - "version": "0.24.0", - "resolved": "https://registry.npmmirror.com/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.24.0.tgz", - "integrity": "sha512-GA9QKLH+vIM8NPaGA+o2t8PDfFUl32J8rUp1zQfMKVJQiNkOX4unE51tR6ppl6iKw5yOrDAdSH7r/UIFLCVhLw==", + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.25.0.tgz", + "integrity": "sha512-rHZ324W3LhfGPxWwQb2N048TThB6nVvnipsqBUJEzh3R9xeK9KI3si+GMQxCuAcpPJBVf0LpDtJ+beYzB3/chg==", "dev": true, "license": "MIT", "dependencies": { "@rollup/plugin-inject": "^5.0.5", - "node-stdlib-browser": "^1.2.0" + "node-stdlib-browser": "^1.3.1" }, "funding": { "url": "https://github.com/sponsors/davidmyersdev" @@ -8629,69 +8426,39 @@ "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/vitepress": { - "version": "2.0.0-alpha.12", - "resolved": "https://registry.npmmirror.com/vitepress/-/vitepress-2.0.0-alpha.12.tgz", - "integrity": "sha512-yZwCwRRepcpN5QeAhwSnEJxS3I6zJcVixqL1dnm6km4cnriLpQyy2sXQDsE5Ti3pxGPbhU51nTMwI+XC1KNnJg==", + "version": "2.0.0-alpha.17", + "resolved": "https://registry.npmmirror.com/vitepress/-/vitepress-2.0.0-alpha.17.tgz", + "integrity": "sha512-Z3VPUpwk/bHYqt1uMVOOK1/4xFiWQov1GNc2FvMdz6kvje4JRXEOngVI9C+bi5jeedMSHiA4dwKkff1NCvbZ9Q==", "dev": true, "license": "MIT", "dependencies": { - "@docsearch/css": "^4.0.0-beta.7", - "@docsearch/js": "^4.0.0-beta.7", - "@iconify-json/simple-icons": "^1.2.47", - "@shikijs/core": "^3.9.2", - "@shikijs/transformers": "^3.9.2", - "@shikijs/types": "^3.9.2", + "@docsearch/css": "^4.5.3", + "@docsearch/js": "^4.5.3", + "@docsearch/sidepanel-js": "^4.5.3", + "@iconify-json/simple-icons": "^1.2.69", + "@shikijs/core": "^3.22.0", + "@shikijs/transformers": "^3.22.0", + "@shikijs/types": "^3.22.0", "@types/markdown-it": "^14.1.2", - "@vitejs/plugin-vue": "^6.0.1", - "@vue/devtools-api": "^8.0.0", - "@vue/shared": "^3.5.18", - "@vueuse/core": "^13.6.0", - "@vueuse/integrations": "^13.6.0", - "focus-trap": "^7.6.5", + "@vitejs/plugin-vue": "^6.0.4", + "@vue/devtools-api": "^8.0.5", + "@vue/shared": "^3.5.27", + "@vueuse/core": "^14.2.0", + "@vueuse/integrations": "^14.2.0", + "focus-trap": "^8.0.0", "mark.js": "8.11.1", - "minisearch": "^7.1.2", - "shiki": "^3.9.2", - "vite": "^7.1.2", - "vue": "^3.5.18" + "minisearch": "^7.2.0", + "shiki": "^3.22.0", + "vite": "^7.3.1", + "vue": "^3.5.27" }, "bin": { "vitepress": "bin/vitepress.js" }, "peerDependencies": { "markdown-it-mathjax3": "^4", - "oxc-minify": "^0.82.1", + "oxc-minify": "*", "postcss": "^8" }, "peerDependenciesMeta": { @@ -8707,74 +8474,68 @@ } }, "node_modules/vitepress/node_modules/@vue/devtools-api": { - "version": "8.0.3", - "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-8.0.3.tgz", - "integrity": "sha512-YxZE7xNvvfq5XmjJh1ml+CzVNrRjuZYCuT5Xjj0u9RlXU7za/MRuZDUXcKfp0j7IvYkDut49vlKqbiQ1xhXP2w==", + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-8.1.1.tgz", + "integrity": "sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==", "dev": true, "license": "MIT", "dependencies": { - "@vue/devtools-kit": "^8.0.3" + "@vue/devtools-kit": "^8.1.1" } }, "node_modules/vitepress/node_modules/@vue/devtools-kit": { - "version": "8.0.3", - "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-8.0.3.tgz", - "integrity": "sha512-UF4YUOVGdfzXLCv5pMg2DxocB8dvXz278fpgEE+nJ/DRALQGAva7sj9ton0VWZ9hmXw+SV8yKMrxP2MpMhq9Wg==", + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-8.1.1.tgz", + "integrity": "sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==", "dev": true, "license": "MIT", "dependencies": { - "@vue/devtools-shared": "^8.0.3", + "@vue/devtools-shared": "^8.1.1", "birpc": "^2.6.1", "hookable": "^5.5.3", - "mitt": "^3.0.1", - "perfect-debounce": "^2.0.0", - "speakingurl": "^14.0.1", - "superjson": "^2.2.2" + "perfect-debounce": "^2.0.0" } }, "node_modules/vitepress/node_modules/@vue/devtools-shared": { - "version": "8.0.3", - "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-8.0.3.tgz", - "integrity": "sha512-s/QNll7TlpbADFZrPVsaUNPCOF8NvQgtgmmB7Tip6pLf/HcOvBTly0lfLQ0Eylu9FQ4OqBhFpLyBgwykiSf8zw==", + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-8.1.1.tgz", + "integrity": "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==", "dev": true, - "license": "MIT", - "dependencies": { - "rfdc": "^1.4.1" - } + "license": "MIT" }, "node_modules/vitepress/node_modules/perfect-debounce": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.0.0.tgz", - "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", "dev": true, "license": "MIT" }, "node_modules/vitest": { - "version": "4.0.16", - "resolved": "https://registry.npmmirror.com/vitest/-/vitest-4.0.16.tgz", - "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.0.16", - "@vitest/mocker": "4.0.16", - "@vitest/pretty-format": "4.0.16", - "@vitest/runner": "4.0.16", - "@vitest/snapshot": "4.0.16", - "@vitest/spy": "4.0.16", - "@vitest/utils": "4.0.16", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", - "std-env": "^3.10.0", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -8790,12 +8551,13 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.16", - "@vitest/browser-preview": "4.0.16", - "@vitest/browser-webdriverio": "4.0.16", - "@vitest/ui": "4.0.16", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -8824,32 +8586,12 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, - "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vitest/node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/vm-browserify/-/vm-browserify-1.1.2.tgz", @@ -8904,20 +8646,19 @@ "version": "3.1.0", "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", - "dev": true, "license": "MIT" }, "node_modules/vue": { - "version": "3.5.26", - "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.26.tgz", - "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", + "version": "3.5.31", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.31.tgz", + "integrity": "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.26", - "@vue/compiler-sfc": "3.5.26", - "@vue/runtime-dom": "3.5.26", - "@vue/server-renderer": "3.5.26", - "@vue/shared": "3.5.26" + "@vue/compiler-dom": "3.5.31", + "@vue/compiler-sfc": "3.5.31", + "@vue/runtime-dom": "3.5.31", + "@vue/server-renderer": "3.5.31", + "@vue/shared": "3.5.31" }, "peerDependencies": { "typescript": "*" @@ -8929,16 +8670,16 @@ } }, "node_modules/vue-eslint-parser": { - "version": "10.2.0", - "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-10.2.0.tgz", - "integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==", + "version": "10.4.0", + "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-10.4.0.tgz", + "integrity": "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.0", "esquery": "^1.6.0", "semver": "^7.6.3" }, @@ -8949,17 +8690,18 @@ "url": "https://github.com/sponsors/mysticatea" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" } }, "node_modules/vue-i18n": { - "version": "11.2.8", - "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.2.8.tgz", - "integrity": "sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==", + "version": "11.3.0", + "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.3.0.tgz", + "integrity": "sha512-1J+xDfDJTLhDxElkd3+XUhT7FYSZd2b8pa7IRKGxhWH/8yt6PTvi3xmWhGwhYT5EaXdatui11pF2R6tL73/zPA==", "license": "MIT", "dependencies": { - "@intlify/core-base": "11.2.8", - "@intlify/shared": "11.2.8", + "@intlify/core-base": "11.3.0", + "@intlify/devtools-types": "11.3.0", + "@intlify/shared": "11.3.0", "@vue/devtools-api": "^6.5.0" }, "engines": { @@ -8992,35 +8734,120 @@ } }, "node_modules/vue-router": { - "version": "4.6.4", - "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", - "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-5.0.4.tgz", + "integrity": "sha512-lCqDLCI2+fKVRl2OzXuzdSWmxXFLQRxQbmHugnRpTMyYiT+hNaycV0faqG5FBHDXoYrZ6MQcX87BvbY8mQ20Bg==", "license": "MIT", "dependencies": { - "@vue/devtools-api": "^6.6.4" + "@babel/generator": "^7.28.6", + "@vue-macros/common": "^3.1.1", + "@vue/devtools-api": "^8.0.6", + "ast-walker-scope": "^0.8.3", + "chokidar": "^5.0.0", + "json5": "^2.2.3", + "local-pkg": "^1.1.2", + "magic-string": "^0.30.21", + "mlly": "^1.8.0", + "muggle-string": "^0.4.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "scule": "^1.3.0", + "tinyglobby": "^0.2.15", + "unplugin": "^3.0.0", + "unplugin-utils": "^0.3.1", + "yaml": "^2.8.2" }, "funding": { "url": "https://github.com/sponsors/posva" }, "peerDependencies": { + "@pinia/colada": ">=0.21.2", + "@vue/compiler-sfc": "^3.5.17", + "pinia": "^3.0.4", "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "@pinia/colada": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "pinia": { + "optional": true + } } }, "node_modules/vue-router/node_modules/@vue/devtools-api": { - "version": "6.6.4", - "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", - "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-8.1.1.tgz", + "integrity": "sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.1.1" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-kit": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-8.1.1.tgz", + "integrity": "sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.1.1", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-shared": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-8.1.1.tgz", + "integrity": "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==", "license": "MIT" }, + "node_modules/vue-router/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/vue-router/node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, + "node_modules/vue-router/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/vue-tsc": { - "version": "3.2.1", - "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.2.1.tgz", - "integrity": "sha512-I23Rk8dkQfmcSbxDO0dmg9ioMLjKA1pjlU3Lz6Jfk2pMGu3Uryu9810XkcZH24IzPbhzPCnkKo2rEMRX0skSrw==", + "version": "3.2.6", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.2.6.tgz", + "integrity": "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==", "dev": true, "license": "MIT", "dependencies": { - "@volar/typescript": "2.4.27", - "@vue/language-core": "3.2.1" + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.2.6" }, "bin": { "vue-tsc": "bin/vue-tsc.js" @@ -9039,21 +8866,8 @@ "version": "0.6.2", "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", - "dev": true, "license": "MIT" }, - "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", @@ -9071,9 +8885,9 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "version": "1.1.20", + "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -9139,6 +8953,21 @@ "node": ">=0.4" } }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -9162,50 +8991,6 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } - }, - "src/common/prettier/plugins/scala/prettier-plugin-scala": { - "name": "@simochee/prettier-plugin-scala", - "version": "0.1.0", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@simochee/scala-parser": "file:../scala-parser" - }, - "devDependencies": { - "@tsconfig/node-ts": "^23.6.1", - "@tsconfig/node24": "^24.0.1", - "@types/node": "^22.10.2", - "prettier": "^3.4.2", - "tsup": "^8.5.0", - "typescript": "^5.7.2", - "vitest": "^2.1.9" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "prettier": "^3.0.0" - } - }, - "src/common/prettier/plugins/scala/scala-parser": { - "name": "@simochee/scala-parser", - "version": "0.1.0", - "extraneous": true, - "license": "MIT", - "dependencies": { - "chevrotain": "^11.0.3" - }, - "devDependencies": { - "@tsconfig/node-ts": "^23.6.1", - "@tsconfig/node24": "^24.0.1", - "@types/node": "^22.10.2", - "tsup": "^8.5.0", - "typescript": "^5.7.2", - "vitest": "^2.1.9" - }, - "engines": { - "node": ">=20.0.0" - } } } } diff --git a/frontend/package.json b/frontend/package.json index 88428dd8..83c457ac 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,19 +22,19 @@ "app:generate": "cd .. && wails3 generate bindings -ts" }, "dependencies": { - "@codemirror/autocomplete": "^6.20.0", - "@codemirror/commands": "^6.10.1", + "@codemirror/autocomplete": "^6.20.1", + "@codemirror/commands": "^6.10.3", "@codemirror/lang-angular": "^0.1.4", "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-go": "^6.0.1", "@codemirror/lang-html": "^6.4.11", "@codemirror/lang-java": "^6.0.2", - "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-json": "^6.0.2", "@codemirror/lang-less": "^6.0.2", "@codemirror/lang-lezer": "^6.0.2", - "@codemirror/lang-liquid": "^6.3.1", + "@codemirror/lang-liquid": "^6.3.2", "@codemirror/lang-markdown": "^6.5.0", "@codemirror/lang-php": "^6.0.2", "@codemirror/lang-python": "^6.2.1", @@ -43,65 +43,61 @@ "@codemirror/lang-sql": "^6.10.0", "@codemirror/lang-vue": "^0.1.3", "@codemirror/lang-wast": "^6.0.2", - "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "^6.12.1", + "@codemirror/lang-yaml": "^6.1.3", + "@codemirror/language": "^6.12.3", "@codemirror/language-data": "^6.5.2", "@codemirror/legacy-modes": "^6.5.2", - "@codemirror/lint": "^6.9.2", - "@codemirror/search": "^6.5.11", - "@codemirror/state": "^6.5.3", - "@codemirror/view": "^6.39.8", + "@codemirror/lint": "^6.9.5", + "@codemirror/search": "^6.6.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.40.0", "@cospaia/prettier-plugin-clojure": "^0.0.2", "@lezer/highlight": "^1.2.3", - "@lezer/lr": "^1.4.5", + "@lezer/lr": "^1.4.8", "@prettier/plugin-xml": "^3.4.2", "@replit/codemirror-lang-svelte": "^6.0.0", "@toml-tools/lexer": "^1.0.1", "@toml-tools/parser": "^1.0.1", - "@types/katex": "^0.16.7", - "@zumer/snapdom": "^2.0.1", + "@types/katex": "^0.16.8", + "@zumer/snapdom": "^2.7.0", "codemirror": "^6.0.2", - "codemirror-lang-elixir": "^4.0.0", - "colors-named": "^1.0.4", - "colors-named-hex": "^1.0.3", + "codemirror-lang-elixir": "^4.0.1", + "colors-named": "^1.0.5", + "colors-named-hex": "^1.0.4", "groovy-beautify": "^0.0.17", "hsl-matcher": "^1.2.4", - "java-parser": "^3.0.1", - "katex": "^0.16.27", - "linguist-languages": "^9.1.11", - "marked": "^17.0.1", - "mermaid": "^11.12.2", - "php-parser": "^3.2.5", + "katex": "^0.16.44", + "linguist-languages": "^9.3.1", + "marked": "^17.0.5", + "mermaid": "^11.13.0", + "php-parser": "^3.5.0", "pinia": "^3.0.4", "pinia-plugin-persistedstate": "^4.7.1", - "prettier": "^3.7.4", - "sass": "^1.97.1", - "vue": "^3.5.26", - "vue-i18n": "^11.2.8", + "prettier": "^3.8.1", + "sass": "^1.98.0", + "vue": "^3.5.31", + "vue-i18n": "^11.3.0", "vue-pick-colors": "^1.8.0", - "vue-router": "^4.6.4" + "vue-router": "^5.0.4" }, "devDependencies": { - "@eslint/js": "^9.39.2", + "@eslint/js": "^10.0.1", "@lezer/generator": "^1.8.0", - "@types/node": "^25.0.3", - "@vitejs/plugin-vue": "^6.0.3", - "@wailsio/runtime": "^3.0.0-alpha.77", + "@types/node": "^25.5.0", + "@vitejs/plugin-vue": "^6.0.5", + "@wailsio/runtime": "^3.0.0-alpha.79", "cross-env": "^10.1.0", - "eslint": "^9.39.2", - "eslint-plugin-vue": "^10.6.2", - "globals": "^16.5.0", + "eslint": "^10.1.0", + "eslint-plugin-vue": "^10.8.0", + "globals": "^17.4.0", "typescript": "^5.9.3", - "typescript-eslint": "^8.51.0", - "unplugin-vue-components": "^30.0.0", - "vite": "npm:rolldown-vite@latest", - "vite-plugin-node-polyfills": "^0.24.0", + "typescript-eslint": "^8.57.2", + "unplugin-vue-components": "^32.0.0", + "vite": "^7.3.1", + "vite-plugin-node-polyfills": "^0.25.0", "vitepress": "^2.0.0-alpha.12", - "vitest": "^4.0.16", - "vue-eslint-parser": "^10.2.0", - "vue-tsc": "^3.2.1" - }, - "overrides": { - "vite": "npm:rolldown-vite@latest" + "vitest": "^4.1.2", + "vue-eslint-parser": "^10.4.0", + "vue-tsc": "^3.2.6" } } diff --git a/frontend/src/common/prettier/plugins/clang/index.ts b/frontend/src/common/prettier/plugins/clang/index.ts index c9e8e97f..98e3353a 100644 --- a/frontend/src/common/prettier/plugins/clang/index.ts +++ b/frontend/src/common/prettier/plugins/clang/index.ts @@ -23,7 +23,7 @@ const languages = [ { name: 'C', aliases: ['c'], - parsers: ['c'], + parsers: [parserName], extensions: ['.c', '.h'], filenames: ['*.c', '*.h'], aceMode: 'c_cpp', @@ -34,7 +34,7 @@ const languages = [ { name: 'C++', aliases: ['cpp', 'cxx', 'cc'], - parsers: ['cpp'], + parsers: [parserName], extensions: ['.cpp', '.cxx', '.cc', '.hpp', '.hxx', '.hh', '.C', '.H'], filenames: ['*.cpp', '*.cxx', '*.cc', '*.hpp', '*.hxx', '*.hh', '*.C', '*.H'], aceMode: 'c_cpp', @@ -45,7 +45,7 @@ const languages = [ { name: 'Objective-C', aliases: ['objc', 'objectivec'], - parsers: ['objective-c'], + parsers: [parserName], extensions: ['.m'], filenames: ['*.m'], aceMode: 'objectivec', @@ -56,7 +56,7 @@ const languages = [ { name: 'Objective-C++', aliases: ['objcpp', 'objectivecpp'], - parsers: ['objective-cpp'], + parsers: [parserName], extensions: ['.mm'], filenames: ['*.mm'], aceMode: 'objectivec', @@ -67,7 +67,7 @@ const languages = [ { name: 'C#', aliases: ['csharp', 'cs'], - parsers: ['cs'], + parsers: [parserName], extensions: ['.cs'], filenames: ['*.cs'], aceMode: 'csharp', @@ -78,7 +78,7 @@ const languages = [ { name: 'Java', aliases: ['java'], - parsers: ['java'], + parsers: [parserName], extensions: ['.java'], filenames: ['*.java'], aceMode: 'java', @@ -89,7 +89,7 @@ const languages = [ { name: 'Protocol Buffer', aliases: ['protobuf', 'proto'], - parsers: ['proto'], + parsers: [parserName], extensions: ['.proto'], filenames: ['*.proto'], aceMode: 'protobuf', @@ -158,17 +158,7 @@ const clangPrinter: Printer = { // Helper function to determine clang-format style function getClangStyle(options: any): string { - // You can extend this to support more options - const style = options.clangStyle || 'LLVM'; - - // Support common styles - const validStyles = ['LLVM', 'Google', 'Chromium', 'Mozilla', 'WebKit', 'Microsoft', 'GNU']; - if (validStyles.includes(style)) { - return style; - } - - // Default to LLVM style - return 'LLVM'; + return options.clangStyle || 'LLVM'; } // Plugin options diff --git a/frontend/src/common/prettier/plugins/java/comments.d.ts b/frontend/src/common/prettier/plugins/java/comments.d.ts deleted file mode 100644 index 844c4ea6..00000000 --- a/frontend/src/common/prettier/plugins/java/comments.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { IToken } from "java-parser"; -import { type AstPath } from "prettier"; -import { type JavaNode, type JavaNonTerminal, type JavaParserOptions } from "./printers/helpers.js"; -export declare function determineFormatterOffOnRanges(cst: JavaNonTerminal): void; -export declare function isFullyBetweenFormatterOffOn(path: AstPath): boolean; -export declare function canAttachComment(node: JavaNode): boolean; -export declare function handleLineComment(commentNode: JavaComment, _: string, options: JavaParserOptions): boolean; -export declare function handleRemainingComment(commentNode: JavaComment): boolean; -export type JavaComment = IToken & { - value: string; - leading: boolean; - trailing: boolean; - printed: boolean; - enclosingNode?: JavaNonTerminal; - precedingNode?: JavaNonTerminal; - followingNode?: JavaNonTerminal; -}; diff --git a/frontend/src/common/prettier/plugins/java/comments.js b/frontend/src/common/prettier/plugins/java/comments.js deleted file mode 100644 index 0e15efe4..00000000 --- a/frontend/src/common/prettier/plugins/java/comments.js +++ /dev/null @@ -1,199 +0,0 @@ -import { util } from "prettier"; -import parser from "./parser.js"; -import { isEmptyStatement, isNonTerminal, isTerminal } from "./printers/helpers.js"; -const formatterOffOnRangesByCst = new WeakMap(); -export function determineFormatterOffOnRanges(cst) { - const { comments } = cst; - if (!comments) { - return; - } - const ranges = comments - .filter(({ image }) => /^(\/\/\s*@formatter:(off|on)\s*|\/\*\s*@formatter:(off|on)\s*\*\/)$/.test(image)) - .reduce((ranges, { image, startOffset }) => { - const previous = ranges.at(-1); - if (image.endsWith("off")) { - if ((previous === null || previous === void 0 ? void 0 : previous.on) !== Infinity) { - ranges.push({ off: startOffset, on: Infinity }); - } - } - else if ((previous === null || previous === void 0 ? void 0 : previous.on) === Infinity) { - previous.on = startOffset; - } - return ranges; - }, new Array()); - formatterOffOnRangesByCst.set(cst, ranges); -} -export function isFullyBetweenFormatterOffOn(path) { - var _a; - const { node, root } = path; - const start = parser.locStart(node); - const end = parser.locEnd(node); - return (((_a = formatterOffOnRangesByCst - .get(root)) === null || _a === void 0 ? void 0 : _a.some(range => range.off < start && end < range.on)) === true); -} -export function canAttachComment(node) { - var _a, _b, _c; - if (isTerminal(node)) { - const { name, CATEGORIES } = node.tokenType; - return (name === "Identifier" || - (CATEGORIES === null || CATEGORIES === void 0 ? void 0 : CATEGORIES.find(({ name }) => name === "BinaryOperator")) !== undefined); - } - const { children, name } = node; - switch (name) { - case "argumentList": - case "blockStatements": - case "emptyStatement": - case "enumBodyDeclarations": - return false; - case "annotationInterfaceMemberDeclaration": - case "classMemberDeclaration": - case "interfaceMemberDeclaration": - case "methodBody": - return !children.Semicolon; - case "blockStatement": - return !children.statement || !isEmptyStatement(children.statement[0]); - case "classBodyDeclaration": - return !((_a = children.classMemberDeclaration) === null || _a === void 0 ? void 0 : _a[0].children.Semicolon); - case "recordBodyDeclaration": - return !((_c = (_b = children.classBodyDeclaration) === null || _b === void 0 ? void 0 : _b[0].children.classMemberDeclaration) === null || _c === void 0 ? void 0 : _c[0].children.Semicolon); - case "statement": - return !isEmptyStatement(node); - case "statementWithoutTrailingSubstatement": - return !children.emptyStatement; - default: - return true; - } -} -export function handleLineComment(commentNode, _, options) { - return [ - handleBinaryExpressionComments, - handleFqnOrRefTypeComments, - handleIfStatementComments, - handleJumpStatementComments, - handleLabeledStatementComments, - handleNameComments - ].some(fn => fn(commentNode, options)); -} -export function handleRemainingComment(commentNode) { - return [ - handleFqnOrRefTypeComments, - handleMethodDeclaratorComments, - handleNameComments, - handleJumpStatementComments - ].some(fn => fn(commentNode)); -} -function handleBinaryExpressionComments(commentNode, options) { - const { enclosingNode, precedingNode, followingNode } = commentNode; - if (enclosingNode && - isNonTerminal(enclosingNode) && - enclosingNode.name === "binaryExpression") { - if (isBinaryOperator(followingNode)) { - if (options.experimentalOperatorPosition === "start") { - util.addLeadingComment(followingNode, commentNode); - } - else { - util.addTrailingComment(followingNode, commentNode); - } - return true; - } - else if (options.experimentalOperatorPosition === "start" && - isBinaryOperator(precedingNode)) { - util.addLeadingComment(precedingNode, commentNode); - return true; - } - } - return false; -} -function handleFqnOrRefTypeComments(commentNode) { - const { enclosingNode, followingNode } = commentNode; - if (enclosingNode && - isNonTerminal(enclosingNode) && - enclosingNode.name === "fqnOrRefType" && - followingNode) { - util.addLeadingComment(followingNode, commentNode); - return true; - } - return false; -} -function handleIfStatementComments(commentNode) { - const { enclosingNode, precedingNode } = commentNode; - if (enclosingNode && - isNonTerminal(enclosingNode) && - enclosingNode.name === "ifStatement" && - precedingNode && - isNonTerminal(precedingNode) && - precedingNode.name === "statement") { - util.addDanglingComment(enclosingNode, commentNode, undefined); - return true; - } - return false; -} -function handleJumpStatementComments(commentNode) { - const { enclosingNode, precedingNode, followingNode } = commentNode; - if (enclosingNode && - !precedingNode && - !followingNode && - isNonTerminal(enclosingNode) && - ["breakStatement", "continueStatement", "returnStatement"].includes(enclosingNode.name)) { - util.addTrailingComment(enclosingNode, commentNode); - return true; - } - return false; -} -function handleLabeledStatementComments(commentNode) { - const { enclosingNode, precedingNode } = commentNode; - if (enclosingNode && - precedingNode && - isNonTerminal(enclosingNode) && - enclosingNode.name === "labeledStatement" && - isTerminal(precedingNode) && - precedingNode.tokenType.name === "Identifier") { - util.addLeadingComment(precedingNode, commentNode); - return true; - } - return false; -} -function handleMethodDeclaratorComments(commentNode) { - const { enclosingNode } = commentNode; - if (enclosingNode && - isNonTerminal(enclosingNode) && - enclosingNode.name === "methodDeclarator" && - !enclosingNode.children.receiverParameter && - !enclosingNode.children.formalParameterList && - enclosingNode.children.LBrace[0].startOffset < commentNode.startOffset && - commentNode.startOffset < enclosingNode.children.RBrace[0].startOffset) { - util.addDanglingComment(enclosingNode, commentNode, undefined); - return true; - } - return false; -} -function handleNameComments(commentNode) { - const { enclosingNode, precedingNode } = commentNode; - if (enclosingNode && - precedingNode && - isNonTerminal(enclosingNode) && - isTerminal(precedingNode) && - precedingNode.tokenType.name === "Identifier" && - [ - "ambiguousName", - "classOrInterfaceTypeToInstantiate", - "expressionName", - "moduleDeclaration", - "moduleName", - "packageDeclaration", - "packageName", - "packageOrTypeName", - "typeName" - ].includes(enclosingNode.name)) { - util.addTrailingComment(precedingNode, commentNode); - return true; - } - return false; -} -function isBinaryOperator(node) { - var _a; - return (node !== undefined && - (isNonTerminal(node) - ? node.name === "shiftOperator" - : (_a = node.tokenType.CATEGORIES) === null || _a === void 0 ? void 0 : _a.some(({ name }) => name === "BinaryOperator"))); -} diff --git a/frontend/src/common/prettier/plugins/java/index.d.ts b/frontend/src/common/prettier/plugins/java/index.d.ts deleted file mode 100644 index 1ae54b62..00000000 --- a/frontend/src/common/prettier/plugins/java/index.d.ts +++ /dev/null @@ -1,563 +0,0 @@ -import type { JavaNode } from "./printers/helpers.js"; -declare const _default: { - languages: { - name: string; - parsers: "java"[]; - group: string; - tmScope: string; - aceMode: string; - codemirrorMode: string; - codemirrorMimeType: string; - extensions: string[]; - linguistLanguageId: number; - vscodeLanguageIds: string[]; - }[]; - parsers: { - java: { - parse(text: string, options: import("./printers/helpers.js").JavaParserOptions): import("./printers/helpers.js").JavaNonTerminal; - astFormat: string; - hasPragma(text: string): boolean; - locStart(node: JavaNode): number; - locEnd(node: JavaNode): number; - }; - }; - printers: { - java: { - print(path: import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath | import("prettier").AstPath, options: import("prettier").ParserOptions, print: (path: import("prettier").AstPath) => import("prettier").Doc, args: unknown): import("prettier/doc.js").builders.Doc; - hasPrettierIgnore(path: import("prettier").AstPath): boolean; - canAttachComment: typeof import("./comments.js").canAttachComment; - isBlockComment(node: JavaNode): boolean; - printComment(commentPath: import("prettier").AstPath): string | import("prettier/doc.js").builders.Doc[]; - getCommentChildNodes(node: JavaNode): any[]; - handleComments: { - ownLine: typeof import("./comments.js").handleLineComment; - endOfLine: typeof import("./comments.js").handleLineComment; - remaining: typeof import("./comments.js").handleRemainingComment; - }; - }; - }; - options: { - entrypoint: { - type: "choice"; - category: string; - default: string; - choices: { - value: string; - description: string; - }[]; - description: string; - }; - arrowParens: { - type: "choice"; - category: string; - default: string; - choices: { - value: string; - description: string; - }[]; - description: string; - }; - trailingComma: { - type: "choice"; - category: string; - default: string; - choices: { - value: string; - description: string; - }[]; - description: string; - }; - experimentalOperatorPosition: { - type: "choice"; - category: string; - default: string; - choices: { - value: string; - description: string; - }[]; - description: string; - }; - }; - defaultOptions: { - arrowParens: "avoid"; - }; -}; -export default _default; diff --git a/frontend/src/common/prettier/plugins/java/index.js b/frontend/src/common/prettier/plugins/java/index.js deleted file mode 100644 index b83a4434..00000000 --- a/frontend/src/common/prettier/plugins/java/index.js +++ /dev/null @@ -1,29 +0,0 @@ -import options from "./options.js"; -import parser from "./parser.js"; -import printer from "./printer.js"; -export default { - languages: [ - { - name: "Java", - parsers: ["java"], - group: "Java", - tmScope: "source.java", - aceMode: "java", - codemirrorMode: "clike", - codemirrorMimeType: "text/x-java", - extensions: [".java"], - linguistLanguageId: 181, - vscodeLanguageIds: ["java"] - } - ], - parsers: { - java: parser - }, - printers: { - java: printer - }, - options, - defaultOptions: { - arrowParens: "avoid" - } -}; diff --git a/frontend/src/common/prettier/plugins/java/options.d.ts b/frontend/src/common/prettier/plugins/java/options.d.ts deleted file mode 100644 index c67b27f1..00000000 --- a/frontend/src/common/prettier/plugins/java/options.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -declare const _default: { - entrypoint: { - type: "choice"; - category: string; - default: string; - choices: { - value: string; - description: string; - }[]; - description: string; - }; - arrowParens: { - type: "choice"; - category: string; - default: string; - choices: { - value: string; - description: string; - }[]; - description: string; - }; - trailingComma: { - type: "choice"; - category: string; - default: string; - choices: { - value: string; - description: string; - }[]; - description: string; - }; - experimentalOperatorPosition: { - type: "choice"; - category: string; - default: string; - choices: { - value: string; - description: string; - }[]; - description: string; - }; -}; -export default _default; diff --git a/frontend/src/common/prettier/plugins/java/options.js b/frontend/src/common/prettier/plugins/java/options.js deleted file mode 100644 index 2651cb75..00000000 --- a/frontend/src/common/prettier/plugins/java/options.js +++ /dev/null @@ -1,284 +0,0 @@ -export default { - entrypoint: { - type: "choice", - category: "Global", - default: "compilationUnit", - // sed -nr 's/.*\.RULE\(([^,]+),.*/\1/p' $(ls path/to/java-parser/rules/folder/*) - choices: [ - { value: "arrayInitializer", description: "" }, - { value: "variableInitializerList", description: "" }, - { value: "block", description: "" }, - { value: "blockStatements", description: "" }, - { value: "blockStatement", description: "" }, - { value: "localVariableDeclarationStatement", description: "" }, - { value: "localVariableDeclaration", description: "" }, - { value: "localVariableType", description: "" }, - { value: "statement", description: "" }, - { value: "statementWithoutTrailingSubstatement", description: "" }, - { value: "emptyStatement", description: "" }, - { value: "labeledStatement", description: "" }, - { value: "expressionStatement", description: "" }, - { value: "statementExpression", description: "" }, - { value: "ifStatement", description: "" }, - { value: "assertStatement", description: "" }, - { value: "switchStatement", description: "" }, - { value: "switchBlock", description: "" }, - { value: "switchBlockStatementGroup", description: "" }, - { value: "switchLabel", description: "" }, - { value: "switchRule", description: "" }, - { value: "caseConstant", description: "" }, - { value: "casePattern", description: "" }, - { value: "whileStatement", description: "" }, - { value: "doStatement", description: "" }, - { value: "forStatement", description: "" }, - { value: "basicForStatement", description: "" }, - { value: "forInit", description: "" }, - { value: "forUpdate", description: "" }, - { value: "statementExpressionList", description: "" }, - { value: "enhancedForStatement", description: "" }, - { value: "breakStatement", description: "" }, - { value: "continueStatement", description: "" }, - { value: "returnStatement", description: "" }, - { value: "throwStatement", description: "" }, - { value: "synchronizedStatement", description: "" }, - { value: "tryStatement", description: "" }, - { value: "catches", description: "" }, - { value: "catchClause", description: "" }, - { value: "catchFormalParameter", description: "" }, - { value: "catchType", description: "" }, - { value: "finally", description: "" }, - { value: "tryWithResourcesStatement", description: "" }, - { value: "resourceSpecification", description: "" }, - { value: "resourceList", description: "" }, - { value: "resource", description: "" }, - { value: "yieldStatement", description: "" }, - { value: "variableAccess", description: "" }, - { value: "classDeclaration", description: "" }, - { value: "normalClassDeclaration", description: "" }, - { value: "classModifier", description: "" }, - { value: "typeParameters", description: "" }, - { value: "typeParameterList", description: "" }, - { value: "classExtends", description: "" }, - { value: "classImplements", description: "" }, - { value: "interfaceTypeList", description: "" }, - { value: "classPermits", description: "" }, - { value: "classBody", description: "" }, - { value: "classBodyDeclaration", description: "" }, - { value: "classMemberDeclaration", description: "" }, - { value: "fieldDeclaration", description: "" }, - { value: "fieldModifier", description: "" }, - { value: "variableDeclaratorList", description: "" }, - { value: "variableDeclarator", description: "" }, - { value: "variableDeclaratorId", description: "" }, - { value: "variableInitializer", description: "" }, - { value: "unannType", description: "" }, - { value: "unannPrimitiveTypeWithOptionalDimsSuffix", description: "" }, - { value: "unannPrimitiveType", description: "" }, - { value: "unannReferenceType", description: "" }, - { value: "unannClassOrInterfaceType", description: "" }, - { value: "unannClassType", description: "" }, - { value: "unannInterfaceType", description: "" }, - { value: "unannTypeVariable", description: "" }, - { value: "methodDeclaration", description: "" }, - { value: "methodModifier", description: "" }, - { value: "methodHeader", description: "" }, - { value: "result", description: "" }, - { value: "methodDeclarator", description: "" }, - { value: "receiverParameter", description: "" }, - { value: "formalParameterList", description: "" }, - { value: "formalParameter", description: "" }, - { value: "variableParaRegularParameter", description: "" }, - { value: "variableArityParameter", description: "" }, - { value: "variableModifier", description: "" }, - { value: "throws", description: "" }, - { value: "exceptionTypeList", description: "" }, - { value: "exceptionType", description: "" }, - { value: "methodBody", description: "" }, - { value: "instanceInitializer", description: "" }, - { value: "staticInitializer", description: "" }, - { value: "constructorDeclaration", description: "" }, - { value: "constructorModifier", description: "" }, - { value: "constructorDeclarator", description: "" }, - { value: "simpleTypeName", description: "" }, - { value: "constructorBody", description: "" }, - { value: "explicitConstructorInvocation", description: "" }, - { value: "unqualifiedExplicitConstructorInvocation", description: "" }, - { value: "qualifiedExplicitConstructorInvocation", description: "" }, - { value: "enumDeclaration", description: "" }, - { value: "enumBody", description: "" }, - { value: "enumConstantList", description: "" }, - { value: "enumConstant", description: "" }, - { value: "enumConstantModifier", description: "" }, - { value: "enumBodyDeclarations", description: "" }, - { value: "recordDeclaration", description: "" }, - { value: "recordHeader", description: "" }, - { value: "recordComponentList", description: "" }, - { value: "recordComponent", description: "" }, - { value: "variableArityRecordComponent", description: "" }, - { value: "recordComponentModifier", description: "" }, - { value: "recordBody", description: "" }, - { value: "recordBodyDeclaration", description: "" }, - { value: "compactConstructorDeclaration", description: "" }, - { value: "isDims", description: "" }, - { value: "expression", description: "" }, - { value: "lambdaExpression", description: "" }, - { value: "lambdaParameters", description: "" }, - { value: "lambdaParametersWithBraces", description: "" }, - { value: "lambdaParameterList", description: "" }, - { value: "conciseLambdaParameterList", description: "" }, - { value: "normalLambdaParameterList", description: "" }, - { value: "normalLambdaParameter", description: "" }, - { value: "regularLambdaParameter", description: "" }, - { value: "lambdaParameterType", description: "" }, - { value: "conciseLambdaParameter", description: "" }, - { value: "lambdaBody", description: "" }, - { value: "conditionalExpression", description: "" }, - { value: "binaryExpression", description: "" }, - { value: "unaryExpression", description: "" }, - { value: "unaryExpressionNotPlusMinus", description: "" }, - { value: "primary", description: "" }, - { value: "primaryPrefix", description: "" }, - { value: "primarySuffix", description: "" }, - { value: "fqnOrRefType", description: "" }, - { value: "fqnOrRefTypePartRest", description: "" }, - { value: "fqnOrRefTypePartCommon", description: "" }, - { value: "fqnOrRefTypePartFirst", description: "" }, - { value: "parenthesisExpression", description: "" }, - { value: "castExpression", description: "" }, - { value: "primitiveCastExpression", description: "" }, - { value: "referenceTypeCastExpression", description: "" }, - { value: "newExpression", description: "" }, - { value: "unqualifiedClassInstanceCreationExpression", description: "" }, - { value: "classOrInterfaceTypeToInstantiate", description: "" }, - { value: "typeArgumentsOrDiamond", description: "" }, - { value: "diamond", description: "" }, - { value: "methodInvocationSuffix", description: "" }, - { value: "argumentList", description: "" }, - { value: "arrayCreationExpression", description: "" }, - { - value: "arrayCreationExpressionWithoutInitializerSuffix", - description: "" - }, - { value: "arrayCreationWithInitializerSuffix", description: "" }, - { value: "dimExprs", description: "" }, - { value: "dimExpr", description: "" }, - { value: "classLiteralSuffix", description: "" }, - { value: "arrayAccessSuffix", description: "" }, - { value: "methodReferenceSuffix", description: "" }, - { value: "templateArgument", description: "" }, - { value: "template", description: "" }, - { value: "stringTemplate", description: "" }, - { value: "textBlockTemplate", description: "" }, - { value: "embeddedExpression", description: "" }, - { value: "pattern", description: "" }, - { value: "typePattern", description: "" }, - { value: "recordPattern", description: "" }, - { value: "componentPatternList", description: "" }, - { value: "componentPattern", description: "" }, - { value: "matchAllPattern", description: "" }, - { value: "guard", description: "" }, - { value: "isRefTypeInMethodRef", description: "" }, - { value: "interfaceDeclaration", description: "" }, - { value: "normalInterfaceDeclaration", description: "" }, - { value: "interfaceModifier", description: "" }, - { value: "interfaceExtends", description: "" }, - { value: "interfacePermits", description: "" }, - { value: "interfaceBody", description: "" }, - { value: "interfaceMemberDeclaration", description: "" }, - { value: "constantDeclaration", description: "" }, - { value: "constantModifier", description: "" }, - { value: "interfaceMethodDeclaration", description: "" }, - { value: "interfaceMethodModifier", description: "" }, - { value: "annotationInterfaceDeclaration", description: "" }, - { value: "annotationInterfaceBody", description: "" }, - { value: "annotationInterfaceMemberDeclaration", description: "" }, - { value: "annotationInterfaceElementDeclaration", description: "" }, - { value: "annotationInterfaceElementModifier", description: "" }, - { value: "defaultValue", description: "" }, - { value: "annotation", description: "" }, - { value: "elementValuePairList", description: "" }, - { value: "elementValuePair", description: "" }, - { value: "elementValue", description: "" }, - { value: "elementValueArrayInitializer", description: "" }, - { value: "elementValueList", description: "" }, - { value: "literal", description: "" }, - { value: "integerLiteral", description: "" }, - { value: "floatingPointLiteral", description: "" }, - { value: "booleanLiteral", description: "" }, - { value: "shiftOperator", description: "" }, - { value: "moduleName", description: "" }, - { value: "packageName", description: "" }, - { value: "typeName", description: "" }, - { value: "expressionName", description: "" }, - { value: "methodName", description: "" }, - { value: "packageOrTypeName", description: "" }, - { value: "ambiguousName", description: "" }, - { value: "compilationUnit", description: "" }, - { value: "ordinaryCompilationUnit", description: "" }, - { value: "modularCompilationUnit", description: "" }, - { value: "packageDeclaration", description: "" }, - { value: "packageModifier", description: "" }, - { value: "importDeclaration", description: "" }, - { value: "typeDeclaration", description: "" }, - { value: "moduleDeclaration", description: "" }, - { value: "moduleDirective", description: "" }, - { value: "requiresModuleDirective", description: "" }, - { value: "exportsModuleDirective", description: "" }, - { value: "opensModuleDirective", description: "" }, - { value: "usesModuleDirective", description: "" }, - { value: "providesModuleDirective", description: "" }, - { value: "requiresModifier", description: "" }, - { value: "primitiveType", description: "" }, - { value: "numericType", description: "" }, - { value: "integralType", description: "" }, - { value: "floatingPointType", description: "" }, - { value: "referenceType", description: "" }, - { value: "classOrInterfaceType", description: "" }, - { value: "classType", description: "" }, - { value: "interfaceType", description: "" }, - { value: "typeVariable", description: "" }, - { value: "dims", description: "" }, - { value: "typeParameter", description: "" }, - { value: "typeParameterModifier", description: "" }, - { value: "typeBound", description: "" }, - { value: "additionalBound", description: "" }, - { value: "typeArguments", description: "" }, - { value: "typeArgumentList", description: "" }, - { value: "typeArgument", description: "" }, - { value: "wildcard", description: "" }, - { value: "wildcardBounds", description: "" } - ], - description: "Prettify from the entrypoint, allowing to use prettier on snippet." - }, - arrowParens: { - type: "choice", - category: "Java", - default: "always", - choices: [ - { value: "always", description: "" }, - { value: "avoid", description: "" } - ], - description: "Include parentheses around a sole arrow function parameter." - }, - trailingComma: { - type: "choice", - category: "Java", - default: "all", - choices: [ - { value: "all", description: "" }, - { value: "es5", description: "" }, - { value: "none", description: "" } - ], - description: "Print trailing commas wherever possible when multi-line." - }, - experimentalOperatorPosition: { - type: "choice", - category: "Java", - default: "end", - choices: [ - { value: "start", description: "" }, - { value: "end", description: "" } - ], - description: "Where to print operators when binary expressions wrap lines." - } -}; diff --git a/frontend/src/common/prettier/plugins/java/parser.d.ts b/frontend/src/common/prettier/plugins/java/parser.d.ts deleted file mode 100644 index 4ed8b4d6..00000000 --- a/frontend/src/common/prettier/plugins/java/parser.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type JavaNode, type JavaNonTerminal, type JavaParserOptions } from "./printers/helpers.js"; -declare const _default: { - parse(text: string, options: JavaParserOptions): JavaNonTerminal; - astFormat: string; - hasPragma(text: string): boolean; - locStart(node: JavaNode): number; - locEnd(node: JavaNode): number; -}; -export default _default; diff --git a/frontend/src/common/prettier/plugins/java/parser.js b/frontend/src/common/prettier/plugins/java/parser.js deleted file mode 100644 index f5befcf9..00000000 --- a/frontend/src/common/prettier/plugins/java/parser.js +++ /dev/null @@ -1,24 +0,0 @@ -import { parse } from "java-parser"; -import { determineFormatterOffOnRanges } from "./comments.js"; -import { isTerminal } from "./printers/helpers.js"; -export default { - parse(text, options) { - var _a; - const cst = parse(text, options.entrypoint); - (_a = cst.comments) === null || _a === void 0 ? void 0 : _a.forEach(comment => { - comment.value = comment.image; - }); - determineFormatterOffOnRanges(cst); - return cst; - }, - astFormat: "java", - hasPragma(text) { - return /^\/\*\*\n\s+\*\s@(format|prettier)\n\s+\*\//.test(text); - }, - locStart(node) { - return isTerminal(node) ? node.startOffset : node.location.startOffset; - }, - locEnd(node) { - return (isTerminal(node) ? node.endOffset : node.location.endOffset) + 1; - } -}; diff --git a/frontend/src/common/prettier/plugins/java/printer.d.ts b/frontend/src/common/prettier/plugins/java/printer.d.ts deleted file mode 100644 index 565fc6eb..00000000 --- a/frontend/src/common/prettier/plugins/java/printer.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { AstPath } from "prettier"; -import { canAttachComment, handleLineComment, handleRemainingComment } from "./comments.js"; -import { type JavaNode } from "./printers/helpers.js"; -declare const _default: { - print(path: DistributedAstPath, options: import("prettier").ParserOptions, print: (path: AstPath) => import("prettier").Doc, args: unknown): import("prettier/doc.js").builders.Doc; - hasPrettierIgnore(path: AstPath): boolean; - canAttachComment: typeof canAttachComment; - isBlockComment(node: JavaNode): boolean; - printComment(commentPath: AstPath): string | import("prettier/doc.js").builders.Doc[]; - getCommentChildNodes(node: JavaNode): any[]; - handleComments: { - ownLine: typeof handleLineComment; - endOfLine: typeof handleLineComment; - remaining: typeof handleRemainingComment; - }; -}; -export default _default; -type DistributedAstPath = T extends any ? AstPath : never; diff --git a/frontend/src/common/prettier/plugins/java/printer.js b/frontend/src/common/prettier/plugins/java/printer.js deleted file mode 100644 index 88b01648..00000000 --- a/frontend/src/common/prettier/plugins/java/printer.js +++ /dev/null @@ -1,40 +0,0 @@ -import { canAttachComment, handleLineComment, handleRemainingComment, isFullyBetweenFormatterOffOn } from "./comments.js"; -import { isNonTerminal, isTerminal, printComment } from "./printers/helpers.js"; -import { printerForNodeType } from "./printers/index.js"; -export default { - print(path, options, print, args) { - return hasTerminal(path) - ? path.node.image - : printerForNodeType(path.node.name)(path, print, options, args); - }, - hasPrettierIgnore(path) { - var _a; - const { node } = path; - return (((_a = node.comments) === null || _a === void 0 ? void 0 : _a.some(({ image }) => /^(\/\/\s*prettier-ignore|\/\*\s*prettier-ignore\s*\*\/)$/.test(image))) === true || - (canAttachComment(node) && isFullyBetweenFormatterOffOn(path))); - }, - canAttachComment, - isBlockComment(node) { - return isTerminal(node) && node.tokenType.name === "TraditionalComment"; - }, - printComment(commentPath) { - const { node } = commentPath; - if (isNonTerminal(node) || node.tokenType.GROUP !== "comments") { - throw new Error(`Not a comment: ${JSON.stringify(node)}`); - } - return printComment(node); - }, - getCommentChildNodes(node) { - return isNonTerminal(node) - ? Object.values(node.children).flatMap(child => child) - : []; - }, - handleComments: { - ownLine: handleLineComment, - endOfLine: handleLineComment, - remaining: handleRemainingComment - } -}; -function hasTerminal(path) { - return isTerminal(path.node); -} diff --git a/frontend/src/common/prettier/plugins/java/printers/arrays.d.ts b/frontend/src/common/prettier/plugins/java/printers/arrays.d.ts deleted file mode 100644 index ab22bfc9..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/arrays.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare const _default: { - arrayInitializer(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn, options: import("./helpers.js").JavaParserOptions): import("prettier/doc.js").builders.Group | "{}"; - variableInitializerList(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): import("prettier/doc.js").builders.Doc[]; -}; -export default _default; diff --git a/frontend/src/common/prettier/plugins/java/printers/arrays.js b/frontend/src/common/prettier/plugins/java/printers/arrays.js deleted file mode 100644 index 704bc6c0..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/arrays.js +++ /dev/null @@ -1,9 +0,0 @@ -import { printArrayInitializer, printList } from "./helpers.js"; -export default { - arrayInitializer(path, print, options) { - return printArrayInitializer(path, print, options, "variableInitializerList"); - }, - variableInitializerList(path, print) { - return printList(path, print, "variableInitializer"); - } -}; diff --git a/frontend/src/common/prettier/plugins/java/printers/blocks-and-statements.d.ts b/frontend/src/common/prettier/plugins/java/printers/blocks-and-statements.d.ts deleted file mode 100644 index a5af49ee..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/blocks-and-statements.d.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { builders } from "prettier/doc"; -import { printSingle } from "./helpers.js"; -declare const _default: { - block(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Group | "{}" | (string | builders.Indent | builders.Hardline)[]; - blockStatements(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - blockStatement: typeof printSingle; - localVariableDeclarationStatement(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - localVariableDeclaration(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - localVariableType: typeof printSingle; - statement: typeof printSingle; - statementWithoutTrailingSubstatement: typeof printSingle; - emptyStatement(): string; - labeledStatement(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - expressionStatement(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - statementExpression: typeof printSingle; - ifStatement(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - assertStatement(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - switchStatement(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - switchBlock(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Group | "{}" | (string | builders.Indent | builders.Hardline)[]; - switchBlockStatementGroup(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - switchLabel(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): "default" | builders.Group | builders.Doc[]; - switchRule(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - caseConstant: typeof printSingle; - casePattern: typeof printSingle; - whileStatement(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - doStatement(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): (string | builders.Group | builders.Doc[])[]; - forStatement: typeof printSingle; - basicForStatement(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - forInit: typeof printSingle; - forUpdate: typeof printSingle; - statementExpressionList(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Group; - enhancedForStatement(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Group; - breakStatement(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[] | "break;"; - continueStatement(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[] | "continue;"; - returnStatement(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - throwStatement(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - synchronizedStatement(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - tryStatement(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc; - catches(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - catchClause(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - catchFormalParameter(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - catchType(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - finally(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - tryWithResourcesStatement(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - resourceSpecification(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Group | "()"; - resourceList(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - resource: typeof printSingle; - yieldStatement(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - variableAccess: typeof printSingle; -}; -export default _default; diff --git a/frontend/src/common/prettier/plugins/java/printers/blocks-and-statements.js b/frontend/src/common/prettier/plugins/java/printers/blocks-and-statements.js deleted file mode 100644 index 224f1565..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/blocks-and-statements.js +++ /dev/null @@ -1,337 +0,0 @@ -import { builders } from "prettier/doc"; -import { call, definedKeys, indentInParentheses, isBinaryExpression, isEmptyStatement, lineEndWithComments, lineStartWithComments, map, onlyDefinedKey, printBlock, printDanglingComments, printSingle, printWithModifiers } from "./helpers.js"; -const { group, hardline, ifBreak, indent, join, line, softline } = builders; -export default { - block(path, print) { - const statements = path.node.children.blockStatements - ? call(path, print, "blockStatements") - : []; - return printBlock(path, statements.length ? [statements] : []); - }, - blockStatements(path, print) { - return join(hardline, map(path, statementPath => { - const { node, previous } = statementPath; - const statement = print(statementPath); - return previous && - lineStartWithComments(node) > lineEndWithComments(previous) + 1 - ? [hardline, statement] - : statement; - }, "blockStatement").filter(doc => doc !== "")); - }, - blockStatement: printSingle, - localVariableDeclarationStatement(path, print) { - return [call(path, print, "localVariableDeclaration"), ";"]; - }, - localVariableDeclaration(path, print) { - const declaration = join(" ", [ - call(path, print, "localVariableType"), - call(path, print, "variableDeclaratorList") - ]); - return printWithModifiers(path, print, "variableModifier", declaration); - }, - localVariableType: printSingle, - statement: printSingle, - statementWithoutTrailingSubstatement: printSingle, - emptyStatement() { - return ""; - }, - labeledStatement(path, print) { - return [ - call(path, print, "Identifier"), - ": ", - call(path, print, "statement") - ]; - }, - expressionStatement(path, print) { - return [call(path, print, "statementExpression"), ";"]; - }, - statementExpression: printSingle, - ifStatement(path, print) { - var _a; - const { children } = path.node; - const hasEmptyStatement = isEmptyStatement(children.statement[0]); - const statements = map(path, print, "statement"); - const statement = [ - "if ", - indentInParentheses(call(path, print, "expression")), - hasEmptyStatement ? ";" : [" ", statements[0]] - ]; - if (children.Else) { - const danglingComments = printDanglingComments(path); - if (danglingComments.length) { - statement.push(hardline, ...danglingComments, hardline); - } - else { - const elseHasBlock = ((_a = children.statement[0].children - .statementWithoutTrailingSubstatement) === null || _a === void 0 ? void 0 : _a[0].children.block) !== - undefined; - statement.push(elseHasBlock ? " " : hardline); - } - const elseHasEmptyStatement = isEmptyStatement(children.statement[1]); - statement.push("else", elseHasEmptyStatement ? ";" : [" ", statements[1]]); - } - return statement; - }, - assertStatement(path, print) { - return ["assert ", ...join([" : "], map(path, print, "expression")), ";"]; - }, - switchStatement(path, print) { - return join(" ", [ - "switch", - indentInParentheses(call(path, print, "expression")), - call(path, print, "switchBlock") - ]); - }, - switchBlock(path, print) { - const { children } = path.node; - const caseKeys = definedKeys(children, [ - "switchBlockStatementGroup", - "switchRule" - ]); - const cases = caseKeys.length === 1 ? map(path, print, caseKeys[0]) : []; - return printBlock(path, cases); - }, - switchBlockStatementGroup(path, print) { - var _a, _b; - const { children } = path.node; - const switchLabel = call(path, print, "switchLabel"); - if (!children.blockStatements) { - return [switchLabel, ":"]; - } - const blockStatements = call(path, print, "blockStatements"); - const statements = children.blockStatements[0].children.blockStatement; - const onlyStatementIsBlock = statements.length === 1 && - ((_b = (_a = statements[0].children.statement) === null || _a === void 0 ? void 0 : _a[0].children.statementWithoutTrailingSubstatement) === null || _b === void 0 ? void 0 : _b[0].children.block) !== undefined; - return [ - switchLabel, - ":", - onlyStatementIsBlock - ? [" ", blockStatements] - : indent([hardline, blockStatements]) - ]; - }, - switchLabel(path, print) { - var _a, _b; - const { children } = path.node; - if (!((_b = (_a = children.caseConstant) !== null && _a !== void 0 ? _a : children.casePattern) !== null && _b !== void 0 ? _b : children.Null)) { - return "default"; - } - const values = []; - if (children.Null) { - values.push("null"); - if (children.Default) { - values.push("default"); - } - } - else { - const valuesKey = onlyDefinedKey(children, [ - "caseConstant", - "casePattern" - ]); - values.push(...map(path, print, valuesKey)); - } - const hasMultipleValues = values.length > 1; - const label = hasMultipleValues - ? ["case", indent([line, ...join([",", line], values)])] - : ["case ", values[0]]; - return children.guard - ? [ - group([...label, hasMultipleValues ? line : " "]), - call(path, print, "guard") - ] - : group(label); - }, - switchRule(path, print) { - const { children } = path.node; - const bodyKey = onlyDefinedKey(children, [ - "block", - "expression", - "throwStatement" - ]); - const parts = [ - call(path, print, "switchLabel"), - " -> ", - call(path, print, bodyKey) - ]; - if (children.Semicolon) { - parts.push(";"); - } - return parts; - }, - caseConstant: printSingle, - casePattern: printSingle, - whileStatement(path, print) { - const statement = call(path, print, "statement"); - const hasEmptyStatement = isEmptyStatement(path.node.children.statement[0]); - return [ - "while ", - indentInParentheses(call(path, print, "expression")), - ...[hasEmptyStatement ? ";" : " ", statement] - ]; - }, - doStatement(path, print) { - const hasEmptyStatement = isEmptyStatement(path.node.children.statement[0]); - return [ - "do", - hasEmptyStatement ? ";" : [" ", call(path, print, "statement")], - " while ", - indentInParentheses(call(path, print, "expression")), - ";" - ]; - }, - forStatement: printSingle, - basicForStatement(path, print) { - const { children } = path.node; - const danglingComments = printDanglingComments(path); - if (danglingComments.length) { - danglingComments.push(hardline); - } - const expressions = ["forInit", "expression", "forUpdate"].map(expressionKey => expressionKey in children ? call(path, print, expressionKey) : ""); - const hasEmptyStatement = isEmptyStatement(children.statement[0]); - return [ - ...danglingComments, - "for ", - expressions.some(expression => expression !== "") - ? indentInParentheses(join([";", line], expressions)) - : "(;;)", - hasEmptyStatement ? ";" : [" ", call(path, print, "statement")] - ]; - }, - forInit: printSingle, - forUpdate: printSingle, - statementExpressionList(path, print) { - return group(map(path, print, "statementExpression").map((expression, index) => index === 0 ? expression : [",", indent([line, expression])])); - }, - enhancedForStatement(path, print) { - var _a; - const statementNode = path.node.children.statement[0]; - const forStatement = [ - printDanglingComments(path), - "for ", - "(", - call(path, print, "localVariableDeclaration"), - " : ", - call(path, print, "expression"), - ")" - ]; - if (isEmptyStatement(statementNode)) { - forStatement.push(";"); - } - else { - const hasStatementBlock = ((_a = statementNode.children.statementWithoutTrailingSubstatement) === null || _a === void 0 ? void 0 : _a[0].children.block) !== undefined; - const statement = call(path, print, "statement"); - forStatement.push(hasStatementBlock ? [" ", statement] : indent([line, statement])); - } - return group(forStatement); - }, - breakStatement(path, print) { - return path.node.children.Identifier - ? ["break ", call(path, print, "Identifier"), ";"] - : "break;"; - }, - continueStatement(path, print) { - return path.node.children.Identifier - ? ["continue ", call(path, print, "Identifier"), ";"] - : "continue;"; - }, - returnStatement(path, print) { - const { children } = path.node; - const statement = ["return"]; - if (children.expression) { - statement.push(" "); - const expression = call(path, print, "expression"); - if (isBinaryExpression(children.expression[0])) { - statement.push(group([ - ifBreak("("), - indent([softline, expression]), - softline, - ifBreak(")") - ])); - } - else { - statement.push(expression); - } - } - statement.push(";"); - return statement; - }, - throwStatement(path, print) { - return ["throw ", call(path, print, "expression"), ";"]; - }, - synchronizedStatement(path, print) { - return [ - "synchronized ", - indentInParentheses(call(path, print, "expression")), - " ", - call(path, print, "block") - ]; - }, - tryStatement(path, print) { - const { children } = path.node; - if (children.tryWithResourcesStatement) { - return call(path, print, "tryWithResourcesStatement"); - } - const blocks = ["try", call(path, print, "block")]; - if (children.catches) { - blocks.push(call(path, print, "catches")); - } - if (children.finally) { - blocks.push(call(path, print, "finally")); - } - return join(" ", blocks); - }, - catches(path, print) { - return join(" ", map(path, print, "catchClause")); - }, - catchClause(path, print) { - return [ - "catch ", - indentInParentheses(call(path, print, "catchFormalParameter")), - " ", - call(path, print, "block") - ]; - }, - catchFormalParameter(path, print) { - return join(" ", [ - ...map(path, print, "variableModifier"), - call(path, print, "catchType"), - call(path, print, "variableDeclaratorId") - ]); - }, - catchType(path, print) { - return join([line, "| "], [call(path, print, "unannClassType"), ...map(path, print, "classType")]); - }, - finally(path, print) { - return ["finally ", call(path, print, "block")]; - }, - tryWithResourcesStatement(path, print) { - const { children } = path.node; - const blocks = [ - "try", - call(path, print, "resourceSpecification"), - call(path, print, "block") - ]; - if (children.catches) { - blocks.push(call(path, print, "catches")); - } - if (children.finally) { - blocks.push(call(path, print, "finally")); - } - return join(" ", blocks); - }, - resourceSpecification(path, print) { - const resources = [call(path, print, "resourceList")]; - if (path.node.children.Semicolon) { - resources.push(ifBreak(";")); - } - return indentInParentheses(resources); - }, - resourceList(path, print) { - return join([";", line], map(path, print, "resource")); - }, - resource: printSingle, - yieldStatement(path, print) { - return ["yield ", call(path, print, "expression"), ";"]; - }, - variableAccess: printSingle -}; diff --git a/frontend/src/common/prettier/plugins/java/printers/classes.d.ts b/frontend/src/common/prettier/plugins/java/printers/classes.d.ts deleted file mode 100644 index dae34f5e..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/classes.d.ts +++ /dev/null @@ -1,157 +0,0 @@ -import type { ClassBodyCstNode, EnumBodyDeclarationsCstNode } from "java-parser"; -import type { AstPath } from "prettier"; -import { builders } from "prettier/doc"; -import { printClassPermits, printClassType, printSingle, type JavaPrintFn } from "./helpers.js"; -declare const _default: { - classDeclaration(path: AstPath, print: JavaPrintFn): builders.Doc[]; - normalClassDeclaration(path: AstPath, print: JavaPrintFn): builders.Doc[]; - classModifier: typeof printSingle; - typeParameters(path: AstPath, print: JavaPrintFn): builders.Group; - typeParameterList(path: AstPath, print: JavaPrintFn): builders.Doc[]; - classExtends(path: AstPath, print: JavaPrintFn): builders.Doc[]; - classImplements(path: AstPath, print: JavaPrintFn): builders.Group; - classPermits: typeof printClassPermits; - interfaceTypeList(path: AstPath, print: JavaPrintFn): builders.Group; - classBody(path: AstPath, print: JavaPrintFn): builders.Group | "{}" | (string | builders.Indent | builders.Hardline)[]; - classBodyDeclaration: typeof printSingle; - classMemberDeclaration(path: AstPath, print: JavaPrintFn): builders.Doc; - fieldDeclaration(path: AstPath, print: JavaPrintFn): builders.Doc[]; - fieldModifier: typeof printSingle; - variableDeclaratorList(path: AstPath, print: JavaPrintFn): builders.Group | builders.Doc[]; - variableDeclarator(path: AstPath, print: JavaPrintFn): builders.Doc; - variableDeclaratorId(path: AstPath, print: JavaPrintFn): builders.Doc; - variableInitializer: typeof printSingle; - unannType: typeof printSingle; - unannPrimitiveTypeWithOptionalDimsSuffix(path: AstPath, print: JavaPrintFn): builders.Doc; - unannPrimitiveType: typeof printSingle; - unannReferenceType(path: AstPath, print: JavaPrintFn): builders.Doc; - unannClassOrInterfaceType: typeof printSingle; - unannClassType: typeof printClassType; - unannInterfaceType: typeof printSingle; - unannTypeVariable: typeof printSingle; - methodDeclaration(path: AstPath, print: JavaPrintFn): builders.Doc[]; - methodModifier: typeof printSingle; - methodHeader(path: AstPath, print: JavaPrintFn): builders.Group; - result: typeof printSingle; - methodDeclarator(path: AstPath, print: JavaPrintFn): builders.Doc[]; - receiverParameter(path: AstPath, print: JavaPrintFn): builders.Doc[]; - formalParameterList(path: AstPath, print: JavaPrintFn): builders.Doc[]; - formalParameter: typeof printSingle; - variableParaRegularParameter(path: AstPath, print: JavaPrintFn): builders.Doc[]; - variableArityParameter(path: AstPath, print: JavaPrintFn): builders.Doc[]; - variableModifier: typeof printSingle; - throws(path: AstPath, print: JavaPrintFn): builders.Doc[]; - exceptionTypeList(path: AstPath, print: JavaPrintFn): builders.Doc[]; - exceptionType: typeof printSingle; - methodBody: typeof printSingle; - instanceInitializer: typeof printSingle; - staticInitializer(path: AstPath, print: JavaPrintFn): builders.Doc[]; - constructorDeclaration(path: AstPath, print: JavaPrintFn): builders.Doc[]; - constructorModifier: typeof printSingle; - constructorDeclarator(path: AstPath, print: JavaPrintFn): builders.Doc[]; - simpleTypeName: typeof printSingle; - constructorBody(path: AstPath, print: JavaPrintFn): builders.Group | "{}" | (string | builders.Indent | builders.Hardline)[]; - explicitConstructorInvocation: typeof printSingle; - unqualifiedExplicitConstructorInvocation(path: AstPath, print: JavaPrintFn): builders.Doc[]; - qualifiedExplicitConstructorInvocation(path: AstPath, print: JavaPrintFn): builders.Doc[]; - enumDeclaration(path: AstPath, print: JavaPrintFn): builders.Doc[]; - enumBody(path: AstPath, print: JavaPrintFn, options: import("./helpers.js").JavaParserOptions): builders.Group | "{}" | (string | builders.Indent | builders.Hardline)[]; - enumConstantList(path: AstPath, print: JavaPrintFn): builders.Doc[]; - enumConstant(path: AstPath, print: JavaPrintFn): builders.Doc[]; - enumConstantModifier: typeof printSingle; - enumBodyDeclarations(path: AstPath, print: JavaPrintFn): builders.Doc[]; - recordDeclaration(path: AstPath, print: JavaPrintFn): builders.Doc[]; - recordHeader(path: AstPath, print: JavaPrintFn): builders.Group | "()"; - recordComponentList(path: AstPath, print: JavaPrintFn): builders.Doc[]; - recordComponent(path: AstPath, print: JavaPrintFn): builders.Group; - variableArityRecordComponent(path: AstPath, print: JavaPrintFn): builders.Doc[]; - recordComponentModifier: typeof printSingle; - recordBody(path: AstPath, print: JavaPrintFn): builders.Group | "{}" | (string | builders.Indent | builders.Hardline)[]; - recordBodyDeclaration: typeof printSingle; - compactConstructorDeclaration(path: AstPath, print: JavaPrintFn): builders.Doc[]; -}; -export default _default; diff --git a/frontend/src/common/prettier/plugins/java/printers/classes.js b/frontend/src/common/prettier/plugins/java/printers/classes.js deleted file mode 100644 index b69b51d2..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/classes.js +++ /dev/null @@ -1,446 +0,0 @@ -import { builders } from "prettier/doc"; -import { call, each, hasDeclarationAnnotations, hasLeadingComments, indentInParentheses, isBinaryExpression, lineEndWithComments, lineStartWithComments, map, onlyDefinedKey, printBlock, printClassPermits, printClassType, printDanglingComments, printList, printSingle, printWithModifiers } from "./helpers.js"; -const { group, hardline, indent, indentIfBreak, join, line, softline } = builders; -export default { - classDeclaration(path, print) { - const declarationKey = onlyDefinedKey(path.node.children, [ - "enumDeclaration", - "normalClassDeclaration", - "recordDeclaration" - ]); - const declaration = call(path, print, declarationKey); - return printWithModifiers(path, print, "classModifier", declaration, true); - }, - normalClassDeclaration(path, print) { - const { classExtends, classImplements, classPermits, typeParameters } = path.node.children; - const header = ["class ", call(path, print, "typeIdentifier")]; - if (typeParameters) { - header.push(call(path, print, "typeParameters")); - } - if (classExtends) { - header.push(indent([line, call(path, print, "classExtends")])); - } - if (classImplements) { - header.push(indent([line, call(path, print, "classImplements")])); - } - if (classPermits) { - header.push(indent([line, call(path, print, "classPermits")])); - } - return [group(header), " ", call(path, print, "classBody")]; - }, - classModifier: printSingle, - typeParameters(path, print) { - return group([ - "<", - indent([softline, call(path, print, "typeParameterList")]), - softline, - ">" - ]); - }, - typeParameterList(path, print) { - return printList(path, print, "typeParameter"); - }, - classExtends(path, print) { - return ["extends ", call(path, print, "classType")]; - }, - classImplements(path, print) { - return group([ - "implements", - indent([line, call(path, print, "interfaceTypeList")]) - ]); - }, - classPermits: printClassPermits, - interfaceTypeList(path, print) { - return group(printList(path, print, "interfaceType")); - }, - classBody(path, print) { - return printBlock(path, printClassBodyDeclarations(path, print)); - }, - classBodyDeclaration: printSingle, - classMemberDeclaration(path, print) { - const { children } = path.node; - return children.Semicolon - ? "" - : call(path, print, onlyDefinedKey(children)); - }, - fieldDeclaration(path, print) { - const declaration = [ - call(path, print, "unannType"), - " ", - call(path, print, "variableDeclaratorList"), - ";" - ]; - return printWithModifiers(path, print, "fieldModifier", declaration); - }, - fieldModifier: printSingle, - variableDeclaratorList(path, print) { - var _a; - const declarators = map(path, print, "variableDeclarator"); - return declarators.length > 1 && - path.node.children.variableDeclarator.some(({ children }) => children.Equals) - ? group(indent(join([",", line], declarators)), { - shouldBreak: ((_a = path.getNode(4)) === null || _a === void 0 ? void 0 : _a.name) !== "forInit" - }) - : join(", ", declarators); - }, - variableDeclarator(path, print) { - var _a, _b; - const { children } = path.node; - const variableInitializer = (_a = children.variableInitializer) === null || _a === void 0 ? void 0 : _a[0]; - const declaratorId = call(path, print, "variableDeclaratorId"); - if (!variableInitializer) { - return declaratorId; - } - const expression = (_b = variableInitializer.children.expression) === null || _b === void 0 ? void 0 : _b[0]; - const declarator = [declaratorId, " ", call(path, print, "Equals")]; - const initializer = call(path, print, "variableInitializer"); - if (hasLeadingComments(variableInitializer) || - (expression && isBinaryExpression(expression))) { - declarator.push(group(indent([line, initializer]))); - } - else { - const groupId = Symbol("assignment"); - declarator.push(group(indent(line), { id: groupId }), indentIfBreak(initializer, { groupId })); - } - return group(declarator); - }, - variableDeclaratorId(path, print) { - const { dims, Underscore } = path.node.children; - if (Underscore) { - return "_"; - } - const identifier = call(path, print, "Identifier"); - return dims ? [identifier, call(path, print, "dims")] : identifier; - }, - variableInitializer: printSingle, - unannType: printSingle, - unannPrimitiveTypeWithOptionalDimsSuffix(path, print) { - const type = call(path, print, "unannPrimitiveType"); - return path.node.children.dims ? [type, call(path, print, "dims")] : type; - }, - unannPrimitiveType: printSingle, - unannReferenceType(path, print) { - const type = call(path, print, "unannClassOrInterfaceType"); - return path.node.children.dims ? [type, call(path, print, "dims")] : type; - }, - unannClassOrInterfaceType: printSingle, - unannClassType: printClassType, - unannInterfaceType: printSingle, - unannTypeVariable: printSingle, - methodDeclaration(path, print) { - const declaration = [ - call(path, print, "methodHeader"), - path.node.children.methodBody[0].children.Semicolon ? "" : " ", - call(path, print, "methodBody") - ]; - return printWithModifiers(path, print, "methodModifier", declaration); - }, - methodModifier: printSingle, - methodHeader(path, print) { - const { typeParameters, annotation, throws } = path.node.children; - const header = []; - if (typeParameters) { - header.push(call(path, print, "typeParameters")); - } - if (annotation) { - header.push(join(line, map(path, print, "annotation"))); - } - header.push(call(path, print, "result"), call(path, print, "methodDeclarator")); - return throws - ? group([ - ...join(" ", header), - group(indent([line, call(path, print, "throws")])) - ]) - : group(join(" ", header)); - }, - result: printSingle, - methodDeclarator(path, print) { - const { dims, formalParameterList, receiverParameter } = path.node.children; - const declarator = [call(path, print, "Identifier")]; - const parameters = []; - if (receiverParameter) { - parameters.push(call(path, print, "receiverParameter")); - } - if (formalParameterList) { - parameters.push(call(path, print, "formalParameterList")); - } - const items = parameters.length - ? join([",", line], parameters) - : printDanglingComments(path); - declarator.push(items.length ? indentInParentheses(items) : "()"); - if (dims) { - declarator.push(call(path, print, "dims")); - } - return declarator; - }, - receiverParameter(path, print) { - return join(" ", [ - ...map(path, print, "annotation"), - call(path, print, "unannType"), - path.node.children.Identifier - ? [call(path, print, "Identifier"), ".this"] - : "this" - ]); - }, - formalParameterList(path, print) { - return printList(path, print, "formalParameter"); - }, - formalParameter: printSingle, - variableParaRegularParameter(path, print) { - return join(" ", [ - ...map(path, print, "variableModifier"), - call(path, print, "unannType"), - call(path, print, "variableDeclaratorId") - ]); - }, - variableArityParameter(path, print) { - const type = join(" ", [ - ...map(path, print, "variableModifier"), - call(path, print, "unannType"), - ...map(path, print, "annotation") - ]); - return [type, "... ", call(path, print, "Identifier")]; - }, - variableModifier: printSingle, - throws(path, print) { - return ["throws ", call(path, print, "exceptionTypeList")]; - }, - exceptionTypeList(path, print) { - return join(", ", map(path, print, "exceptionType")); - }, - exceptionType: printSingle, - methodBody: printSingle, - instanceInitializer: printSingle, - staticInitializer(path, print) { - return ["static ", call(path, print, "block")]; - }, - constructorDeclaration(path, print) { - const declaration = [call(path, print, "constructorDeclarator")]; - if (path.node.children.throws) { - declaration.push(group(indent([line, call(path, print, "throws")]))); - } - declaration.push(" ", call(path, print, "constructorBody")); - return printWithModifiers(path, print, "constructorModifier", declaration, true); - }, - constructorModifier: printSingle, - constructorDeclarator(path, print) { - const { children } = path.node; - const parameters = []; - if (children.receiverParameter) { - parameters.push(call(path, print, "receiverParameter")); - } - if (children.formalParameterList) { - parameters.push(call(path, print, "formalParameterList")); - } - const header = [call(path, print, "simpleTypeName")]; - header.push(parameters.length - ? indentInParentheses(join([",", line], parameters)) - : "()"); - return children.typeParameters - ? [call(path, print, "typeParameters"), " ", ...header] - : header; - }, - simpleTypeName: printSingle, - constructorBody(path, print) { - const { children } = path.node; - const statements = []; - if (children.explicitConstructorInvocation) { - statements.push(call(path, print, "explicitConstructorInvocation")); - } - if (children.blockStatements) { - statements.push(call(path, print, "blockStatements")); - } - return printBlock(path, statements); - }, - explicitConstructorInvocation: printSingle, - unqualifiedExplicitConstructorInvocation(path, print) { - const { children } = path.node; - const invocation = []; - if (children.typeArguments) { - invocation.push(call(path, print, "typeArguments")); - } - invocation.push(children.Super ? "super" : "this"); - if (children.argumentList) { - invocation.push(group(["(", call(path, print, "argumentList"), ")"])); - } - else { - invocation.push(indentInParentheses(printDanglingComments(path), { shouldBreak: true })); - } - invocation.push(";"); - return invocation; - }, - qualifiedExplicitConstructorInvocation(path, print) { - const { children } = path.node; - const invocation = [call(path, print, "expressionName"), "."]; - if (children.typeArguments) { - invocation.push(call(path, print, "typeArguments")); - } - invocation.push("super"); - if (children.argumentList) { - invocation.push(group(["(", call(path, print, "argumentList"), ")"])); - } - else { - invocation.push(indentInParentheses(printDanglingComments(path), { shouldBreak: true })); - } - invocation.push(";"); - return invocation; - }, - enumDeclaration(path, print) { - const header = ["enum", call(path, print, "typeIdentifier")]; - if (path.node.children.classImplements) { - header.push(call(path, print, "classImplements")); - } - return join(" ", [...header, call(path, print, "enumBody")]); - }, - enumBody(path, print, options) { - var _a; - const { children } = path.node; - const contents = []; - const hasNonEmptyDeclaration = ((_a = children.enumBodyDeclarations) !== null && _a !== void 0 ? _a : []) - .flatMap(({ children }) => { var _a; return (_a = children.classBodyDeclaration) !== null && _a !== void 0 ? _a : []; }) - .some(({ children }) => { var _a; return !((_a = children.classMemberDeclaration) === null || _a === void 0 ? void 0 : _a[0].children.Semicolon); }); - if (children.enumConstantList) { - contents.push(call(path, print, "enumConstantList")); - if (!hasNonEmptyDeclaration && options.trailingComma !== "none") { - contents.push(","); - } - } - if (hasNonEmptyDeclaration) { - contents.push(";", hardline, call(path, print, "enumBodyDeclarations")); - } - return printBlock(path, contents.length ? [contents] : []); - }, - enumConstantList(path, print) { - return join([",", hardline], map(path, constantPath => { - const constant = print(constantPath); - const { node, previous } = constantPath; - return !previous || - lineStartWithComments(node) <= lineEndWithComments(previous) + 1 - ? constant - : [hardline, constant]; - }, "enumConstant")); - }, - enumConstant(path, print) { - const { argumentList, classBody } = path.node.children; - const initializer = [call(path, print, "Identifier")]; - if (argumentList) { - initializer.push(group(["(", call(path, print, "argumentList"), ")"])); - } - if (classBody) { - initializer.push(" ", call(path, print, "classBody")); - } - return printWithModifiers(path, print, "enumConstantModifier", initializer); - }, - enumConstantModifier: printSingle, - enumBodyDeclarations(path, print) { - return join(hardline, printClassBodyDeclarations(path, print)); - }, - recordDeclaration(path, print) { - const { children } = path.node; - const header = ["record ", call(path, print, "typeIdentifier")]; - if (children.typeParameters) { - header.push(call(path, print, "typeParameters")); - } - header.push(call(path, print, "recordHeader")); - if (children.classImplements) { - header.push(" ", call(path, print, "classImplements")); - } - return [group(header), " ", call(path, print, "recordBody")]; - }, - recordHeader(path, print) { - return path.node.children.recordComponentList - ? indentInParentheses(call(path, print, "recordComponentList")) - : indentInParentheses(printDanglingComments(path), { shouldBreak: true }); - }, - recordComponentList(path, print) { - return join([",", line], map(path, componentPath => { - const { node, previous } = componentPath; - const blankLine = previous && - lineStartWithComments(node) > lineEndWithComments(previous) + 1; - const component = print(componentPath); - return blankLine ? [softline, component] : component; - }, "recordComponent")); - }, - recordComponent(path, print) { - const { children } = path.node; - const component = [call(path, print, "unannType")]; - if (children.Identifier || - children.variableArityRecordComponent[0].children.annotation) { - component.push(" "); - } - const suffixKey = onlyDefinedKey(children, [ - "Identifier", - "variableArityRecordComponent" - ]); - component.push(call(path, print, suffixKey)); - return group(join(line, [...map(path, print, "recordComponentModifier"), component])); - }, - variableArityRecordComponent(path, print) { - return [ - ...join(" ", map(path, print, "annotation")), - "... ", - call(path, print, "Identifier") - ]; - }, - recordComponentModifier: printSingle, - recordBody(path, print) { - const declarations = []; - let previousRequiresPadding = false; - each(path, declarationPath => { - var _a, _b, _c, _d; - const declaration = print(declarationPath); - if (declaration === "") { - return; - } - const { node, previous } = declarationPath; - const fieldDeclaration = (_c = (_b = (_a = node.children.classBodyDeclaration) === null || _a === void 0 ? void 0 : _a[0].children.classMemberDeclaration) === null || _b === void 0 ? void 0 : _b[0].children.fieldDeclaration) === null || _c === void 0 ? void 0 : _c[0].children; - const currentRequiresPadding = !fieldDeclaration || - hasDeclarationAnnotations((_d = fieldDeclaration.fieldModifier) !== null && _d !== void 0 ? _d : []); - const blankLine = declarations.length > 0 && - (previousRequiresPadding || - currentRequiresPadding || - lineStartWithComments(node) > lineEndWithComments(previous) + 1); - declarations.push(blankLine ? [hardline, declaration] : declaration); - previousRequiresPadding = currentRequiresPadding; - }, "recordBodyDeclaration"); - return printBlock(path, declarations); - }, - recordBodyDeclaration: printSingle, - compactConstructorDeclaration(path, print) { - const declaration = [ - call(path, print, "simpleTypeName"), - " ", - call(path, print, "constructorBody") - ]; - return printWithModifiers(path, print, "constructorModifier", declaration, true); - } -}; -function printClassBodyDeclarations(path, print) { - var _a; - if (!path.node.children.classBodyDeclaration) { - return []; - } - const declarations = []; - let previousRequiresPadding = path.node.name === "enumBodyDeclarations" || - ((_a = path.grandparent) === null || _a === void 0 ? void 0 : _a.name) === - "normalClassDeclaration"; - each(path, declarationPath => { - var _a, _b, _c; - const declaration = print(declarationPath); - if (declaration === "") { - return; - } - const { node, previous } = declarationPath; - const fieldDeclaration = (_b = (_a = node.children.classMemberDeclaration) === null || _a === void 0 ? void 0 : _a[0].children.fieldDeclaration) === null || _b === void 0 ? void 0 : _b[0].children; - const currentRequiresPadding = fieldDeclaration - ? hasDeclarationAnnotations((_c = fieldDeclaration.fieldModifier) !== null && _c !== void 0 ? _c : []) - : true; - const blankLine = previousRequiresPadding || - (declarations.length > 0 && - (currentRequiresPadding || - lineStartWithComments(node) > lineEndWithComments(previous) + 1)); - declarations.push(blankLine ? [hardline, declaration] : declaration); - previousRequiresPadding = currentRequiresPadding; - }, "classBodyDeclaration"); - return declarations; -} diff --git a/frontend/src/common/prettier/plugins/java/printers/expressions.d.ts b/frontend/src/common/prettier/plugins/java/printers/expressions.d.ts deleted file mode 100644 index 29eff550..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/expressions.d.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type { StringTemplateCstNode, TextBlockTemplateCstNode } from "java-parser"; -import type { AstPath } from "prettier"; -import { builders } from "prettier/doc"; -import type { JavaComment } from "../comments.js"; -import { printSingle, type JavaPrintFn } from "./helpers.js"; -declare const _default: { - expression: typeof printSingle; - lambdaExpression(path: AstPath, print: JavaPrintFn, _: import("./helpers.js").JavaParserOptions, args?: unknown): builders.Doc[]; - lambdaParameters(path: AstPath, print: JavaPrintFn, options: import("./helpers.js").JavaParserOptions): builders.Doc; - lambdaParametersWithBraces(path: AstPath, print: JavaPrintFn, options: import("./helpers.js").JavaParserOptions): builders.Doc; - lambdaParameterList: typeof printSingle; - conciseLambdaParameterList(path: AstPath, print: JavaPrintFn): builders.Doc[]; - normalLambdaParameterList(path: AstPath, print: JavaPrintFn): builders.Doc[]; - normalLambdaParameter: typeof printSingle; - regularLambdaParameter(path: AstPath, print: JavaPrintFn): builders.Doc[]; - lambdaParameterType: typeof printSingle; - conciseLambdaParameter: typeof printSingle; - lambdaBody: typeof printSingle; - conditionalExpression(path: AstPath, print: JavaPrintFn): builders.Doc; - binaryExpression(path: AstPath, print: JavaPrintFn, options: import("./helpers.js").JavaParserOptions): builders.Doc; - unaryExpression(path: AstPath, print: JavaPrintFn): builders.Doc[]; - unaryExpressionNotPlusMinus(path: AstPath, print: JavaPrintFn): builders.Doc[]; - primary(path: AstPath, print: JavaPrintFn): builders.Doc; - primaryPrefix: typeof printSingle; - primarySuffix(path: AstPath, print: JavaPrintFn): builders.Doc; - fqnOrRefType(path: AstPath, print: JavaPrintFn, _: import("./helpers.js").JavaParserOptions, args: unknown): builders.Doc[]; - fqnOrRefTypePartFirst(path: AstPath, print: JavaPrintFn): builders.Doc[]; - fqnOrRefTypePartRest(path: AstPath, print: JavaPrintFn): builders.Doc[]; - fqnOrRefTypePartCommon(path: AstPath, print: JavaPrintFn): builders.Doc; - parenthesisExpression(path: AstPath, print: JavaPrintFn): builders.Group | "()" | (string | builders.Indent)[]; - castExpression: typeof printSingle; - primitiveCastExpression(path: AstPath, print: JavaPrintFn): builders.Doc[]; - referenceTypeCastExpression(path: AstPath, print: JavaPrintFn): builders.Doc[]; - newExpression: typeof printSingle; - unqualifiedClassInstanceCreationExpression(path: AstPath, print: JavaPrintFn): builders.Doc[]; - classOrInterfaceTypeToInstantiate(path: AstPath, print: JavaPrintFn): builders.Doc[]; - typeArgumentsOrDiamond: typeof printSingle; - diamond(): string; - methodInvocationSuffix(path: AstPath, print: JavaPrintFn): builders.Group | "()"; - argumentList(path: AstPath, print: JavaPrintFn): builders.Group | (builders.Indent | builders.Softline)[] | (builders.BreakParent | builders.Group)[]; - arrayCreationExpression(path: AstPath, print: JavaPrintFn): builders.Doc[]; - arrayCreationExpressionWithoutInitializerSuffix(path: AstPath, print: JavaPrintFn): builders.Doc; - arrayCreationWithInitializerSuffix(path: AstPath, print: JavaPrintFn): builders.Doc[]; - dimExprs(path: AstPath, print: JavaPrintFn): builders.Doc[]; - dimExpr(path: AstPath, print: JavaPrintFn): builders.Doc[]; - classLiteralSuffix(path: AstPath, print: JavaPrintFn): builders.Doc[]; - arrayAccessSuffix(path: AstPath, print: JavaPrintFn): builders.Doc[]; - methodReferenceSuffix(path: AstPath, print: JavaPrintFn): builders.Doc[]; - templateArgument: typeof printSingle; - template: typeof printSingle; - stringTemplate(path: AstPath, print: JavaPrintFn): builders.Indent; - textBlockTemplate(path: AstPath, print: JavaPrintFn): builders.Indent; - embeddedExpression: typeof printSingle; - pattern: typeof printSingle; - typePattern: typeof printSingle; - recordPattern(path: AstPath, print: JavaPrintFn): builders.Doc[]; - componentPatternList(path: AstPath, print: JavaPrintFn): builders.Doc[]; - componentPattern: typeof printSingle; - matchAllPattern: typeof printSingle; - guard(path: AstPath, print: JavaPrintFn): builders.Doc[]; -}; -export default _default; diff --git a/frontend/src/common/prettier/plugins/java/printers/expressions.js b/frontend/src/common/prettier/plugins/java/printers/expressions.js deleted file mode 100644 index 9f3fa458..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/expressions.js +++ /dev/null @@ -1,598 +0,0 @@ -import { builders, utils } from "prettier/doc"; -import { call, definedKeys, each, findBaseIndent, flatMap, hasLeadingComments, indentInParentheses, isBinaryExpression, isNonTerminal, isTerminal, map, onlyDefinedKey, printDanglingComments, printList, printName, printSingle } from "./helpers.js"; -const { breakParent, conditionalGroup, group, hardline, ifBreak, indent, indentIfBreak, join, line, lineSuffixBoundary, softline } = builders; -const { removeLines, willBreak } = utils; -export default { - expression: printSingle, - lambdaExpression(path, print, _, args = {}) { - var _a; - const hug = (_a = args.hug) !== null && _a !== void 0 ? _a : false; - const parameters = call(path, print, "lambdaParameters"); - const expression = [hug ? removeLines(parameters) : parameters, " ->"]; - const lambdaExpression = path.node.children.lambdaBody[0].children.expression; - const body = call(path, print, "lambdaBody"); - if (lambdaExpression) { - const suffix = indent([line, body]); - expression.push(group(hug ? [suffix, softline] : suffix)); - } - else { - expression.push(" ", body); - } - return expression; - }, - lambdaParameters(path, print, options) { - const parameters = printSingle(path, print); - return !path.node.children.lambdaParametersWithBraces && - options.arrowParens === "always" - ? ["(", parameters, ")"] - : parameters; - }, - lambdaParametersWithBraces(path, print, options) { - var _a; - const { lambdaParameterList } = path.node.children; - if (!lambdaParameterList) { - return "()"; - } - const { conciseLambdaParameterList, normalLambdaParameterList } = lambdaParameterList[0].children; - const parameterCount = ((_a = conciseLambdaParameterList === null || conciseLambdaParameterList === void 0 ? void 0 : conciseLambdaParameterList[0].children.conciseLambdaParameter) !== null && _a !== void 0 ? _a : normalLambdaParameterList === null || normalLambdaParameterList === void 0 ? void 0 : normalLambdaParameterList[0].children.normalLambdaParameter).length; - const parameters = call(path, print, "lambdaParameterList"); - if (parameterCount > 1) { - return indentInParentheses(parameters); - } - return conciseLambdaParameterList && options.arrowParens === "avoid" - ? parameters - : ["(", parameters, ")"]; - }, - lambdaParameterList: printSingle, - conciseLambdaParameterList(path, print) { - return printList(path, print, "conciseLambdaParameter"); - }, - normalLambdaParameterList(path, print) { - return printList(path, print, "normalLambdaParameter"); - }, - normalLambdaParameter: printSingle, - regularLambdaParameter(path, print) { - return join(" ", [ - ...map(path, print, "variableModifier"), - call(path, print, "lambdaParameterType"), - call(path, print, "variableDeclaratorId") - ]); - }, - lambdaParameterType: printSingle, - conciseLambdaParameter: printSingle, - lambdaBody: printSingle, - conditionalExpression(path, print) { - var _a; - const binaryExpression = call(path, print, "binaryExpression"); - if (!path.node.children.QuestionMark) { - return binaryExpression; - } - const expressions = map(path, print, "expression"); - const contents = indent(join(line, [ - binaryExpression, - ["? ", expressions[0]], - [": ", expressions[1]] - ])); - const isNestedTernary = ((_a = path.getNode(4)) === null || _a === void 0 ? void 0 : _a.name) === - "conditionalExpression"; - return isNestedTernary ? contents : group(contents); - }, - binaryExpression(path, print, options) { - var _a, _b; - const { children } = path.node; - const operands = flatMap(path, print, definedKeys(children, [ - "expression", - "pattern", - "referenceType", - "unaryExpression" - ])); - const operators = flatMap(path, operatorPath => { - const { node } = operatorPath; - let image; - if (isTerminal(node)) { - image = node.image; - } - else if (node.children.Less) { - image = "<<"; - } - else { - image = node.children.Greater.length === 2 ? ">>" : ">>>"; - } - return { image, doc: print(operatorPath) }; - }, definedKeys(children, [ - "AssignmentOperator", - "BinaryOperator", - "Instanceof", - "shiftOperator" - ])); - const hasNonAssignmentOperators = (operators.length > 0 && !children.AssignmentOperator) || - (children.expression !== undefined && - isBinaryExpression(children.expression[0])); - const isInList = ((_a = path.getNode(4)) === null || _a === void 0 ? void 0 : _a.name) === "elementValue" || - ((_b = path.getNode(6)) === null || _b === void 0 ? void 0 : _b.name) === "argumentList"; - return binary(operands, operators, { - hasNonAssignmentOperators, - isInList, - isRoot: true, - operatorPosition: options.experimentalOperatorPosition - }); - }, - unaryExpression(path, print) { - return [ - ...map(path, print, "UnaryPrefixOperator"), - call(path, print, "primary"), - ...map(path, print, "UnarySuffixOperator") - ]; - }, - unaryExpressionNotPlusMinus(path, print) { - const { children } = path.node; - const expression = []; - if (children.UnaryPrefixOperatorNotPlusMinus) { - expression.push(...map(path, print, "UnaryPrefixOperatorNotPlusMinus")); - } - expression.push(call(path, print, "primary")); - if (children.UnarySuffixOperator) { - expression.push(...map(path, print, "UnarySuffixOperator")); - } - return join(" ", expression); - }, - primary(path, print) { - var _a, _b; - const { children } = path.node; - if (!children.primarySuffix) { - return call(path, print, "primaryPrefix"); - } - const methodInvocations = children.primarySuffix - .filter(({ children }) => children.methodInvocationSuffix) - .map(({ children }) => children.methodInvocationSuffix[0].children); - const hasLambdaMethodParameter = methodInvocations.some(({ argumentList }) => argumentList === null || argumentList === void 0 ? void 0 : argumentList[0].children.expression.some(({ children }) => children.lambdaExpression)); - const prefixIsCallExpression = children.primaryPrefix[0].children.newExpression; - const callExpressionCount = methodInvocations.length + - (prefixIsCallExpression ? 1 : 0) + - children.primarySuffix.filter(({ children }) => children.unqualifiedClassInstanceCreationExpression).length; - const fqnOrRefType = (_a = children.primaryPrefix[0].children.fqnOrRefType) === null || _a === void 0 ? void 0 : _a[0].children; - const prefixIsMethodInvocation = (fqnOrRefType === null || fqnOrRefType === void 0 ? void 0 : fqnOrRefType.fqnOrRefTypePartRest) !== undefined && - ((_b = children.primarySuffix) === null || _b === void 0 ? void 0 : _b[0].children.methodInvocationSuffix) !== undefined; - const prefixIsStaticMethodInvocation = prefixIsMethodInvocation && isCapitalizedIdentifier(fqnOrRefType); - const prefixIsInstanceMethodInvocation = prefixIsMethodInvocation && !prefixIsStaticMethodInvocation; - const mustBreakForCallExpressions = methodInvocations.length > 2 && hasLambdaMethodParameter; - const separator = mustBreakForCallExpressions ? hardline : softline; - const prefix = [ - call(path, prefixPath => print(prefixPath, { - lastSeparator: prefixIsStaticMethodInvocation || - (prefixIsInstanceMethodInvocation && callExpressionCount === 1) - ? "" - : separator - }), "primaryPrefix") - ]; - const canBreakForCallExpressions = callExpressionCount > 2 || - (callExpressionCount === 2 && prefixIsInstanceMethodInvocation) || - willBreak(prefix); - const suffixes = []; - each(path, suffixPath => { - const { node, previous } = suffixPath; - const suffix = print(suffixPath); - if (node.children.Dot) { - if ((canBreakForCallExpressions && - ((!previous && prefixIsCallExpression) || - (previous === null || previous === void 0 ? void 0 : previous.children.methodInvocationSuffix) || - (previous === null || previous === void 0 ? void 0 : previous.children.unqualifiedClassInstanceCreationExpression))) || - (!node.children.templateArgument && willBreak(suffix))) { - suffixes.push(separator); - } - suffixes.push(suffix); - } - else if (previous) { - suffixes.push(suffix); - } - else { - prefix.push(prefixIsInstanceMethodInvocation && callExpressionCount >= 2 - ? indent(suffix) - : suffix); - } - }, "primarySuffix"); - const hasSuffixComments = children.primarySuffix.some(suffix => hasLeadingComments(suffix)); - return group(canBreakForCallExpressions || hasSuffixComments - ? [prefix, indent(suffixes)] - : [prefix, ...suffixes]); - }, - primaryPrefix: printSingle, - primarySuffix(path, print) { - const { children } = path.node; - if (!children.Dot) { - return printSingle(path, print); - } - const suffix = ["."]; - if (children.This) { - suffix.push("this"); - } - else if (children.Identifier) { - if (children.typeArguments) { - suffix.push(call(path, print, "typeArguments")); - } - suffix.push(call(path, print, "Identifier")); - } - else { - const suffixKey = onlyDefinedKey(children, [ - "templateArgument", - "unqualifiedClassInstanceCreationExpression" - ]); - suffix.push(call(path, print, suffixKey)); - } - return suffix; - }, - fqnOrRefType(path, print, _, args) { - var _a; - const lastSeparator = (_a = args.lastSeparator) !== null && _a !== void 0 ? _a : ""; - const fqnOrRefType = [ - call(path, print, "fqnOrRefTypePartFirst"), - ...map(path, partPath => { - const part = print(partPath); - return partPath.isLast - ? [willBreak(part) ? hardline : lastSeparator, part] - : part; - }, "fqnOrRefTypePartRest") - ]; - fqnOrRefType.push(indent(fqnOrRefType.pop())); - return path.node.children.dims - ? [fqnOrRefType, call(path, print, "dims")] - : fqnOrRefType; - }, - fqnOrRefTypePartFirst(path, print) { - return join(" ", [ - ...map(path, print, "annotation"), - call(path, print, "fqnOrRefTypePartCommon") - ]); - }, - fqnOrRefTypePartRest(path, print) { - const common = call(path, print, "fqnOrRefTypePartCommon"); - const type = path.node.children.typeArguments - ? [call(path, print, "typeArguments"), common] - : common; - return [".", ...join(" ", [...map(path, print, "annotation"), type])]; - }, - fqnOrRefTypePartCommon(path, print) { - const { children } = path.node; - const keywordKey = onlyDefinedKey(children, ["Identifier", "Super"]); - const keyword = call(path, print, keywordKey); - return children.typeArguments - ? [keyword, call(path, print, "typeArguments")] - : keyword; - }, - parenthesisExpression(path, print) { - var _a; - const expression = call(path, print, "expression"); - const ancestorName = (_a = path.getNode(14)) === null || _a === void 0 ? void 0 : _a.name; - const binaryExpression = path.getNode(8); - return ancestorName && - ["guard", "returnStatement"].includes(ancestorName) && - binaryExpression && - binaryExpression.name === "binaryExpression" && - Object.keys(binaryExpression.children).length === 1 - ? indentInParentheses(expression) - : ["(", indent(expression), ")"]; - }, - castExpression: printSingle, - primitiveCastExpression(path, print) { - return [ - "(", - call(path, print, "primitiveType"), - ") ", - call(path, print, "unaryExpression") - ]; - }, - referenceTypeCastExpression(path, print) { - const { children } = path.node; - const type = call(path, print, "referenceType"); - const cast = children.additionalBound - ? indentInParentheses(join(line, [type, ...map(path, print, "additionalBound")])) - : ["(", type, ")"]; - const expressionKey = onlyDefinedKey(children, [ - "lambdaExpression", - "unaryExpressionNotPlusMinus" - ]); - return [cast, " ", call(path, print, expressionKey)]; - }, - newExpression: printSingle, - unqualifiedClassInstanceCreationExpression(path, print) { - const { children } = path.node; - const expression = ["new "]; - if (children.typeArguments) { - expression.push(call(path, print, "typeArguments")); - } - expression.push(call(path, print, "classOrInterfaceTypeToInstantiate"), children.argumentList - ? group(["(", call(path, print, "argumentList"), ")"]) - : "()"); - if (children.classBody) { - expression.push(" ", call(path, print, "classBody")); - } - return expression; - }, - classOrInterfaceTypeToInstantiate(path, print) { - const { children } = path.node; - const type = children.annotation - ? flatMap(path, childPath => [ - print(childPath), - isNonTerminal(childPath.node) ? " " : "." - ], ["annotation", "Identifier"]) - : printName(path, print); - if (children.typeArgumentsOrDiamond) { - type.push(call(path, print, "typeArgumentsOrDiamond")); - } - return type; - }, - typeArgumentsOrDiamond: printSingle, - diamond() { - return "<>"; - }, - methodInvocationSuffix(path, print) { - return path.node.children.argumentList - ? group(["(", call(path, print, "argumentList"), ")"]) - : indentInParentheses(printDanglingComments(path), { shouldBreak: true }); - }, - argumentList(path, print) { - var _a, _b, _c, _d; - const expressions = path.node.children.expression; - const lastExpression = expressions.at(-1); - const lastExpressionLambdaBodyExpression = (_b = (_a = lastExpression.children.lambdaExpression) === null || _a === void 0 ? void 0 : _a[0].children.lambdaBody[0].children.expression) === null || _b === void 0 ? void 0 : _b[0].children; - const lastExpressionLambdaBodyTernaryExpression = (_c = lastExpressionLambdaBodyExpression === null || lastExpressionLambdaBodyExpression === void 0 ? void 0 : lastExpressionLambdaBodyExpression.conditionalExpression) === null || _c === void 0 ? void 0 : _c[0].children; - const isHuggable = !lastExpression.comments && - (!lastExpressionLambdaBodyExpression || - (lastExpressionLambdaBodyTernaryExpression === null || lastExpressionLambdaBodyTernaryExpression === void 0 ? void 0 : lastExpressionLambdaBodyTernaryExpression.QuestionMark) !== undefined || - ((_d = lastExpressionLambdaBodyTernaryExpression === null || lastExpressionLambdaBodyTernaryExpression === void 0 ? void 0 : lastExpressionLambdaBodyTernaryExpression.binaryExpression) === null || _d === void 0 ? void 0 : _d[0].children.unaryExpression.length) === 1) && - expressions.findIndex(({ children }) => children.lambdaExpression) === - expressions.length - 1; - const args = map(path, print, "expression"); - const allArgsExpandable = [ - indent([softline, ...join([",", line], args)]), - softline - ]; - if (!isHuggable || willBreak(args.at(-1)[0])) { - return allArgsExpandable; - } - const headArgs = args.slice(0, -1); - const huggedLastArg = path.call(argPath => print(argPath, { hug: true }), "children", "expression", args.length - 1); - const lastArgExpanded = join(", ", [ - ...headArgs, - group(huggedLastArg, { shouldBreak: true }) - ]); - if (willBreak(huggedLastArg)) { - return [ - breakParent, - conditionalGroup([lastArgExpanded, allArgsExpandable]) - ]; - } - return conditionalGroup([ - join(", ", [...headArgs, huggedLastArg]), - lastArgExpanded, - allArgsExpandable - ]); - }, - arrayCreationExpression(path, print) { - const { children } = path.node; - const typeKey = onlyDefinedKey(children, [ - "classOrInterfaceType", - "primitiveType" - ]); - const suffixKey = onlyDefinedKey(children, [ - "arrayCreationExpressionWithoutInitializerSuffix", - "arrayCreationWithInitializerSuffix" - ]); - return ["new ", call(path, print, typeKey), call(path, print, suffixKey)]; - }, - arrayCreationExpressionWithoutInitializerSuffix(path, print) { - const expressions = call(path, print, "dimExprs"); - return path.node.children.dims - ? [expressions, call(path, print, "dims")] - : expressions; - }, - arrayCreationWithInitializerSuffix(path, print) { - return [ - call(path, print, "dims"), - " ", - call(path, print, "arrayInitializer") - ]; - }, - dimExprs(path, print) { - return map(path, print, "dimExpr"); - }, - dimExpr(path, print) { - return join(" ", [ - ...map(path, print, "annotation"), - ["[", call(path, print, "expression"), "]"] - ]); - }, - classLiteralSuffix(path, print) { - const lSquares = map(path, print, "LSquare"); - const rSquares = map(path, print, "RSquare"); - return [ - ...lSquares.flatMap((lSquare, index) => [lSquare, rSquares[index]]), - ".class" - ]; - }, - arrayAccessSuffix(path, print) { - return ["[", call(path, print, "expression"), "]"]; - }, - methodReferenceSuffix(path, print) { - const { children } = path.node; - const reference = ["::"]; - if (children.typeArguments) { - reference.push(call(path, print, "typeArguments")); - } - reference.push(call(path, print, onlyDefinedKey(children, ["Identifier", "New"]))); - return reference; - }, - templateArgument: printSingle, - template: printSingle, - stringTemplate(path, print) { - return printTemplate(path, print, "StringTemplateBegin", "StringTemplateMid", "StringTemplateEnd"); - }, - textBlockTemplate(path, print) { - return printTemplate(path, print, "TextBlockTemplateBegin", "TextBlockTemplateMid", "TextBlockTemplateEnd"); - }, - embeddedExpression: printSingle, - pattern: printSingle, - typePattern: printSingle, - recordPattern(path, print) { - const patterns = path.node.children.componentPatternList - ? indentInParentheses(call(path, print, "componentPatternList")) - : "()"; - return [call(path, print, "referenceType"), patterns]; - }, - componentPatternList(path, print) { - return printList(path, print, "componentPattern"); - }, - componentPattern: printSingle, - matchAllPattern: printSingle, - guard(path, print) { - var _a; - const expression = call(path, print, "expression"); - const hasParentheses = ((_a = path.node.children.expression[0].children.conditionalExpression) === null || _a === void 0 ? void 0 : _a[0].children.binaryExpression[0].children.unaryExpression[0].children.primary[0].children.primaryPrefix[0].children.parenthesisExpression) !== - undefined; - return [ - "when ", - hasParentheses - ? expression - : group([ - ifBreak("("), - indent([softline, expression]), - softline, - ifBreak(")") - ]) - ]; - } -}; -function binary(operands, operators, { hasNonAssignmentOperators = false, isInList = false, isRoot = false, operatorPosition }) { - let levelOperator; - let levelPrecedence; - let level = []; - while (operators.length) { - const nextOperator = operators[0].image; - const nextPrecedence = getOperatorPrecedence(nextOperator); - if (levelPrecedence === undefined || nextPrecedence === levelPrecedence) { - const { image: operator, doc: operatorDoc } = operators.shift(); - level.push(operands.shift()); - if (levelOperator !== undefined && - needsParentheses(levelOperator, operator)) { - level = [["(", group(indent(level)), ")"]]; - } - const parts = [" ", operatorDoc, line]; - if (operatorPosition === "start" && !isAssignmentOperator(operator)) { - parts.reverse(); - } - level.push(parts); - levelOperator = operator; - levelPrecedence = nextPrecedence; - } - else if (nextPrecedence < levelPrecedence) { - if (!isRoot) { - break; - } - level.push(operands.shift()); - const content = group(indent(level)); - operands.unshift(levelOperator !== undefined && - needsParentheses(levelOperator, nextOperator) - ? ["(", content, ")"] - : content); - level = []; - levelOperator = undefined; - levelPrecedence = undefined; - } - else { - const content = binary(operands, operators, { operatorPosition }); - operands.unshift(levelOperator !== undefined && - needsParentheses(nextOperator, levelOperator) - ? ["(", indent(content), ")"] - : content); - } - } - level.push(operands.shift()); - if (!levelOperator || - (!isInList && - !isAssignmentOperator(levelOperator) && - levelOperator !== "instanceof")) { - return group(level); - } - if (!isRoot || hasNonAssignmentOperators) { - return group(indent(level)); - } - const groupId = Symbol("assignment"); - return group([ - level[0], - group(indent(level[1]), { id: groupId }), - indentIfBreak(level[2], { groupId }) - ]); -} -const precedencesByOperator = new Map([ - ["||"], - ["&&"], - ["|"], - ["^"], - ["&"], - ["==", "!="], - ["<", ">", "<=", ">=", "instanceof"], - ["<<", ">>", ">>>"], - ["+", "-"], - ["*", "/", "%"] -].flatMap((operators, index) => operators.map(operator => [operator, index]))); -function getOperatorPrecedence(operator) { - var _a; - return (_a = precedencesByOperator.get(operator)) !== null && _a !== void 0 ? _a : -1; -} -function needsParentheses(operator, parentOperator) { - return ((operator === "&&" && parentOperator === "||") || - (["|", "^", "&", "<<", ">>", ">>>"].includes(parentOperator) && - getOperatorPrecedence(operator) > - getOperatorPrecedence(parentOperator)) || - [operator, parentOperator].every(o => ["==", "!="].includes(o)) || - [operator, parentOperator].every(o => ["<<", ">>", ">>>"].includes(o)) || - (operator === "*" && parentOperator === "/") || - (operator === "/" && parentOperator === "*") || - (operator === "%" && ["+", "-", "*", "/"].includes(parentOperator)) || - (["*", "/"].includes(operator) && parentOperator === "%")); -} -const assignmentOperators = new Set([ - "=", - "*=", - "/=", - "%=", - "+=", - "-=", - "<<=", - ">>=", - ">>>=", - "&=", - "^=", - "|=" -]); -function isAssignmentOperator(operator) { - return assignmentOperators.has(operator); -} -function isCapitalizedIdentifier(fqnOrRefType) { - var _a, _b, _c; - const nextToLastIdentifier = (_c = (_b = [ - fqnOrRefType.fqnOrRefTypePartFirst[0], - ...((_a = fqnOrRefType.fqnOrRefTypePartRest) !== null && _a !== void 0 ? _a : []) - ].at(-2)) === null || _b === void 0 ? void 0 : _b.children.fqnOrRefTypePartCommon[0].children.Identifier) === null || _c === void 0 ? void 0 : _c[0].image; - return /^\p{Uppercase_Letter}/u.test(nextToLastIdentifier !== null && nextToLastIdentifier !== void 0 ? nextToLastIdentifier : ""); -} -function printTemplate(path, print, beginKey, midKey, endKey) { - const begin = call(path, ({ node }) => node.image, beginKey); - const mids = map(path, ({ node }) => node.image, midKey); - const end = call(path, ({ node }) => node.image, endKey); - const lines = [begin, ...mids, end].join("").split("\n").slice(1); - const baseIndent = findBaseIndent(lines); - const prefix = "\n" + " ".repeat(baseIndent); - const parts = [begin, ...mids, end].map(image => join(hardline, image.split(prefix))); - return indent([ - parts[0], - ...map(path, (expressionPath, index) => { - const expression = group([ - indent([softline, print(expressionPath), lineSuffixBoundary]), - softline - ]); - return index === 0 ? expression : [parts[index], expression]; - }, "embeddedExpression"), - parts.at(-1) - ]); -} diff --git a/frontend/src/common/prettier/plugins/java/printers/helpers.d.ts b/frontend/src/common/prettier/plugins/java/printers/helpers.d.ts deleted file mode 100644 index a270d5eb..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/helpers.d.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { AnnotationCstNode, ClassPermitsCstNode, ClassTypeCtx, CstElement, CstNode, ExpressionCstNode, InterfacePermitsCstNode, IToken, StatementCstNode } from "java-parser"; -import type { AstPath, Doc, ParserOptions } from "prettier"; -import { builders } from "prettier/doc"; -import type { JavaComment } from "../comments.js"; -export declare function onlyDefinedKey, K extends Key & string>(obj: T, options?: K[]): K; -export declare function definedKeys, K extends Key & string>(obj: T, options?: K[]): K[]; -export declare function printWithModifiers>(path: AstPath, print: JavaPrintFn, modifierChild: P, contents: Doc, noTypeAnnotations?: boolean): builders.Doc[]; -export declare function hasDeclarationAnnotations(modifiers: ModifierNode[]): boolean; -export declare function call>(path: AstPath, callback: MapCallback, P>, U>, child: P): U; -export declare function each>(path: AstPath, callback: MapCallback, P>, void>, child: P): void; -export declare function map>(path: AstPath, callback: MapCallback, P>, U>, child: P): U[]; -export declare function flatMap>(path: AstPath, callback: MapCallback, P>, U>, children: P[]): U[]; -export declare function printSingle(path: AstPath, print: JavaPrintFn, _?: JavaParserOptions, args?: unknown): builders.Doc; -export declare function lineStartWithComments(node: JavaNonTerminal): number; -export declare function lineEndWithComments(node: JavaNonTerminal): number; -export declare function printDanglingComments(path: AstPath): builders.Doc[]; -export declare function printComment(node: JavaTerminal): string | builders.Doc[]; -export declare function hasLeadingComments(node: JavaNode): boolean | undefined; -export declare function indentInParentheses(contents: Doc, opts?: { - shouldBreak?: boolean; -}): builders.Group | "()"; -export declare function printArrayInitializer>(path: AstPath, print: JavaPrintFn, options: JavaParserOptions, child: P): builders.Group | "{}"; -export declare function printBlock(path: AstPath, contents: Doc[]): builders.Group | "{}" | (string | builders.Indent | builders.Hardline)[]; -export declare function printName(path: AstPath, print: JavaPrintFn): builders.Doc[]; -export declare function printList>(path: AstPath, print: JavaPrintFn, child: P): builders.Doc[]; -export declare function printClassPermits(path: AstPath, print: JavaPrintFn): builders.Group; -export declare function printClassType(path: AstPath, print: JavaPrintFn): builders.Doc[]; -export declare function isBinaryExpression(expression: ExpressionCstNode): boolean; -export declare function findBaseIndent(lines: string[]): number; -export declare function isEmptyStatement(statement: StatementCstNode): boolean; -export declare function isNonTerminal(node: CstElement): node is JavaNonTerminal; -export declare function isTerminal(node: CstElement): node is IToken; -export type JavaNode = CstElement & { - comments?: JavaComment[]; -}; -export type JavaNonTerminal = Exclude; -export type JavaTerminal = Exclude; -export type JavaNodePrinters = { - [T in JavaNonTerminal["name"]]: JavaNodePrinter; -}; -export type JavaNodePrinter = (path: AstPath>, print: JavaPrintFn, options: JavaParserOptions, args?: unknown) => Doc; -export type JavaPrintFn = (path: AstPath, args?: unknown) => Doc; -export type JavaParserOptions = ParserOptions & { - entrypoint?: string; -}; -export type IterProperties = T extends any[] ? IndexProperties : ArrayProperties; -type Key = T extends T ? keyof T : never; -type ModifierNode = JavaNonTerminal & { - children: { - annotation?: AnnotationCstNode[]; - }; -}; -type IsTuple = T extends [] ? true : T extends [infer _First, ...infer Remain] ? IsTuple : false; -type IndexProperties = IsTuple extends true ? Exclude["length"], T["length"]> : number; -type ArrayProperties = { - [K in keyof T]: NonNullable extends readonly any[] ? K : never; -}[keyof T]; -type ArrayElement = T extends Array ? E : never; -type MapCallback = (path: AstPath>, index: number, value: any) => U; -type IndexValue = T extends any[] ? P extends number ? T[P] : never : P extends keyof T ? T[P] : never; -export {}; diff --git a/frontend/src/common/prettier/plugins/java/printers/helpers.js b/frontend/src/common/prettier/plugins/java/printers/helpers.js deleted file mode 100644 index e0072c80..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/helpers.js +++ /dev/null @@ -1,239 +0,0 @@ -import { builders } from "prettier/doc"; -import parser from "../parser.js"; -const { group, hardline, ifBreak, indent, join, line, softline } = builders; -export function onlyDefinedKey(obj, options) { - const keys = definedKeys(obj, options); - if (keys.length === 1) { - return keys[0]; - } - throw new Error(keys.length > 1 - ? `More than one defined key found: ${keys}` - : "No defined keys found"); -} -export function definedKeys(obj, options) { - return (options !== null && options !== void 0 ? options : Object.keys(obj)).filter(key => obj[key] !== undefined); -} -const indexByModifier = [ - "public", - "protected", - "private", - "abstract", - "default", - "static", - "final", - "transient", - "volatile", - "synchronized", - "native", - "sealed", - "non-sealed", - "strictfp" -].reduce((map, name, index) => map.set(name, index), new Map()); -export function printWithModifiers(path, print, modifierChild, contents, noTypeAnnotations = false) { - const declarationAnnotations = []; - const otherModifiers = []; - const typeAnnotations = []; - each(path, modifierPath => { - const { children } = modifierPath.node; - const modifier = print(modifierPath); - if (children.annotation) { - (otherModifiers.length ? typeAnnotations : declarationAnnotations).push(modifier); - } - else { - otherModifiers.push(modifier); - declarationAnnotations.push(...typeAnnotations); - typeAnnotations.length = 0; - } - }, modifierChild); - if (noTypeAnnotations) { - declarationAnnotations.push(...typeAnnotations); - typeAnnotations.length = 0; - } - otherModifiers.sort((a, b) => indexByModifier.get(a) - indexByModifier.get(b)); - return join(hardline, [ - ...declarationAnnotations, - join(" ", [...otherModifiers, ...typeAnnotations, contents]) - ]); -} -export function hasDeclarationAnnotations(modifiers) { - let hasAnnotation = false; - let hasNonAnnotation = false; - for (const modifier of modifiers) { - if (modifier.children.annotation) { - hasAnnotation = true; - } - else if (hasAnnotation) { - return true; - } - else { - hasNonAnnotation = true; - } - } - return hasAnnotation && !hasNonAnnotation; -} -export function call(path, callback, child) { - return path.map(callback, "children", child)[0]; -} -export function each(path, callback, child) { - if (path.node.children[child]) { - path.each(callback, "children", child); - } -} -export function map(path, callback, child) { - return path.node.children[child] ? path.map(callback, "children", child) : []; -} -export function flatMap(path, callback, children) { - return children - .flatMap(child => map(path, callback, child).map((doc, index) => { - const node = path.node.children[child][index]; - return { - doc, - startOffset: parser.locStart(node) - }; - })) - .sort((a, b) => a.startOffset - b.startOffset) - .map(({ doc }) => doc); -} -export function printSingle(path, print, _, args) { - return call(path, childPath => print(childPath, args), onlyDefinedKey(path.node.children)); -} -export function lineStartWithComments(node) { - const { comments, location } = node; - return comments - ? Math.min(location.startLine, comments[0].startLine) - : location.startLine; -} -export function lineEndWithComments(node) { - const { comments, location } = node; - return comments - ? Math.max(location.endLine, comments.at(-1).endLine) - : location.endLine; -} -export function printDanglingComments(path) { - if (!path.node.comments) { - return []; - } - const comments = []; - path.each(commentPath => { - const comment = commentPath.node; - if (comment.leading || comment.trailing) { - return; - } - comment.printed = true; - comments.push(printComment(comment)); - }, "comments"); - return join(hardline, comments); -} -export function printComment(node) { - const { image } = node; - const lines = image.split("\n").map(line => line.trim()); - return lines.length > 1 && - lines[0].startsWith("/*") && - lines.slice(1).every(line => line.startsWith("*")) && - lines.at(-1).endsWith("*/") - ? join(hardline, lines.map((line, index) => (index === 0 ? line : ` ${line}`))) - : image; -} -export function hasLeadingComments(node) { - var _a; - return (_a = node.comments) === null || _a === void 0 ? void 0 : _a.some(({ leading }) => leading); -} -export function indentInParentheses(contents, opts) { - return !Array.isArray(contents) || contents.length - ? group(["(", indent([softline, contents]), softline, ")"], opts) - : "()"; -} -export function printArrayInitializer(path, print, options, child) { - const list = []; - if (child && child in path.node.children) { - list.push(call(path, print, child)); - if (options.trailingComma !== "none") { - list.push(ifBreak(",")); - } - } - list.push(...printDanglingComments(path)); - return list.length ? group(["{", indent([line, ...list]), line, "}"]) : "{}"; -} -export function printBlock(path, contents) { - if (!contents.length) { - const danglingComments = printDanglingComments(path); - return danglingComments.length - ? ["{", indent([hardline, ...danglingComments]), hardline, "}"] - : "{}"; - } - return group([ - "{", - indent([hardline, ...join(hardline, contents)]), - hardline, - "}" - ]); -} -export function printName(path, print) { - return join(".", map(path, print, "Identifier")); -} -export function printList(path, print, child) { - return join([",", line], map(path, print, child)); -} -export function printClassPermits(path, print) { - return group([ - "permits", - indent([line, group(printList(path, print, "typeName"))]) - ]); -} -export function printClassType(path, print) { - const { children } = path.node; - return definedKeys(children, ["annotation", "Identifier", "typeArguments"]) - .flatMap(child => children[child].map((node, index) => ({ - child, - index, - startOffset: parser.locStart(node) - }))) - .sort((a, b) => a.startOffset - b.startOffset) - .flatMap(({ child, index: childIndex }, index, array) => { - const node = children[child][childIndex]; - const next = array.at(index + 1); - const nextNode = next && children[next.child][next.index]; - const docs = [path.call(print, "children", child, childIndex)]; - if (nextNode) { - if (isNonTerminal(node)) { - docs.push(node.name === "annotation" ? " " : "."); - } - else if (isTerminal(nextNode) || nextNode.name === "annotation") { - docs.push("."); - } - } - return docs; - }); -} -export function isBinaryExpression(expression) { - var _a; - const conditionalExpression = (_a = expression.children.conditionalExpression) === null || _a === void 0 ? void 0 : _a[0].children; - if (!conditionalExpression) { - return false; - } - const isTernary = conditionalExpression.QuestionMark !== undefined; - if (isTernary) { - return false; - } - const hasNonAssignmentOperators = Object.values(conditionalExpression.binaryExpression[0].children).some(child => { - var _a; - return isTerminal(child[0]) && - !((_a = child[0].tokenType.CATEGORIES) === null || _a === void 0 ? void 0 : _a.some(category => category.name === "AssignmentOperator")); - }); - return hasNonAssignmentOperators; -} -export function findBaseIndent(lines) { - return lines.length - ? Math.min(...lines.map(line => line.search(/\S/)).filter(indent => indent >= 0)) - : 0; -} -export function isEmptyStatement(statement) { - var _a; - return (((_a = statement.children.statementWithoutTrailingSubstatement) === null || _a === void 0 ? void 0 : _a[0].children.emptyStatement) !== undefined); -} -export function isNonTerminal(node) { - return !isTerminal(node); -} -export function isTerminal(node) { - return "tokenType" in node; -} diff --git a/frontend/src/common/prettier/plugins/java/printers/index.d.ts b/frontend/src/common/prettier/plugins/java/printers/index.d.ts deleted file mode 100644 index 569b54ae..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/index.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -import type { JavaNodePrinter, JavaNodePrinters } from "./helpers.js"; -export declare function printerForNodeType(type: T): JavaNodePrinter; diff --git a/frontend/src/common/prettier/plugins/java/printers/index.js b/frontend/src/common/prettier/plugins/java/printers/index.js deleted file mode 100644 index e629424e..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/index.js +++ /dev/null @@ -1,13 +0,0 @@ -import arrays from "./arrays.js"; -import blocksAndStatements from "./blocks-and-statements.js"; -import classes from "./classes.js"; -import expressions from "./expressions.js"; -import interfaces from "./interfaces.js"; -import lexicalStructure from "./lexical-structure.js"; -import names from "./names.js"; -import packagesAndModules from "./packages-and-modules.js"; -import typesValuesAndVariables from "./types-values-and-variables.js"; -const printersByNodeType = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, arrays), blocksAndStatements), classes), expressions), interfaces), lexicalStructure), names), packagesAndModules), typesValuesAndVariables); -export function printerForNodeType(type) { - return printersByNodeType[type]; -} diff --git a/frontend/src/common/prettier/plugins/java/printers/interfaces.d.ts b/frontend/src/common/prettier/plugins/java/printers/interfaces.d.ts deleted file mode 100644 index cb9025ef..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/interfaces.d.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { builders } from "prettier/doc"; -import { printClassPermits, printSingle } from "./helpers.js"; -declare const _default: { - interfaceDeclaration(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - normalInterfaceDeclaration(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - interfaceModifier: typeof printSingle; - interfaceExtends(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Group; - interfacePermits: typeof printClassPermits; - interfaceBody(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Group | "{}" | (string | builders.Indent | builders.Hardline)[]; - interfaceMemberDeclaration(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc; - constantDeclaration(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - constantModifier: typeof printSingle; - interfaceMethodDeclaration(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - interfaceMethodModifier: typeof printSingle; - annotationInterfaceDeclaration(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - annotationInterfaceBody(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Group | "{}" | (string | builders.Indent | builders.Hardline)[]; - annotationInterfaceMemberDeclaration(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc; - annotationInterfaceElementDeclaration(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - annotationInterfaceElementModifier: typeof printSingle; - defaultValue(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - annotation(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - elementValuePairList(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - elementValuePair(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - elementValue: typeof printSingle; - elementValueArrayInitializer(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn, options: import("./helpers.js").JavaParserOptions): builders.Group | "{}"; - elementValueList(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Group; -}; -export default _default; diff --git a/frontend/src/common/prettier/plugins/java/printers/interfaces.js b/frontend/src/common/prettier/plugins/java/printers/interfaces.js deleted file mode 100644 index 93a5109f..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/interfaces.js +++ /dev/null @@ -1,157 +0,0 @@ -import { builders } from "prettier/doc"; -import { call, each, hasDeclarationAnnotations, indentInParentheses, lineEndWithComments, lineStartWithComments, onlyDefinedKey, printArrayInitializer, printBlock, printClassPermits, printList, printSingle, printWithModifiers } from "./helpers.js"; -const { group, hardline, indent, join, line } = builders; -export default { - interfaceDeclaration(path, print) { - const declarationKey = onlyDefinedKey(path.node.children, [ - "annotationInterfaceDeclaration", - "normalInterfaceDeclaration" - ]); - return printWithModifiers(path, print, "interfaceModifier", call(path, print, declarationKey), true); - }, - normalInterfaceDeclaration(path, print) { - const { interfaceExtends, interfacePermits, typeParameters } = path.node.children; - const header = ["interface ", call(path, print, "typeIdentifier")]; - if (typeParameters) { - header.push(call(path, print, "typeParameters")); - } - if (interfaceExtends) { - header.push(indent([line, call(path, print, "interfaceExtends")])); - } - if (interfacePermits) { - header.push(indent([line, call(path, print, "interfacePermits")])); - } - return [group(header), " ", call(path, print, "interfaceBody")]; - }, - interfaceModifier: printSingle, - interfaceExtends(path, print) { - return group([ - "extends", - indent([line, call(path, print, "interfaceTypeList")]) - ]); - }, - interfacePermits: printClassPermits, - interfaceBody(path, print) { - const declarations = []; - let previousRequiresPadding = false; - each(path, declarationPath => { - var _a, _b, _c, _d; - const declaration = print(declarationPath); - if (declaration === "") { - return; - } - const { node, previous } = declarationPath; - const constantDeclaration = (_a = node.children.constantDeclaration) === null || _a === void 0 ? void 0 : _a[0].children; - const methodDeclaration = (_b = node.children.interfaceMethodDeclaration) === null || _b === void 0 ? void 0 : _b[0].children; - const currentRequiresPadding = (!constantDeclaration && !methodDeclaration) || - (methodDeclaration === null || methodDeclaration === void 0 ? void 0 : methodDeclaration.methodBody[0].children.block) !== undefined || - hasDeclarationAnnotations((_d = (_c = constantDeclaration === null || constantDeclaration === void 0 ? void 0 : constantDeclaration.constantModifier) !== null && _c !== void 0 ? _c : methodDeclaration === null || methodDeclaration === void 0 ? void 0 : methodDeclaration.interfaceMethodModifier) !== null && _d !== void 0 ? _d : []); - const blankLine = declarations.length > 0 && - (previousRequiresPadding || - currentRequiresPadding || - lineStartWithComments(node) > lineEndWithComments(previous) + 1); - declarations.push(blankLine ? [hardline, declaration] : declaration); - previousRequiresPadding = currentRequiresPadding; - }, "interfaceMemberDeclaration"); - return printBlock(path, declarations); - }, - interfaceMemberDeclaration(path, print) { - const { children } = path.node; - return children.Semicolon - ? "" - : call(path, print, onlyDefinedKey(children)); - }, - constantDeclaration(path, print) { - const declaration = [ - call(path, print, "unannType"), - " ", - call(path, print, "variableDeclaratorList"), - ";" - ]; - return printWithModifiers(path, print, "constantModifier", declaration); - }, - constantModifier: printSingle, - interfaceMethodDeclaration(path, print) { - const declaration = [ - call(path, print, "methodHeader"), - path.node.children.methodBody[0].children.Semicolon ? "" : " ", - call(path, print, "methodBody") - ]; - return printWithModifiers(path, print, "interfaceMethodModifier", declaration); - }, - interfaceMethodModifier: printSingle, - annotationInterfaceDeclaration(path, print) { - return join(" ", [ - "@interface", - call(path, print, "typeIdentifier"), - call(path, print, "annotationInterfaceBody") - ]); - }, - annotationInterfaceBody(path, print) { - const declarations = []; - each(path, declarationPath => { - const declaration = print(declarationPath); - if (declaration === "") { - return; - } - declarations.push(declarationPath.isFirst ? declaration : [hardline, declaration]); - }, "annotationInterfaceMemberDeclaration"); - return printBlock(path, declarations); - }, - annotationInterfaceMemberDeclaration(path, print) { - const { children } = path.node; - return children.Semicolon - ? "" - : call(path, print, onlyDefinedKey(children)); - }, - annotationInterfaceElementDeclaration(path, print) { - const { dims, defaultValue } = path.node.children; - const declaration = [ - call(path, print, "unannType"), - " ", - call(path, print, "Identifier"), - "()" - ]; - if (dims) { - declaration.push(call(path, print, "dims")); - } - if (defaultValue) { - declaration.push(" ", call(path, print, "defaultValue")); - } - declaration.push(";"); - return printWithModifiers(path, print, "annotationInterfaceElementModifier", declaration); - }, - annotationInterfaceElementModifier: printSingle, - defaultValue(path, print) { - return ["default ", call(path, print, "elementValue")]; - }, - annotation(path, print) { - const { children } = path.node; - const annotation = ["@", call(path, print, "typeName")]; - if (children.elementValue || children.elementValuePairList) { - const valuesKey = onlyDefinedKey(children, [ - "elementValue", - "elementValuePairList" - ]); - annotation.push(indentInParentheses(call(path, print, valuesKey))); - } - return annotation; - }, - elementValuePairList(path, print) { - return printList(path, print, "elementValuePair"); - }, - elementValuePair(path, print) { - return join(" ", [ - call(path, print, "Identifier"), - call(path, print, "Equals"), - call(path, print, "elementValue") - ]); - }, - elementValue: printSingle, - elementValueArrayInitializer(path, print, options) { - return printArrayInitializer(path, print, options, "elementValueList"); - }, - elementValueList(path, print) { - return group(printList(path, print, "elementValue")); - } -}; diff --git a/frontend/src/common/prettier/plugins/java/printers/lexical-structure.d.ts b/frontend/src/common/prettier/plugins/java/printers/lexical-structure.d.ts deleted file mode 100644 index a63a9e27..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/lexical-structure.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { builders } from "prettier/doc"; -import { printSingle } from "./helpers.js"; -declare const _default: { - literal(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc; - integerLiteral: typeof printSingle; - floatingPointLiteral: typeof printSingle; - booleanLiteral: typeof printSingle; - shiftOperator(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; -}; -export default _default; diff --git a/frontend/src/common/prettier/plugins/java/printers/lexical-structure.js b/frontend/src/common/prettier/plugins/java/printers/lexical-structure.js deleted file mode 100644 index d94cfd59..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/lexical-structure.js +++ /dev/null @@ -1,29 +0,0 @@ -import { builders } from "prettier/doc"; -import { findBaseIndent, map, onlyDefinedKey, printSingle } from "./helpers.js"; -const { hardline, indent, join } = builders; -export default { - literal(path, print) { - const { TextBlock } = path.node.children; - if (!TextBlock) { - return printSingle(path, print); - } - const [open, ...lines] = TextBlock[0].image.split("\n"); - const baseIndent = findBaseIndent(lines); - const textBlock = join(hardline, [ - open, - ...lines.map(line => line.slice(baseIndent)) - ]); - const ancestor = path.getNode(14); - return (ancestor === null || ancestor === void 0 ? void 0 : ancestor.name) === "variableInitializer" || - ((ancestor === null || ancestor === void 0 ? void 0 : ancestor.name) === "binaryExpression" && - ancestor.children.AssignmentOperator) - ? indent(textBlock) - : textBlock; - }, - integerLiteral: printSingle, - floatingPointLiteral: printSingle, - booleanLiteral: printSingle, - shiftOperator(path, print) { - return map(path, print, onlyDefinedKey(path.node.children)); - } -}; diff --git a/frontend/src/common/prettier/plugins/java/printers/names.d.ts b/frontend/src/common/prettier/plugins/java/printers/names.d.ts deleted file mode 100644 index 18ad1e81..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/names.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { printName, printSingle } from "./helpers.js"; -declare const _default: { - typeIdentifier: typeof printSingle; - moduleName: typeof printName; - packageName: typeof printName; - typeName: typeof printName; - expressionName: typeof printName; - methodName: typeof printSingle; - packageOrTypeName: typeof printName; - ambiguousName: typeof printName; -}; -export default _default; diff --git a/frontend/src/common/prettier/plugins/java/printers/names.js b/frontend/src/common/prettier/plugins/java/printers/names.js deleted file mode 100644 index 3dfec2fd..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/names.js +++ /dev/null @@ -1,11 +0,0 @@ -import { printName, printSingle } from "./helpers.js"; -export default { - typeIdentifier: printSingle, - moduleName: printName, - packageName: printName, - typeName: printName, - expressionName: printName, - methodName: printSingle, - packageOrTypeName: printName, - ambiguousName: printName -}; diff --git a/frontend/src/common/prettier/plugins/java/printers/packages-and-modules.d.ts b/frontend/src/common/prettier/plugins/java/printers/packages-and-modules.d.ts deleted file mode 100644 index c35219ed..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/packages-and-modules.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { ExportsModuleDirectiveCstNode, ImportDeclarationCstNode, OpensModuleDirectiveCstNode } from "java-parser"; -import type { AstPath } from "prettier"; -import { builders } from "prettier/doc"; -import { printSingle, type JavaPrintFn } from "./helpers.js"; -declare const _default: { - compilationUnit(path: AstPath, print: JavaPrintFn): builders.Doc[]; - ordinaryCompilationUnit(path: AstPath, print: JavaPrintFn): builders.Doc[]; - modularCompilationUnit(path: AstPath, print: JavaPrintFn): builders.Doc[]; - packageDeclaration(path: AstPath, print: JavaPrintFn): builders.Doc[]; - packageModifier: typeof printSingle; - importDeclaration(path: AstPath, print: JavaPrintFn): builders.Doc; - typeDeclaration(path: AstPath, print: JavaPrintFn): builders.Doc; - moduleDeclaration(path: AstPath, print: JavaPrintFn): builders.Doc[]; - moduleDirective: typeof printSingle; - requiresModuleDirective(path: AstPath, print: JavaPrintFn): builders.Doc[]; - exportsModuleDirective(path: AstPath, print: JavaPrintFn): builders.Doc[]; - opensModuleDirective(path: AstPath, print: JavaPrintFn): builders.Doc[]; - usesModuleDirective(path: AstPath, print: JavaPrintFn): builders.Doc[]; - providesModuleDirective(path: AstPath, print: JavaPrintFn): builders.Doc[]; - requiresModifier: typeof printSingle; -}; -export default _default; diff --git a/frontend/src/common/prettier/plugins/java/printers/packages-and-modules.js b/frontend/src/common/prettier/plugins/java/printers/packages-and-modules.js deleted file mode 100644 index e1a98c02..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/packages-and-modules.js +++ /dev/null @@ -1,169 +0,0 @@ -import { builders } from "prettier/doc"; -import { call, lineEndWithComments, lineStartWithComments, map, printBlock, printDanglingComments, printName, printSingle } from "./helpers.js"; -const { group, hardline, indent, join, line } = builders; -export default { - compilationUnit(path, print) { - return [...printDanglingComments(path), printSingle(path, print), hardline]; - }, - ordinaryCompilationUnit(path, print) { - const { children } = path.node; - const declarations = []; - if (children.packageDeclaration) { - declarations.push(call(path, print, "packageDeclaration")); - } - if (children.importDeclaration) { - const staticCount = sortImports(children.importDeclaration); - const importDeclarations = map(path, print, "importDeclaration").filter(doc => doc !== ""); - const staticDeclarations = importDeclarations.slice(0, staticCount); - const nonStaticDeclarations = importDeclarations.slice(staticCount); - declarations.push(...[staticDeclarations, nonStaticDeclarations] - .filter(({ length }) => length) - .map(declarations => join(hardline, declarations))); - } - if (children.typeDeclaration) { - declarations.push(...map(path, print, "typeDeclaration").filter(declaration => declaration !== "")); - } - return join([hardline, hardline], declarations); - }, - modularCompilationUnit(path, print) { - const { children } = path.node; - const declarations = []; - if (children.importDeclaration) { - const staticCount = sortImports(children.importDeclaration); - const importDeclarations = map(path, print, "importDeclaration").filter(doc => doc !== ""); - const staticDeclarations = importDeclarations.slice(0, staticCount); - const nonStaticDeclarations = importDeclarations.slice(staticCount); - declarations.push(...[staticDeclarations, nonStaticDeclarations] - .filter(({ length }) => length) - .map(declarations => join(hardline, declarations))); - } - declarations.push(call(path, print, "moduleDeclaration")); - return join([hardline, hardline], declarations); - }, - packageDeclaration(path, print) { - return join(hardline, [ - ...map(path, print, "packageModifier"), - ["package ", printName(path, print), ";"] - ]); - }, - packageModifier: printSingle, - importDeclaration(path, print) { - const { children } = path.node; - if (children.emptyStatement) { - return call(path, print, "emptyStatement"); - } - const declaration = ["import "]; - if (children.Static) { - declaration.push("static "); - } - declaration.push(call(path, print, "packageOrTypeName")); - if (children.Star) { - declaration.push(".*"); - } - declaration.push(";"); - return declaration; - }, - typeDeclaration(path, print) { - return path.node.children.Semicolon ? "" : printSingle(path, print); - }, - moduleDeclaration(path, print) { - const { annotation, Open } = path.node.children; - const prefix = []; - if (annotation) { - prefix.push(...map(path, print, "annotation")); - } - if (Open) { - prefix.push("open"); - } - const declarations = map(path, declarationPath => { - const declaration = print(declarationPath); - const { node, previous } = declarationPath; - return !previous || - lineStartWithComments(node) <= lineEndWithComments(previous) + 1 - ? declaration - : [hardline, declaration]; - }, "moduleDirective"); - return join(" ", [ - ...prefix, - "module", - printName(path, print), - printBlock(path, declarations) - ]); - }, - moduleDirective: printSingle, - requiresModuleDirective(path, print) { - return join(" ", [ - "requires", - ...map(path, print, "requiresModifier"), - [call(path, print, "moduleName"), ";"] - ]); - }, - exportsModuleDirective(path, print) { - return printToModuleNamesDirective(path, print, "exports"); - }, - opensModuleDirective(path, print) { - return printToModuleNamesDirective(path, print, "opens"); - }, - usesModuleDirective(path, print) { - return ["uses ", call(path, print, "typeName"), ";"]; - }, - providesModuleDirective(path, print) { - const [firstTypeName, ...restTypeNames] = map(path, print, "typeName"); - return [ - "provides ", - firstTypeName, - group(indent([ - line, - group(indent(["with", line, ...join([",", line], restTypeNames)])) - ])), - ";" - ]; - }, - requiresModifier: printSingle -}; -function sortImports(importDeclarations) { - importDeclarations.sort(({ children: a }, { children: b }) => { - if (a.Static && !b.Static) { - return -1; - } - else if (b.Static && !a.Static) { - return 1; - } - if (!b.packageOrTypeName) { - if (a.packageOrTypeName) { - return -1; - } - return 0; - } - else if (!a.packageOrTypeName) { - return 1; - } - return compareFqn(a.packageOrTypeName[0], b.packageOrTypeName[0]); - }); - return importDeclarations.reduce((staticCount, importDeclaration) => importDeclaration.children.Static ? staticCount + 1 : staticCount, 0); -} -function compareFqn(a, b) { - const identifiersA = a.children.Identifier; - const identifiersB = b.children.Identifier; - const minParts = Math.min(identifiersA.length, identifiersB.length); - for (let i = 0; i < minParts; i++) { - const imageA = identifiersA[i].image; - const imageB = identifiersB[i].image; - if (imageA < imageB) { - return -1; - } - else if (imageA > imageB) { - return 1; - } - } - return identifiersA.length - identifiersB.length; -} -function printToModuleNamesDirective(path, print, prefix) { - const directive = [prefix, " ", call(path, print, "packageName")]; - if (path.node.children.moduleName) { - const moduleNames = join([",", line], map(path, print, "moduleName")); - directive.push(group(indent([line, group(indent(["to", line, ...moduleNames]))]))); - } - directive.push(";"); - return directive; -} diff --git a/frontend/src/common/prettier/plugins/java/printers/types-values-and-variables.d.ts b/frontend/src/common/prettier/plugins/java/printers/types-values-and-variables.d.ts deleted file mode 100644 index 776c47ba..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/types-values-and-variables.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { builders } from "prettier/doc"; -import { printClassType, printSingle } from "./helpers.js"; -declare const _default: { - primitiveType(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - numericType: typeof printSingle; - integralType: typeof printSingle; - floatingPointType: typeof printSingle; - referenceType(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - classOrInterfaceType: typeof printSingle; - classType: typeof printClassType; - interfaceType: typeof printSingle; - typeVariable(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - dims(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - typeParameter(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - typeParameterModifier: typeof printSingle; - typeBound(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - additionalBound(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - typeArguments(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Group; - typeArgumentList(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - typeArgument: typeof printSingle; - wildcard(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; - wildcardBounds(path: import("prettier").AstPath, print: import("./helpers.js").JavaPrintFn): builders.Doc[]; -}; -export default _default; diff --git a/frontend/src/common/prettier/plugins/java/printers/types-values-and-variables.js b/frontend/src/common/prettier/plugins/java/printers/types-values-and-variables.js deleted file mode 100644 index 3738ac51..00000000 --- a/frontend/src/common/prettier/plugins/java/printers/types-values-and-variables.js +++ /dev/null @@ -1,90 +0,0 @@ -import { builders } from "prettier/doc"; -import { call, definedKeys, flatMap, isNonTerminal, map, onlyDefinedKey, printClassType, printList, printSingle } from "./helpers.js"; -const { group, indent, join, line, softline } = builders; -export default { - primitiveType(path, print) { - const { children } = path.node; - const typeKey = onlyDefinedKey(children, ["Boolean", "numericType"]); - return join(" ", [ - ...map(path, print, "annotation"), - call(path, print, typeKey) - ]); - }, - numericType: printSingle, - integralType: printSingle, - floatingPointType: printSingle, - referenceType(path, print) { - const { children } = path.node; - const typeKey = onlyDefinedKey(children, [ - "primitiveType", - "classOrInterfaceType" - ]); - const type = call(path, print, typeKey); - return join(" ", [ - ...map(path, print, "annotation"), - children.dims ? [type, call(path, print, "dims")] : type - ]); - }, - classOrInterfaceType: printSingle, - classType: printClassType, - interfaceType: printSingle, - typeVariable(path, print) { - return join(" ", [ - ...map(path, print, "annotation"), - call(path, print, "Identifier") - ]); - }, - dims(path, print) { - return flatMap(path, childPath => { - const child = print(childPath); - return isNonTerminal(childPath.node) ? [child, " "] : child; - }, definedKeys(path.node.children, ["annotation", "LSquare", "RSquare"])); - }, - typeParameter(path, print) { - const parameter = [ - ...map(path, print, "typeParameterModifier"), - call(path, print, "typeIdentifier") - ]; - if (path.node.children.typeBound) { - parameter.push(call(path, print, "typeBound")); - } - return join(" ", parameter); - }, - typeParameterModifier: printSingle, - typeBound(path, print) { - const bound = ["extends ", call(path, print, "classOrInterfaceType")]; - if (path.node.children.additionalBound) { - bound.push(group(indent([line, ...join(line, map(path, print, "additionalBound"))]))); - } - return bound; - }, - additionalBound(path, print) { - return ["& ", call(path, print, "interfaceType")]; - }, - typeArguments(path, print) { - return group([ - "<", - indent([softline, call(path, print, "typeArgumentList")]), - softline, - ">" - ]); - }, - typeArgumentList(path, print) { - return printList(path, print, "typeArgument"); - }, - typeArgument: printSingle, - wildcard(path, print) { - const wildcard = [...map(path, print, "annotation"), "?"]; - if (path.node.children.wildcardBounds) { - wildcard.push(call(path, print, "wildcardBounds")); - } - return join(" ", wildcard); - }, - wildcardBounds(path, print) { - return [ - path.node.children.Extends ? "extends" : "super", - " ", - call(path, print, "referenceType") - ]; - } -}; diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts b/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts index 6234f770..b98bbad0 100644 --- a/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts +++ b/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts @@ -56,7 +56,6 @@ import yamlPrettierPlugin from "prettier/plugins/yaml"; import goPrettierPlugin from "@/common/prettier/plugins/go"; import sqlPrettierPlugin from "@/common/prettier/plugins/sql"; import phpPrettierPlugin from "@/common/prettier/plugins/php"; -import javaPrettierPlugin from "@/common/prettier/plugins/java"; import xmlPrettierPlugin from "@prettier/plugin-xml"; import shellPrettierPlugin from "@/common/prettier/plugins/shell"; import dockerfilePrettierPlugin from "@/common/prettier/plugins/docker"; @@ -134,8 +133,12 @@ export const LANGUAGES: LanguageInfo[] = [ plugins: [markdownPrettierPlugin] }), new LanguageInfo("java", "Java", javaLanguage.parser, ["java"], { - parser: "java", - plugins: [javaPrettierPlugin] + parser: "clang-format", + plugins: [clangPrettierPlugin], + options: { + filename: "Main.java", + clangStyle: "Google" + } }), new LanguageInfo("php", "PHP", phpLanguage.configure({top: "Program"}).parser, ["php"], { parser: "php", @@ -268,4 +271,4 @@ export function getLanguage(token: SupportedLanguage): LanguageInfo | undefined */ export function getAllSupportedLanguages(): SupportedLanguage[] { return ['auto', ...LANGUAGES.map(lang => lang.token)]; -} \ No newline at end of file +} diff --git a/go.mod b/go.mod index 1e117118..3d3ee6a4 100644 --- a/go.mod +++ b/go.mod @@ -1,54 +1,54 @@ module voidraft -go 1.25 +go 1.26 require ( - entgo.io/ent v0.14.5 + entgo.io/ent v0.14.6 github.com/creativeprojects/go-selfupdate v1.5.2 - github.com/go-git/go-git/v5 v5.16.4 + github.com/go-git/go-git/v5 v5.17.0 github.com/google/uuid v1.6.0 github.com/knadh/koanf/parsers/json v1.0.0 github.com/knadh/koanf/providers/file v1.2.1 github.com/knadh/koanf/providers/structs v1.0.0 - github.com/knadh/koanf/v2 v2.3.0 - github.com/mattn/go-sqlite3 v1.14.33 + github.com/knadh/koanf/v2 v2.3.4 + github.com/mattn/go-sqlite3 v1.14.37 github.com/stretchr/testify v1.11.1 - github.com/wailsapp/wails/v3 v3.0.0-alpha.55 - golang.org/x/net v0.48.0 - golang.org/x/sys v0.39.0 - golang.org/x/text v0.32.0 + github.com/wailsapp/wails/v3 v3.0.0-alpha.74 + golang.org/x/net v0.52.0 + golang.org/x/sys v0.42.0 + golang.org/x/text v0.35.0 resty.dev/v3 v3.0.0-beta.6 ) require ( - ariga.io/atlas v1.0.0 // indirect - code.gitea.io/sdk/gitea v0.22.1 // indirect + ariga.io/atlas v1.1.0 // indirect + code.gitea.io/sdk/gitea v0.24.1 // indirect dario.cat/mergo v1.0.2 // indirect git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect - github.com/42wim/httpsig v1.2.3 // indirect + github.com/42wim/httpsig v1.2.4 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/ProtonMail/go-crypto v1.4.1 // indirect github.com/adrg/xdg v0.5.3 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/bep/debounce v1.2.1 // indirect github.com/bmatcuk/doublestar v1.3.4 // indirect - github.com/cloudflare/circl v1.6.2 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/coder/websocket v1.8.14 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect - github.com/ebitengine/purego v0.9.1 // indirect + github.com/ebitengine/purego v0.10.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.7.0 // indirect + github.com/go-git/go-billy/v5 v5.8.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-openapi/inflect v0.21.5 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/goccy/go-json v0.10.5 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -60,12 +60,12 @@ require ( github.com/hashicorp/hcl/v2 v2.24.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect - github.com/kevinburke/ssh_config v1.4.0 // indirect + github.com/kevinburke/ssh_config v1.6.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/leaanthony/go-ansi-parser v1.6.1 // indirect github.com/leaanthony/u v1.1.1 // indirect - github.com/lmittmann/tint v1.1.2 // indirect + github.com/lmittmann/tint v1.1.3 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -75,23 +75,22 @@ require ( github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/samber/lo v1.52.0 // indirect + github.com/samber/lo v1.53.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect github.com/skeema/knownhosts v1.3.2 // indirect github.com/ulikunitz/xz v0.5.15 // indirect github.com/wailsapp/go-webview2 v1.0.23 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - github.com/zclconf/go-cty v1.17.0 // indirect + github.com/zclconf/go-cty v1.18.0 // indirect github.com/zclconf/go-cty-yaml v1.2.0 // indirect - gitlab.com/gitlab-org/api/client-go v1.10.0 // indirect - golang.org/x/crypto v0.46.0 // indirect - golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect - golang.org/x/image v0.34.0 // indirect - golang.org/x/mod v0.31.0 // indirect - golang.org/x/oauth2 v0.34.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.40.0 // indirect + gitlab.com/gitlab-org/api/client-go v1.46.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/image v0.38.0 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.43.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1859f42c..11fcdbc6 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,15 @@ -ariga.io/atlas v1.0.0 h1:v9DQH49xK+SM2kKwk4OQBjfz/KNRMUR+pvDiEIxSJto= -ariga.io/atlas v1.0.0/go.mod h1:esBbk3F+pi/mM2PvbCymDm+kWhaOk4PaaiegQdNELk8= -code.gitea.io/sdk/gitea v0.22.1 h1:7K05KjRORyTcTYULQ/AwvlVS6pawLcWyXZcTr7gHFyA= -code.gitea.io/sdk/gitea v0.22.1/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM= +ariga.io/atlas v1.1.0 h1:Dk9Xemh6pr5RogNCsFylf/9ozhSPWDqzHb8EkR2rA78= +ariga.io/atlas v1.1.0/go.mod h1:esBbk3F+pi/mM2PvbCymDm+kWhaOk4PaaiegQdNELk8= +code.gitea.io/sdk/gitea v0.24.1 h1:hpaqcdGcBmfMpV7JSbBJVwE99qo+WqGreJYKrDKEyW8= +code.gitea.io/sdk/gitea v0.24.1/go.mod h1:5/77BL3sHneCMEiZaMT9lfTvnnibsYxyO48mceCF3qA= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4= -entgo.io/ent v0.14.5/go.mod h1:zTzLmWtPvGpmSwtkaayM2cm5m819NdM7z7tYPq3vN0U= +entgo.io/ent v0.14.6 h1:/f2696BpwuWAEEG6PVGWflg6+Inrpq4pRWuNlWz/Skk= +entgo.io/ent v0.14.6/go.mod h1:z46QBUdGC+BATwsedbDuREfSS0oSCV+csdEYlL4p73s= git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA= git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= -github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs= -github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM= +github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU= +github.com/42wim/httpsig v1.2.4/go.mod h1:yKsYfSyTBEohkPik224QPFylmzEBtda/kjyIAJjh3ps= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= @@ -17,8 +17,8 @@ github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lpr github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= -github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= +github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= @@ -33,16 +33,10 @@ github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= -github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= -github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= -github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= -github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= -github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= -github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ= -github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= -github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= -github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/creativeprojects/go-selfupdate v1.5.2 h1:3KR3JLrq70oplb9yZzbmJ89qRP78D1AN/9u+l3k0LJ4= github.com/creativeprojects/go-selfupdate v1.5.2/go.mod h1:BCOuwIl1dRRCmPNRPH0amULeZqayhKyY2mH/h4va7Dk= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= @@ -52,8 +46,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= -github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= -github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= @@ -70,12 +64,12 @@ github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= -github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= +github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= +github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= -github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM= +github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI= github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU= github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= @@ -84,10 +78,8 @@ github.com/go-openapi/inflect v0.21.5 h1:M2RCq6PPS3YbIaL7CXosGL3BbzAcmfBAT0nC3Yf github.com/go-openapi/inflect v0.21.5/go.mod h1:GypUyi6bU880NYurWaEH2CmH84zFDNd+EhhmzroHmB4= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= @@ -101,6 +93,8 @@ github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfh github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/graph-gophers/graphql-go v1.9.0 h1:yu0ucKHLc5qGpRwLYKIWtr9bOoxovkWasuBrPQwlHls= +github.com/graph-gophers/graphql-go v1.9.0/go.mod h1:23olKZ7duEvHlF/2ELEoSZaY1aNPfShjP782SOoNTyM= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= @@ -115,10 +109,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ= github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= -github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= +github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= @@ -129,8 +121,8 @@ github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= github.com/knadh/koanf/providers/structs v1.0.0 h1:DznjB7NQykhqCar2LvNug3MuxEQsZ5KvfgMbio+23u4= github.com/knadh/koanf/providers/structs v1.0.0/go.mod h1:kjo5TFtgpaZORlpoJqcbeLowM2cINodv8kX+oFAeQ1w= -github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM= -github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= +github.com/knadh/koanf/v2 v2.3.4 h1:fnynNSDlujWE+v83hAp8wKr/cdoxHLO0629SN+U8Urc= +github.com/knadh/koanf/v2 v2.3.4/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -142,8 +134,8 @@ github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= -github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= -github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I= +github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= @@ -151,18 +143,14 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= -github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg= +github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= @@ -178,8 +166,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= -github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= +github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= @@ -190,47 +178,43 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0= github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= -github.com/wailsapp/wails/v3 v3.0.0-alpha.55 h1:Wxwxc4EN6axDAvH/O5n3uoZQ+XRY/HQZ5rMdn0npq78= -github.com/wailsapp/wails/v3 v3.0.0-alpha.55/go.mod h1:AyH9vRcseorpL3p5XvxKgK0Lv/agJ7pTmcPdy25xZPo= +github.com/wailsapp/wails/v3 v3.0.0-alpha.74 h1:wRm1EiDQtxDisXk46NtpiBH90STwfKp36NrTDwOEdxw= +github.com/wailsapp/wails/v3 v3.0.0-alpha.74/go.mod h1:4saK4A4K9970X+X7RkMwP2lyGbLogcUz54wVeq4C/V8= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= -github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= +github.com/zclconf/go-cty v1.18.0 h1:pJ8+HNI4gFoyRNqVE37wWbJWVw43BZczFo7KUoRczaA= +github.com/zclconf/go-cty v1.18.0/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= github.com/zclconf/go-cty-yaml v1.2.0 h1:GDyL4+e/Qe/S0B7YaecMLbVvAR/Mp21CXMOSiCTOi1M= github.com/zclconf/go-cty-yaml v1.2.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= -gitlab.com/gitlab-org/api/client-go v1.10.0 h1:VlB9gXQdG6w643lH53VduUHVnCWQG5Ty86VbXnyi70A= -gitlab.com/gitlab-org/api/client-go v1.10.0/go.mod h1:U3QKvjbT1J1FrgLsA7w/XlhoBIendUqB4o3/Ht3UhEQ= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +gitlab.com/gitlab-org/api/client-go v1.46.0 h1:YxBWFZIFYKcGESCb9fpkwzouo+apyB9pr/XTWzNoL24= +gitlab.com/gitlab-org/api/client-go v1.46.0/go.mod h1:FtgyU6g2HS5+fMhw6nLK96GBEEBx5MzntOiJWfIaiN8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0= -golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= -golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8= -golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU= -golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= -golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= +golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE= +golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -242,21 +226,21 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= From 4c5fff5390a896cf43ac929a95aeb04bdc63f33c Mon Sep 17 00:00:00 2001 From: landaiqing Date: Mon, 30 Mar 2026 00:03:23 +0800 Subject: [PATCH 03/26] :recycle: Refactor synchronization service --- .../voidraft/internal/models/models.ts | 1310 ----------------- .../internal/services/backupservice.ts | 79 - .../voidraft/internal/services/index.ts | 41 - frontend/src/common/constant/config.ts | 76 +- frontend/src/i18n/locales/en-US.ts | 30 +- frontend/src/i18n/locales/zh-CN.ts | 22 +- frontend/src/router/index.ts | 10 +- frontend/src/stores/configStore.ts | 192 ++- .../stores/{backupStore.ts => syncStore.ts} | 11 +- frontend/src/views/settings/Settings.vue | 4 +- .../src/views/settings/pages/BackupPage.vue | 321 ---- .../src/views/settings/pages/SyncPage.vue | 368 +++++ internal/models/config.go | 83 +- internal/services/backup_service.go | 1246 ---------------- internal/services/service_manager.go | 10 +- internal/services/sync_service.go | 242 +++ internal/syncer/app.go | 283 ++++ internal/syncer/backend/backend.go | 31 + internal/syncer/backend/git/auth.go | 60 + internal/syncer/backend/git/backend.go | 518 +++++++ .../syncer/backend/snapshotstore/backend.go | 413 ++++++ .../backend/snapshotstore/backend_test.go | 109 ++ .../syncer/backend/snapshotstore/blob/blob.go | 34 + .../snapshotstore/blob/localfs/store.go | 182 +++ .../snapshotstore/blob/localfs/store_test.go | 73 + internal/syncer/config.go | 173 +++ internal/syncer/engine/sync_engine.go | 184 +++ internal/syncer/errors.go | 16 + internal/syncer/merge/merger.go | 19 + internal/syncer/merge/updated_at_wins.go | 98 ++ internal/syncer/merge/updated_at_wins_test.go | 50 + internal/syncer/resource/adapter.go | 61 + internal/syncer/resource/document_adapter.go | 117 ++ internal/syncer/resource/extension_adapter.go | 114 ++ internal/syncer/resource/helpers.go | 75 + .../syncer/resource/keybinding_adapter.go | 135 ++ internal/syncer/resource/theme_adapter.go | 114 ++ internal/syncer/scheduler/ticker.go | 75 + internal/syncer/snapshot/snapshot.go | 248 ++++ internal/syncer/snapshot/store.go | 266 ++++ internal/syncer/snapshot/store_test.go | 59 + internal/syncer/types.go | 20 + 42 files changed, 4375 insertions(+), 3197 deletions(-) delete mode 100644 frontend/bindings/voidraft/internal/models/models.ts delete mode 100644 frontend/bindings/voidraft/internal/services/backupservice.ts delete mode 100644 frontend/bindings/voidraft/internal/services/index.ts rename frontend/src/stores/{backupStore.ts => syncStore.ts} (61%) delete mode 100644 frontend/src/views/settings/pages/BackupPage.vue create mode 100644 frontend/src/views/settings/pages/SyncPage.vue delete mode 100644 internal/services/backup_service.go create mode 100644 internal/services/sync_service.go create mode 100644 internal/syncer/app.go create mode 100644 internal/syncer/backend/backend.go create mode 100644 internal/syncer/backend/git/auth.go create mode 100644 internal/syncer/backend/git/backend.go create mode 100644 internal/syncer/backend/snapshotstore/backend.go create mode 100644 internal/syncer/backend/snapshotstore/backend_test.go create mode 100644 internal/syncer/backend/snapshotstore/blob/blob.go create mode 100644 internal/syncer/backend/snapshotstore/blob/localfs/store.go create mode 100644 internal/syncer/backend/snapshotstore/blob/localfs/store_test.go create mode 100644 internal/syncer/config.go create mode 100644 internal/syncer/engine/sync_engine.go create mode 100644 internal/syncer/errors.go create mode 100644 internal/syncer/merge/merger.go create mode 100644 internal/syncer/merge/updated_at_wins.go create mode 100644 internal/syncer/merge/updated_at_wins_test.go create mode 100644 internal/syncer/resource/adapter.go create mode 100644 internal/syncer/resource/document_adapter.go create mode 100644 internal/syncer/resource/extension_adapter.go create mode 100644 internal/syncer/resource/helpers.go create mode 100644 internal/syncer/resource/keybinding_adapter.go create mode 100644 internal/syncer/resource/theme_adapter.go create mode 100644 internal/syncer/scheduler/ticker.go create mode 100644 internal/syncer/snapshot/snapshot.go create mode 100644 internal/syncer/snapshot/store.go create mode 100644 internal/syncer/snapshot/store_test.go create mode 100644 internal/syncer/types.go diff --git a/frontend/bindings/voidraft/internal/models/models.ts b/frontend/bindings/voidraft/internal/models/models.ts deleted file mode 100644 index 38354e33..00000000 --- a/frontend/bindings/voidraft/internal/models/models.ts +++ /dev/null @@ -1,1310 +0,0 @@ -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import {Create as $Create} from "@wailsio/runtime"; - -/** - * AppConfig 应用配置 - 按照前端设置页面分类组织 - */ -export class AppConfig { - /** - * 通用设置 - */ - "general": GeneralConfig; - - /** - * 编辑设置 - */ - "editing": EditingConfig; - - /** - * 外观设置 - */ - "appearance": AppearanceConfig; - - /** - * 更新设置 - */ - "updates": UpdatesConfig; - - /** - * Git备份设置 - */ - "backup": GitBackupConfig; - - /** - * 配置元数据 - */ - "metadata": ConfigMetadata; - - /** Creates a new AppConfig instance. */ - constructor($$source: Partial = {}) { - if (!("general" in $$source)) { - this["general"] = (new GeneralConfig()); - } - if (!("editing" in $$source)) { - this["editing"] = (new EditingConfig()); - } - if (!("appearance" in $$source)) { - this["appearance"] = (new AppearanceConfig()); - } - if (!("updates" in $$source)) { - this["updates"] = (new UpdatesConfig()); - } - if (!("backup" in $$source)) { - this["backup"] = (new GitBackupConfig()); - } - if (!("metadata" in $$source)) { - this["metadata"] = (new ConfigMetadata()); - } - - Object.assign(this, $$source); - } - - /** - * Creates a new AppConfig instance from a string or object. - */ - static createFrom($$source: any = {}): AppConfig { - const $$createField0_0 = $$createType0; - const $$createField1_0 = $$createType1; - const $$createField2_0 = $$createType2; - const $$createField3_0 = $$createType3; - const $$createField4_0 = $$createType4; - const $$createField5_0 = $$createType5; - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - if ("general" in $$parsedSource) { - $$parsedSource["general"] = $$createField0_0($$parsedSource["general"]); - } - if ("editing" in $$parsedSource) { - $$parsedSource["editing"] = $$createField1_0($$parsedSource["editing"]); - } - if ("appearance" in $$parsedSource) { - $$parsedSource["appearance"] = $$createField2_0($$parsedSource["appearance"]); - } - if ("updates" in $$parsedSource) { - $$parsedSource["updates"] = $$createField3_0($$parsedSource["updates"]); - } - if ("backup" in $$parsedSource) { - $$parsedSource["backup"] = $$createField4_0($$parsedSource["backup"]); - } - if ("metadata" in $$parsedSource) { - $$parsedSource["metadata"] = $$createField5_0($$parsedSource["metadata"]); - } - return new AppConfig($$parsedSource as Partial); - } -} - -/** - * AppearanceConfig 外观设置配置 - */ -export class AppearanceConfig { - /** - * 界面语言 - */ - "language": LanguageType; - - /** - * 系统界面主题 - */ - "systemTheme": SystemThemeType; - - /** - * 当前选择的预设主题名称 - */ - "currentTheme": string; - - /** Creates a new AppearanceConfig instance. */ - constructor($$source: Partial = {}) { - if (!("language" in $$source)) { - this["language"] = ("" as LanguageType); - } - if (!("systemTheme" in $$source)) { - this["systemTheme"] = ("" as SystemThemeType); - } - if (!("currentTheme" in $$source)) { - this["currentTheme"] = ""; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new AppearanceConfig instance from a string or object. - */ - static createFrom($$source: any = {}): AppearanceConfig { - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - return new AppearanceConfig($$parsedSource as Partial); - } -} - -/** - * Git备份相关类型定义 - * - * AuthMethod 定义Git认证方式 - */ -export enum AuthMethod { - /** - * The Go zero value for the underlying type of the enum. - */ - $zero = "", - - /** - * 认证方式 - */ - Token = "token", - SSHKey = "ssh_key", - UserPass = "user_pass", -}; - -/** - * ConfigMetadata 配置元数据 - */ -export class ConfigMetadata { - /** - * 最后更新时间 - */ - "lastUpdated": string; - - /** - * 配置版本号 - */ - "version": string; - - /** Creates a new ConfigMetadata instance. */ - constructor($$source: Partial = {}) { - if (!("lastUpdated" in $$source)) { - this["lastUpdated"] = ""; - } - if (!("version" in $$source)) { - this["version"] = ""; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new ConfigMetadata instance from a string or object. - */ - static createFrom($$source: any = {}): ConfigMetadata { - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - return new ConfigMetadata($$parsedSource as Partial); - } -} - -/** - * EditingConfig 编辑设置配置 - */ -export class EditingConfig { - /** - * 字体设置 - * 字体大小 - */ - "fontSize": number; - - /** - * 字体族 - */ - "fontFamily": string; - - /** - * 字体粗细 - */ - "fontWeight": string; - - /** - * 行高 - */ - "lineHeight": number; - - /** - * Tab设置 - * 是否启用Tab缩进 - */ - "enableTabIndent": boolean; - - /** - * Tab大小 - */ - "tabSize": number; - - /** - * Tab类型(空格或Tab) - */ - "tabType": TabType; - - /** - * 快捷键模式 - * 快捷键模式(standard 或 emacs) - */ - "keymapMode": KeyBindingType; - - /** - * 保存选项 - * 自动保存延迟(毫秒) - */ - "autoSaveDelay": number; - - /** Creates a new EditingConfig instance. */ - constructor($$source: Partial = {}) { - if (!("fontSize" in $$source)) { - this["fontSize"] = 0; - } - if (!("fontFamily" in $$source)) { - this["fontFamily"] = ""; - } - if (!("fontWeight" in $$source)) { - this["fontWeight"] = ""; - } - if (!("lineHeight" in $$source)) { - this["lineHeight"] = 0; - } - if (!("enableTabIndent" in $$source)) { - this["enableTabIndent"] = false; - } - if (!("tabSize" in $$source)) { - this["tabSize"] = 0; - } - if (!("tabType" in $$source)) { - this["tabType"] = ("" as TabType); - } - if (!("keymapMode" in $$source)) { - this["keymapMode"] = ("" as KeyBindingType); - } - if (!("autoSaveDelay" in $$source)) { - this["autoSaveDelay"] = 0; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new EditingConfig instance from a string or object. - */ - static createFrom($$source: any = {}): EditingConfig { - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - return new EditingConfig($$parsedSource as Partial); - } -} - -/** - * Extension 扩展配置 - */ -export class Extension { - "key": ExtensionName; - "enabled": boolean; - "config": ExtensionConfig; - - /** Creates a new Extension instance. */ - constructor($$source: Partial = {}) { - if (!("key" in $$source)) { - this["key"] = ("" as ExtensionName); - } - if (!("enabled" in $$source)) { - this["enabled"] = false; - } - if (!("config" in $$source)) { - this["config"] = ({} as ExtensionConfig); - } - - Object.assign(this, $$source); - } - - /** - * Creates a new Extension instance from a string or object. - */ - static createFrom($$source: any = {}): Extension { - const $$createField2_0 = $$createType6; - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - if ("config" in $$parsedSource) { - $$parsedSource["config"] = $$createField2_0($$parsedSource["config"]); - } - return new Extension($$parsedSource as Partial); - } -} - -/** - * ExtensionConfig 扩展配置项 - */ -export type ExtensionConfig = { [_: string]: any }; - -/** - * ExtensionName 扩展标识符 - */ -export enum ExtensionName { - /** - * The Go zero value for the underlying type of the enum. - */ - $zero = "", - - /** - * 彩虹括号 - */ - RainbowBrackets = "rainbowBrackets", - - /** - * 超链接 - */ - Hyperlink = "hyperlink", - - /** - * 颜色选择器 - */ - ColorSelector = "colorSelector", - - /** - * 代码折叠 - */ - Fold = "fold", - - /** - * 划词翻译 - */ - Translator = "translator", - - /** - * Markdown渲染 - */ - Markdown = "markdown", - - /** - * 显示空白字符 - */ - HighlightWhitespace = "highlightWhitespace", - - /** - * 高亮行尾空白 - */ - HighlightTrailingWhitespace = "highlightTrailingWhitespace", - - /** - * 小地图 - */ - Minimap = "minimap", - - /** - * 行号显示 - */ - LineNumbers = "lineNumbers", - - /** - * 上下文菜单 - */ - ContextMenu = "contextMenu", - - /** - * 搜索功能 - */ - Search = "search", - - /** - * HTTP 客户端 - */ - HttpClient = "httpClient", - - /** - * 代码块导出图片 - */ - BlockImage = "blockImage", -}; - -/** - * GeneralConfig 通用设置配置 - */ -export class GeneralConfig { - /** - * 窗口是否置顶 - */ - "alwaysOnTop": boolean; - - /** - * 数据存储路径 - */ - "dataPath": string; - - /** - * 是否启用系统托盘 - */ - "enableSystemTray": boolean; - - /** - * 开机启动设置 - */ - "startAtLogin": boolean; - - /** - * 窗口吸附设置 - * 是否启用窗口吸附功能(阈值现在是自适应的) - */ - "enableWindowSnap": boolean; - - /** - * 全局热键设置 - * 是否启用全局热键 - */ - "enableGlobalHotkey": boolean; - - /** - * 全局热键组合 - */ - "globalHotkey": HotkeyCombo; - - /** - * 界面设置 - * 是否启用加载动画 - */ - "enableLoadingAnimation": boolean; - - /** - * 是否启用标签页模式 - */ - "enableTabs": boolean; - - /** - * 是否启用内存监视器 - */ - "enableMemoryMonitor": boolean; - - /** Creates a new GeneralConfig instance. */ - constructor($$source: Partial = {}) { - if (!("alwaysOnTop" in $$source)) { - this["alwaysOnTop"] = false; - } - if (!("dataPath" in $$source)) { - this["dataPath"] = ""; - } - if (!("enableSystemTray" in $$source)) { - this["enableSystemTray"] = false; - } - if (!("startAtLogin" in $$source)) { - this["startAtLogin"] = false; - } - if (!("enableWindowSnap" in $$source)) { - this["enableWindowSnap"] = false; - } - if (!("enableGlobalHotkey" in $$source)) { - this["enableGlobalHotkey"] = false; - } - if (!("globalHotkey" in $$source)) { - this["globalHotkey"] = (new HotkeyCombo()); - } - if (!("enableLoadingAnimation" in $$source)) { - this["enableLoadingAnimation"] = false; - } - if (!("enableTabs" in $$source)) { - this["enableTabs"] = false; - } - if (!("enableMemoryMonitor" in $$source)) { - this["enableMemoryMonitor"] = false; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new GeneralConfig instance from a string or object. - */ - static createFrom($$source: any = {}): GeneralConfig { - const $$createField6_0 = $$createType8; - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - if ("globalHotkey" in $$parsedSource) { - $$parsedSource["globalHotkey"] = $$createField6_0($$parsedSource["globalHotkey"]); - } - return new GeneralConfig($$parsedSource as Partial); - } -} - -/** - * GitBackupConfig Git备份配置 - */ -export class GitBackupConfig { - "enabled": boolean; - "repo_url": string; - "auth_method": AuthMethod; - "username"?: string; - "password"?: string; - "token"?: string; - "ssh_key_path"?: string; - "ssh_key_passphrase"?: string; - - /** - * 分钟 - */ - "backup_interval": number; - "auto_backup": boolean; - - /** Creates a new GitBackupConfig instance. */ - constructor($$source: Partial = {}) { - if (!("enabled" in $$source)) { - this["enabled"] = false; - } - if (!("repo_url" in $$source)) { - this["repo_url"] = ""; - } - if (!("auth_method" in $$source)) { - this["auth_method"] = ("" as AuthMethod); - } - if (!("backup_interval" in $$source)) { - this["backup_interval"] = 0; - } - if (!("auto_backup" in $$source)) { - this["auto_backup"] = false; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new GitBackupConfig instance from a string or object. - */ - static createFrom($$source: any = {}): GitBackupConfig { - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - return new GitBackupConfig($$parsedSource as Partial); - } -} - -/** - * GithubConfig GitHub配置 - */ -export class GithubConfig { - /** - * 仓库所有者 - */ - "owner": string; - - /** - * 仓库名称 - */ - "repo": string; - - /** Creates a new GithubConfig instance. */ - constructor($$source: Partial = {}) { - if (!("owner" in $$source)) { - this["owner"] = ""; - } - if (!("repo" in $$source)) { - this["repo"] = ""; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new GithubConfig instance from a string or object. - */ - static createFrom($$source: any = {}): GithubConfig { - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - return new GithubConfig($$parsedSource as Partial); - } -} - -/** - * HotkeyCombo 热键组合定义 - */ -export class HotkeyCombo { - /** - * Ctrl键 - */ - "ctrl": boolean; - - /** - * Shift键 - */ - "shift": boolean; - - /** - * Alt键 - */ - "alt": boolean; - - /** - * Win键 - */ - "win": boolean; - - /** - * 主键(如 'X', 'F1' 等) - */ - "key": string; - - /** Creates a new HotkeyCombo instance. */ - constructor($$source: Partial = {}) { - if (!("ctrl" in $$source)) { - this["ctrl"] = false; - } - if (!("shift" in $$source)) { - this["shift"] = false; - } - if (!("alt" in $$source)) { - this["alt"] = false; - } - if (!("win" in $$source)) { - this["win"] = false; - } - if (!("key" in $$source)) { - this["key"] = ""; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new HotkeyCombo instance from a string or object. - */ - static createFrom($$source: any = {}): HotkeyCombo { - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - return new HotkeyCombo($$parsedSource as Partial); - } -} - -/** - * KeyBinding 单个快捷键绑定 - */ -export class KeyBinding { - /** - * 命令唯一标识符 - */ - "name": KeyBindingName; - - /** - * 快捷键类型(standard 或 "emacs") - */ - "type": KeyBindingType; - - /** - * 通用快捷键(跨平台) - */ - "key"?: string; - - /** - * macOS 专用快捷键 - */ - "macos"?: string; - - /** - * windows 专用快捷键 - */ - "win"?: string; - - /** - * Linux 专用快捷键 - */ - "linux"?: string; - - /** - * 所属扩展 - */ - "extension": ExtensionName; - - /** - * 是否启用 - */ - "enabled": boolean; - - /** - * 阻止浏览器默认行为 - */ - "preventDefault": boolean; - - /** - * 作用域(默认 "editor") - */ - "scope"?: string; - - /** Creates a new KeyBinding instance. */ - constructor($$source: Partial = {}) { - if (!("name" in $$source)) { - this["name"] = ("" as KeyBindingName); - } - if (!("type" in $$source)) { - this["type"] = ("" as KeyBindingType); - } - if (!("extension" in $$source)) { - this["extension"] = ("" as ExtensionName); - } - if (!("enabled" in $$source)) { - this["enabled"] = false; - } - if (!("preventDefault" in $$source)) { - this["preventDefault"] = false; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new KeyBinding instance from a string or object. - */ - static createFrom($$source: any = {}): KeyBinding { - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - return new KeyBinding($$parsedSource as Partial); - } -} - -/** - * KeyBindingName 快捷键命令标识符 - */ -export enum KeyBindingName { - /** - * The Go zero value for the underlying type of the enum. - */ - $zero = "", - - /** - * 显示搜索 - */ - ShowSearch = "showSearch", - - /** - * 隐藏搜索 - */ - HideSearch = "hideSearch", - - /** - * 块内选择全部 - */ - BlockSelectAll = "blockSelectAll", - - /** - * 在当前块后添加新块 - */ - BlockAddAfterCurrent = "blockAddAfterCurrent", - - /** - * 在最后添加新块 - */ - BlockAddAfterLast = "blockAddAfterLast", - - /** - * 在当前块前添加新块 - */ - BlockAddBeforeCurrent = "blockAddBeforeCurrent", - - /** - * 跳转到上一个块 - */ - BlockGotoPrevious = "blockGotoPrevious", - - /** - * 跳转到下一个块 - */ - BlockGotoNext = "blockGotoNext", - - /** - * 选择上一个块 - */ - BlockSelectPrevious = "blockSelectPrevious", - - /** - * 选择下一个块 - */ - BlockSelectNext = "blockSelectNext", - - /** - * 删除当前块 - */ - BlockDelete = "blockDelete", - - /** - * 向上移动当前块 - */ - BlockMoveUp = "blockMoveUp", - - /** - * 向下移动当前块 - */ - BlockMoveDown = "blockMoveDown", - - /** - * 删除行 - */ - BlockDeleteLine = "blockDeleteLine", - - /** - * 向上移动行 - */ - BlockMoveLineUp = "blockMoveLineUp", - - /** - * 向下移动行 - */ - BlockMoveLineDown = "blockMoveLineDown", - - /** - * 字符转置 - */ - BlockTransposeChars = "blockTransposeChars", - - /** - * 格式化代码块 - */ - BlockFormat = "blockFormat", - - /** - * 复制 - */ - BlockCopy = "blockCopy", - - /** - * 剪切 - */ - BlockCut = "blockCut", - - /** - * 粘贴 - */ - BlockPaste = "blockPaste", - - /** - * 折叠代码 - */ - FoldCode = "foldCode", - - /** - * 展开代码 - */ - UnfoldCode = "unfoldCode", - - /** - * 折叠全部 - */ - FoldAll = "foldAll", - - /** - * 展开全部 - */ - UnfoldAll = "unfoldAll", - - /** - * 光标按语法左移 - */ - CursorSyntaxLeft = "cursorSyntaxLeft", - - /** - * 光标按语法右移 - */ - CursorSyntaxRight = "cursorSyntaxRight", - - /** - * 按语法选择左侧 - */ - SelectSyntaxLeft = "selectSyntaxLeft", - - /** - * 按语法选择右侧 - */ - SelectSyntaxRight = "selectSyntaxRight", - - /** - * 向上复制行 - */ - CopyLineUp = "copyLineUp", - - /** - * 向下复制行 - */ - CopyLineDown = "copyLineDown", - - /** - * 插入空行 - */ - InsertBlankLine = "insertBlankLine", - - /** - * 选择行 - */ - SelectLine = "selectLine", - - /** - * 选择父级语法 - */ - SelectParentSyntax = "selectParentSyntax", - - /** - * 简化选择 - */ - SimplifySelection = "simplifySelection", - - /** - * 在上方添加光标 - */ - AddCursorAbove = "addCursorAbove", - - /** - * 在下方添加光标 - */ - AddCursorBelow = "addCursorBelow", - - /** - * 光标按单词左移 - */ - CursorGroupLeft = "cursorGroupLeft", - - /** - * 光标按单词右移 - */ - CursorGroupRight = "cursorGroupRight", - - /** - * 按单词选择左侧 - */ - SelectGroupLeft = "selectGroupLeft", - - /** - * 按单词选择右侧 - */ - SelectGroupRight = "selectGroupRight", - - /** - * 删除到行尾 - */ - DeleteToLineEnd = "deleteToLineEnd", - - /** - * 删除到行首 - */ - DeleteToLineStart = "deleteToLineStart", - - /** - * 移动到行首 - */ - CursorLineStart = "cursorLineStart", - - /** - * 移动到行尾 - */ - CursorLineEnd = "cursorLineEnd", - - /** - * 选择到行首 - */ - SelectLineStart = "selectLineStart", - - /** - * 选择到行尾 - */ - SelectLineEnd = "selectLineEnd", - - /** - * 跳转到文档开头 - */ - CursorDocStart = "cursorDocStart", - - /** - * 跳转到文档结尾 - */ - CursorDocEnd = "cursorDocEnd", - - /** - * 选择到文档开头 - */ - SelectDocStart = "selectDocStart", - - /** - * 选择到文档结尾 - */ - SelectDocEnd = "selectDocEnd", - - /** - * 选择到匹配括号 - */ - SelectMatchingBracket = "selectMatchingBracket", - - /** - * 分割行 - */ - SplitLine = "splitLine", - - /** - * 光标左移一个字符 - */ - CursorCharLeft = "cursorCharLeft", - - /** - * 光标右移一个字符 - */ - CursorCharRight = "cursorCharRight", - - /** - * 光标上移一行 - */ - CursorLineUp = "cursorLineUp", - - /** - * 光标下移一行 - */ - CursorLineDown = "cursorLineDown", - - /** - * 向上翻页 - */ - CursorPageUp = "cursorPageUp", - - /** - * 向下翻页 - */ - CursorPageDown = "cursorPageDown", - - /** - * 选择左移一个字符 - */ - SelectCharLeft = "selectCharLeft", - - /** - * 选择右移一个字符 - */ - SelectCharRight = "selectCharRight", - - /** - * 选择上移一行 - */ - SelectLineUp = "selectLineUp", - - /** - * 选择下移一行 - */ - SelectLineDown = "selectLineDown", - - /** - * 减少缩进 - */ - IndentLess = "indentLess", - - /** - * 增加缩进 - */ - IndentMore = "indentMore", - - /** - * 缩进选择 - */ - IndentSelection = "indentSelection", - - /** - * 光标到匹配括号 - */ - CursorMatchingBracket = "cursorMatchingBracket", - - /** - * 切换注释 - */ - ToggleComment = "toggleComment", - - /** - * 切换块注释 - */ - ToggleBlockComment = "toggleBlockComment", - - /** - * 插入新行并缩进 - */ - InsertNewlineAndIndent = "insertNewlineAndIndent", - - /** - * 向后删除字符 - */ - DeleteCharBackward = "deleteCharBackward", - - /** - * 向前删除字符 - */ - DeleteCharForward = "deleteCharForward", - - /** - * 向后删除组 - */ - DeleteGroupBackward = "deleteGroupBackward", - - /** - * 向前删除组 - */ - DeleteGroupForward = "deleteGroupForward", - - /** - * 撤销 - */ - HistoryUndo = "historyUndo", - - /** - * 重做 - */ - HistoryRedo = "historyRedo", - - /** - * 撤销选择 - */ - HistoryUndoSelection = "historyUndoSelection", - - /** - * 重做选择 - */ - HistoryRedoSelection = "historyRedoSelection", - - /** - * 复制块为图片 - */ - CopyBlockImage = "copyBlockImage", -}; - -export enum KeyBindingType { - /** - * The Go zero value for the underlying type of the enum. - */ - $zero = "", - - /** - * standard 标准快捷键 - */ - Standard = "standard", - - /** - * emacs 快捷键 - */ - Emacs = "emacs", -}; - -/** - * LanguageType 语言类型定义 - */ -export enum LanguageType { - /** - * The Go zero value for the underlying type of the enum. - */ - $zero = "", - - /** - * LangZhCN 中文简体 - */ - LangZhCN = "zh-CN", - - /** - * LangEnUS 英文-美国 - */ - LangEnUS = "en-US", -}; - -/** - * SystemThemeType 系统主题类型定义 - */ -export enum SystemThemeType { - /** - * The Go zero value for the underlying type of the enum. - */ - $zero = "", - - /** - * SystemThemeDark 深色系统主题 - */ - SystemThemeDark = "dark", - - /** - * SystemThemeLight 浅色系统主题 - */ - SystemThemeLight = "light", - - /** - * SystemThemeAuto 跟随系统主题 - */ - SystemThemeAuto = "auto", -}; - -/** - * TabType 定义了制表符类型 - */ -export enum TabType { - /** - * The Go zero value for the underlying type of the enum. - */ - $zero = "", - - /** - * TabTypeSpaces 使用空格作为制表符 - */ - TabTypeSpaces = "spaces", - - /** - * TabTypeTab 使用Tab作为制表符 - */ - TabTypeTab = "tab", -}; - -/** - * UpdatesConfig 更新设置配置 - */ -export class UpdatesConfig { - /** - * 当前版本号 - */ - "version": string; - - /** - * 是否自动更新 - */ - "autoUpdate": boolean; - - /** - * 更新前是否备份 - */ - "backupBeforeUpdate": boolean; - - /** - * 更新超时时间(秒) - */ - "updateTimeout": number; - - /** - * GitHub配置 - */ - "github": GithubConfig; - - /** Creates a new UpdatesConfig instance. */ - constructor($$source: Partial = {}) { - if (!("version" in $$source)) { - this["version"] = ""; - } - if (!("autoUpdate" in $$source)) { - this["autoUpdate"] = false; - } - if (!("backupBeforeUpdate" in $$source)) { - this["backupBeforeUpdate"] = false; - } - if (!("updateTimeout" in $$source)) { - this["updateTimeout"] = 0; - } - if (!("github" in $$source)) { - this["github"] = (new GithubConfig()); - } - - Object.assign(this, $$source); - } - - /** - * Creates a new UpdatesConfig instance from a string or object. - */ - static createFrom($$source: any = {}): UpdatesConfig { - const $$createField4_0 = $$createType9; - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - if ("github" in $$parsedSource) { - $$parsedSource["github"] = $$createField4_0($$parsedSource["github"]); - } - return new UpdatesConfig($$parsedSource as Partial); - } -} - -// Private type creation functions -const $$createType0 = GeneralConfig.createFrom; -const $$createType1 = EditingConfig.createFrom; -const $$createType2 = AppearanceConfig.createFrom; -const $$createType3 = UpdatesConfig.createFrom; -const $$createType4 = GitBackupConfig.createFrom; -const $$createType5 = ConfigMetadata.createFrom; -var $$createType6 = (function $$initCreateType6(...args): any { - if ($$createType6 === $$initCreateType6) { - $$createType6 = $$createType7; - } - return $$createType6(...args); -}); -const $$createType7 = $Create.Map($Create.Any, $Create.Any); -const $$createType8 = HotkeyCombo.createFrom; -const $$createType9 = GithubConfig.createFrom; diff --git a/frontend/bindings/voidraft/internal/services/backupservice.ts b/frontend/bindings/voidraft/internal/services/backupservice.ts deleted file mode 100644 index 5ff03b88..00000000 --- a/frontend/bindings/voidraft/internal/services/backupservice.ts +++ /dev/null @@ -1,79 +0,0 @@ -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -/** - * BackupService 提供基于Git的备份同步功能 - * @module - */ - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import * as models$0 from "../models/models.js"; - -/** - * HandleConfigChange 处理配置变更 - */ -export function HandleConfigChange(config: models$0.GitBackupConfig | null): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(395287784, config) as any; - return $resultPromise; -} - -/** - * Initialize 初始化备份服务 - */ -export function Initialize(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1052437974) as any; - return $resultPromise; -} - -/** - * Reinitialize 重新初始化 - */ -export function Reinitialize(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(301562543) as any; - return $resultPromise; -} - -/** - * ServiceShutdown 服务关闭 - */ -export function ServiceShutdown(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(422131801) as any; - return $resultPromise; -} - -export function ServiceStartup(options: application$0.ServiceOptions): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2900331732, options) as any; - return $resultPromise; -} - -/** - * StartAutoBackup 启动自动备份 - */ -export function StartAutoBackup(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3035755449) as any; - return $resultPromise; -} - -/** - * StopAutoBackup 停止自动备份 - */ -export function StopAutoBackup(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2641894021) as any; - return $resultPromise; -} - -/** - * Sync 执行完整的同步流程:导出 -> commit -> pull -> 解决冲突 -> push -> 导入 - */ -export function Sync(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3420383211) as any; - return $resultPromise; -} diff --git a/frontend/bindings/voidraft/internal/services/index.ts b/frontend/bindings/voidraft/internal/services/index.ts deleted file mode 100644 index 4bc0bbe2..00000000 --- a/frontend/bindings/voidraft/internal/services/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -import * as BackupService from "./backupservice.js"; -import * as ConfigService from "./configservice.js"; -import * as DatabaseService from "./databaseservice.js"; -import * as DialogService from "./dialogservice.js"; -import * as DocumentService from "./documentservice.js"; -import * as ExtensionService from "./extensionservice.js"; -import * as HotkeyService from "./hotkeyservice.js"; -import * as HttpClientService from "./httpclientservice.js"; -import * as KeyBindingService from "./keybindingservice.js"; -import * as MigrationService from "./migrationservice.js"; -import * as SelfUpdateService from "./selfupdateservice.js"; -import * as StartupService from "./startupservice.js"; -import * as SystemService from "./systemservice.js"; -import * as TestService from "./testservice.js"; -import * as ThemeService from "./themeservice.js"; -import * as TranslationService from "./translationservice.js"; -import * as WindowService from "./windowservice.js"; -export { - BackupService, - ConfigService, - DatabaseService, - DialogService, - DocumentService, - ExtensionService, - HotkeyService, - HttpClientService, - KeyBindingService, - MigrationService, - SelfUpdateService, - StartupService, - SystemService, - TestService, - ThemeService, - TranslationService, - WindowService -}; - -export * from "./models.js"; diff --git a/frontend/src/common/constant/config.ts b/frontend/src/common/constant/config.ts index c3ad2ba0..5b75af88 100644 --- a/frontend/src/common/constant/config.ts +++ b/frontend/src/common/constant/config.ts @@ -1,6 +1,7 @@ import { AppConfig, AuthMethod, + SyncTarget, KeyBindingType, LanguageType, SystemThemeType, @@ -8,12 +9,8 @@ import { } from '@/../bindings/voidraft/internal/models/models'; import {FONT_OPTIONS} from './fonts'; -export type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight'; -export type ConfigSection = 'general' | 'editing' | 'appearance' | 'updates' | 'backup'; - // 统一配置键映射(平级展开) export const CONFIG_KEY_MAP = { - // general alwaysOnTop: 'general.alwaysOnTop', dataPath: 'general.dataPath', enableSystemTray: 'general.enableSystemTray', @@ -24,7 +21,7 @@ export const CONFIG_KEY_MAP = { enableLoadingAnimation: 'general.enableLoadingAnimation', enableTabs: 'general.enableTabs', enableMemoryMonitor: 'general.enableMemoryMonitor', - // editing + fontSize: 'editing.fontSize', fontFamily: 'editing.fontFamily', fontWeight: 'editing.fontWeight', @@ -34,33 +31,35 @@ export const CONFIG_KEY_MAP = { tabType: 'editing.tabType', keymapMode: 'editing.keymapMode', autoSaveDelay: 'editing.autoSaveDelay', - // appearance + language: 'appearance.language', systemTheme: 'appearance.systemTheme', currentTheme: 'appearance.currentTheme', - // updates - version: 'updates.version', + autoUpdate: 'updates.autoUpdate', - primarySource: 'updates.primarySource', - backupSource: 'updates.backupSource', backupBeforeUpdate: 'updates.backupBeforeUpdate', updateTimeout: 'updates.updateTimeout', github: 'updates.github', - gitea: 'updates.gitea', - // backup - enabled: 'backup.enabled', - repo_url: 'backup.repo_url', - auth_method: 'backup.auth_method', - username: 'backup.username', - password: 'backup.password', - token: 'backup.token', - ssh_key_path: 'backup.ssh_key_path', - ssh_key_passphrase: 'backup.ssh_key_passphrase', - backup_interval: 'backup.backup_interval', - auto_backup: 'backup.auto_backup', + + sync_target: 'sync.target', + git_enabled: 'sync.git.enabled', + git_auto_sync: 'sync.git.auto_sync', + git_sync_interval: 'sync.git.sync_interval', + git_repo_url: 'sync.git.repo_url', + git_auth_method: 'sync.git.auth_method', + git_username: 'sync.git.username', + git_password: 'sync.git.password', + git_token: 'sync.git.token', + git_ssh_key_path: 'sync.git.ssh_key_path', + git_ssh_key_passphrase: 'sync.git.ssh_key_passphrase', + localfs_enabled: 'sync.localfs.enabled', + localfs_auto_sync: 'sync.localfs.auto_sync', + localfs_sync_interval: 'sync.localfs.sync_interval', + localfs_root_path: 'sync.localfs.root_path', } as const; export type ConfigKey = keyof typeof CONFIG_KEY_MAP; +export type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight'; // 配置限制 export const CONFIG_LIMITS = { @@ -116,20 +115,29 @@ export const DEFAULT_CONFIG: AppConfig = { repo: "voidraft", }, }, - backup: { - enabled: false, - repo_url: "", - auth_method: AuthMethod.UserPass, - username: "", - password: "", - token: "", - ssh_key_path: "", - ssh_key_passphrase: "", - backup_interval: 60, - auto_backup: true, + sync: { + target: SyncTarget.SyncTargetGit, + git: { + enabled: false, + auto_sync: false, + sync_interval: 60, + repo_url: '', + auth_method: AuthMethod.UserPass, + username: '', + password: '', + token: '', + ssh_key_path: '', + ssh_key_passphrase: '', + }, + localfs: { + enabled: false, + auto_sync: false, + sync_interval: 60, + root_path: '', + }, }, metadata: { version: '1.0.0', lastUpdated: new Date().toString(), } -}; \ No newline at end of file +}; diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 5607cc18..77f60ed8 100644 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -182,7 +182,7 @@ export default { general: 'General', editing: 'Editor', appearance: 'Appearance', - backupPage: 'Backup', + syncPage: 'Sync', keyBindings: 'Key Bindings', updates: 'Updates', reset: 'Reset', @@ -257,11 +257,16 @@ export default { restartNow: 'Restart Now', hotkeyPreview: 'Preview:', none: 'None', - backup: { - basicSettings: 'Basic Settings', - enableBackup: 'Enable Git Backup', - autoBackup: 'Auto Backup', - backupInterval: 'Backup Interval', + sync: { + basicSettings: 'Basic Settings', + enableSync: 'Enable Sync', + targetType: 'Sync Type', + targetTypes: { + git: 'Git', + localfs: 'Local File System' + }, + autoSync: 'Auto Sync', + syncInterval: 'Sync Interval', intervals: { '5min': '5 minutes', '10min': '10 minutes', @@ -270,8 +275,11 @@ export default { '1hour': '1 hour' }, repositoryConfig: 'Repository Configuration', - repoUrl: 'Repository URL', - repoUrlPlaceholder: 'Enter Git repository URL', + storageConfig: 'Storage Configuration', + repoUrl: 'Repository URL', + repoUrlPlaceholder: 'Enter Git repository URL', + localfsRootPath: 'Local Storage Directory', + localfsRootPathPlaceholder: 'Select local sync directory', authConfig: 'Authentication Configuration', authMethod: 'Authentication Method', authMethods: { @@ -289,9 +297,11 @@ export default { sshKeyPathPlaceholder: 'Select SSH key file', sshKeyPassphrase: 'SSH Key Passphrase', sshKeyPassphrasePlaceholder: 'Enter SSH key passphrase', - backupOperations: 'Backup Operations', - syncToRemote: 'Sync to Remote', + syncOperations: 'Sync Operations', + syncToRemote: 'Sync to Remote', + syncToTarget: 'Sync to Target', syncing: 'Syncing...', + syncSuccess: 'Sync completed', actions: { sync: 'Sync', } diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index b8fe43fe..6b60d81d 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -182,7 +182,7 @@ export default { general: '常规', editing: '编辑器', appearance: '外观', - backupPage: '备份', + syncPage: '同步', extensions: '扩展', keyBindings: '快捷键', updates: '更新', @@ -259,11 +259,16 @@ export default { colorValue: '颜色值', hotkeyPreview: '预览:', none: '无', - backup: { + sync: { basicSettings: '基本设置', - enableBackup: '启用备份', - autoBackup: '自动备份', - backupInterval: '备份间隔', + enableSync: '启用同步', + targetType: '同步方式', + targetTypes: { + git: 'Git', + localfs: '本地文件系统' + }, + autoSync: '自动同步', + syncInterval: '同步间隔', intervals: { '5min': '5分钟', '10min': '10分钟', @@ -272,8 +277,11 @@ export default { '1hour': '1小时' }, repositoryConfig: '仓库配置', + storageConfig: '存储配置', repoUrl: '仓库地址', repoUrlPlaceholder: '请输入Git仓库地址', + localfsRootPath: '本地存储目录', + localfsRootPathPlaceholder: '请选择本地同步目录', authConfig: '认证配置', authMethod: '认证方式', authMethods: { @@ -291,9 +299,11 @@ export default { sshKeyPathPlaceholder: '请选择SSH密钥文件', sshKeyPassphrase: 'SSH密钥密码', sshKeyPassphrasePlaceholder: '请输入SSH密钥密码', - backupOperations: '备份操作', + syncOperations: '同步操作', syncToRemote: '同步到远程', + syncToTarget: '同步到目标', syncing: '同步中...', + syncSuccess: '同步成功', actions: { sync: '同步', } diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index a40d9990..deb37135 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -7,7 +7,7 @@ import AppearancePage from '@/views/settings/pages/AppearancePage.vue'; import KeyBindingsPage from '@/views/settings/pages/KeyBindingsPage.vue'; import UpdatesPage from '@/views/settings/pages/UpdatesPage.vue'; import ExtensionsPage from '@/views/settings/pages/ExtensionsPage.vue'; -import BackupPage from '@/views/settings/pages/BackupPage.vue'; +import SyncPage from '@/views/settings/pages/SyncPage.vue'; // 测试页面 import TestPage from '@/views/settings/pages/TestPage.vue'; @@ -44,9 +44,9 @@ const settingsChildren: RouteRecordRaw[] = [ component: UpdatesPage }, { - path: 'backup', - name: 'SettingsBackup', - component: BackupPage + path: 'sync', + name: 'SettingsSync', + component: SyncPage } ]; @@ -79,4 +79,4 @@ const router = createRouter({ routes: routes }); -export default router; \ No newline at end of file +export default router; diff --git a/frontend/src/stores/configStore.ts b/frontend/src/stores/configStore.ts index 259ebd3e..3bbb1353 100644 --- a/frontend/src/stores/configStore.ts +++ b/frontend/src/stores/configStore.ts @@ -1,113 +1,124 @@ import {defineStore} from 'pinia'; import {computed, reactive} from 'vue'; import {ConfigService, StartupService} from '@/../bindings/voidraft/internal/services'; -import {AppConfig, AuthMethod, LanguageType, SystemThemeType, TabType} from '@/../bindings/voidraft/internal/models/models'; +import { + AppConfig, + AuthMethod, + SyncTarget, + LanguageType, + SystemThemeType, +} from '@/../bindings/voidraft/internal/models/models'; import {useI18n} from 'vue-i18n'; import {ConfigUtils} from '@/common/utils/configUtils'; import {FONT_OPTIONS} from '@/common/constant/fonts'; -import { - CONFIG_KEY_MAP, - CONFIG_LIMITS, - ConfigKey, - ConfigSection, - DEFAULT_CONFIG, - NumberConfigKey -} from '@/common/constant/config'; +import {CONFIG_KEY_MAP, CONFIG_LIMITS, ConfigKey, DEFAULT_CONFIG, NumberConfigKey} from '@/common/constant/config'; import * as runtime from '@wailsio/runtime'; export const useConfigStore = defineStore('config', () => { const {locale} = useI18n(); - // 响应式状态 const state = reactive({ - config: {...DEFAULT_CONFIG} as AppConfig, + config: structuredClone(DEFAULT_CONFIG) as AppConfig, isLoading: false, configLoaded: false }); - // Font options (no longer localized) const fontOptions = computed(() => FONT_OPTIONS); - // 统一配置更新方法 - const updateConfig = async (key: K, value: any): Promise => { + const applyConfig = (appConfig?: AppConfig | null): void => { + const nextConfig = structuredClone(DEFAULT_CONFIG) as AppConfig; + + if (appConfig?.general) Object.assign(nextConfig.general, appConfig.general); + if (appConfig?.editing) Object.assign(nextConfig.editing, appConfig.editing); + if (appConfig?.appearance) Object.assign(nextConfig.appearance, appConfig.appearance); + if (appConfig?.updates) Object.assign(nextConfig.updates, appConfig.updates); + if (appConfig?.sync) { + if (appConfig.sync.target) { + nextConfig.sync.target = appConfig.sync.target; + } + if (appConfig.sync.git) { + Object.assign(nextConfig.sync.git, appConfig.sync.git); + } + if (appConfig.sync.localfs) { + Object.assign(nextConfig.sync.localfs, appConfig.sync.localfs); + } + } + if (appConfig?.metadata) Object.assign(nextConfig.metadata, appConfig.metadata); + + state.config = nextConfig; + }; + + const ensureConfigLoaded = async (): Promise => { if (!state.configLoaded && !state.isLoading) { await initConfig(); } + }; - const backendKey = CONFIG_KEY_MAP[key]; - if (!backendKey) { - throw new Error(`No backend key mapping found for ${String(key)}`); + const setValueByPath = (target: Record, path: string, value: unknown): void => { + const segments = path.split('.'); + const lastIndex = segments.length - 1; + + let current: Record = target; + for (let index = 0; index < lastIndex; index++) { + current = current[segments[index]]; } + current[segments[lastIndex]] = value; + }; - // 从 backendKey 提取 section(例如 'general.alwaysOnTop' -> 'general') - const section = backendKey.split('.')[0] as ConfigSection; + const getValueByPath = (target: Record, path: string): unknown => { + return path.split('.').reduce((current, segment) => (current as Record)[segment], target); + }; - await ConfigService.Set(backendKey, value); - (state.config[section] as any)[key] = value; + const updateConfig = async (key: K, value: unknown): Promise => { + await ensureConfigLoaded(); + const path = CONFIG_KEY_MAP[key]; + await ConfigService.Set(path, value); + setValueByPath(state.config as Record, path, value); }; - // 只更新本地状态,不保存到后端 - const updateConfigLocal = (key: K, value: any): void => { - const backendKey = CONFIG_KEY_MAP[key]; - const section = backendKey.split('.')[0] as ConfigSection; - (state.config[section] as any)[key] = value; + const updateConfigLocal = (key: K, value: unknown): void => { + setValueByPath(state.config as Record, CONFIG_KEY_MAP[key], value); }; - // 保存指定配置到后端 const saveConfig = async (key: K): Promise => { - const backendKey = CONFIG_KEY_MAP[key]; - const section = backendKey.split('.')[0] as ConfigSection; - await ConfigService.Set(backendKey, (state.config[section] as any)[key]); + const path = CONFIG_KEY_MAP[key]; + await ConfigService.Set(path, getValueByPath(state.config as Record, path)); }; - // 加载配置 + const activeSyncKey = (gitKey: G, localFSKey: L): G | L => ( + state.config.sync.target === SyncTarget.SyncTargetGit ? gitKey : localFSKey + ); + const initConfig = async (): Promise => { if (state.isLoading) return; state.isLoading = true; try { - const appConfig = await ConfigService.GetConfig(); - - if (appConfig) { - // 合并配置 - if (appConfig.general) Object.assign(state.config.general, appConfig.general); - if (appConfig.editing) Object.assign(state.config.editing, appConfig.editing); - if (appConfig.appearance) Object.assign(state.config.appearance, appConfig.appearance); - if (appConfig.updates) Object.assign(state.config.updates, appConfig.updates); - if (appConfig.backup) Object.assign(state.config.backup, appConfig.backup); - if (appConfig.metadata) Object.assign(state.config.metadata, appConfig.metadata); - } - + applyConfig(await ConfigService.GetConfig()); state.configLoaded = true; - } finally { state.isLoading = false; } }; - // 重置配置 const resetConfig = async (): Promise => { if (state.isLoading) return; state.isLoading = true; try { await ConfigService.ResetConfig(); - const appConfig = await ConfigService.GetConfig(); - if (appConfig) { - state.config = JSON.parse(JSON.stringify(appConfig)) as AppConfig; - } + applyConfig(await ConfigService.GetConfig()); + state.configLoaded = true; } finally { state.isLoading = false; } }; - // 辅助函数:限制数值范围 const clampValue = (value: number, key: NumberConfigKey): number => { const limit = CONFIG_LIMITS[key]; return ConfigUtils.clamp(value, limit.min, limit.max); }; - // 计算属性 const fontConfig = computed(() => ({ fontSize: state.config.editing.fontSize, fontFamily: state.config.editing.fontFamily, @@ -122,7 +133,6 @@ export const useConfigStore = defineStore('config', () => { })); return { - // 状态 config: computed(() => state.config), configLoaded: computed(() => state.configLoaded), isLoading: computed(() => state.isLoading), @@ -130,31 +140,25 @@ export const useConfigStore = defineStore('config', () => { fontConfig, tabConfig, - // 核心方法 initConfig, resetConfig, - // 语言相关方法 - setLanguage: (value: LanguageType) => { - updateConfig('language', value); + setLanguage: async (value: LanguageType) => { + await updateConfig('language', value); locale.value = value as any; }, - // 主题相关方法 setSystemTheme: (value: SystemThemeType) => updateConfig('systemTheme', value), setCurrentTheme: (value: string) => updateConfig('currentTheme', value), - // 字体大小操作 setFontSize: async (value: number) => { await updateConfig('fontSize', clampValue(value, 'fontSize')); }, increaseFontSize: async () => { - const newValue = state.config.editing.fontSize + 1; - await updateConfig('fontSize', clampValue(newValue, 'fontSize')); + await updateConfig('fontSize', clampValue(state.config.editing.fontSize + 1, 'fontSize')); }, decreaseFontSize: async () => { - const newValue = state.config.editing.fontSize - 1; - await updateConfig('fontSize', clampValue(newValue, 'fontSize')); + await updateConfig('fontSize', clampValue(state.config.editing.fontSize - 1, 'fontSize')); }, resetFontSize: async () => { await updateConfig('fontSize', CONFIG_LIMITS.fontSize.default); @@ -169,89 +173,63 @@ export const useConfigStore = defineStore('config', () => { await saveConfig('fontSize'); }, - // 字体操作 setFontFamily: (value: string) => updateConfig('fontFamily', value), setFontWeight: (value: string) => updateConfig('fontWeight', value), - - // 行高操作 setLineHeight: async (value: number) => { await updateConfig('lineHeight', clampValue(value, 'lineHeight')); }, - // Tab操作 setEnableTabIndent: (value: boolean) => updateConfig('enableTabIndent', value), setTabSize: async (value: number) => { await updateConfig('tabSize', clampValue(value, 'tabSize')); }, increaseTabSize: async () => { - const newValue = state.config.editing.tabSize + 1; - await updateConfig('tabSize', clampValue(newValue, 'tabSize')); + await updateConfig('tabSize', clampValue(state.config.editing.tabSize + 1, 'tabSize')); }, decreaseTabSize: async () => { - const newValue = state.config.editing.tabSize - 1; - await updateConfig('tabSize', clampValue(newValue, 'tabSize')); + await updateConfig('tabSize', clampValue(state.config.editing.tabSize - 1, 'tabSize')); }, toggleTabType: async () => { const values = CONFIG_LIMITS.tabType.values; const currentIndex = values.indexOf(state.config.editing.tabType as typeof values[number]); - const nextIndex = (currentIndex + 1) % values.length; - await updateConfig('tabType', values[nextIndex]); + await updateConfig('tabType', values[(currentIndex + 1) % values.length]); }, - // 窗口操作 toggleAlwaysOnTop: async () => { await updateConfig('alwaysOnTop', !state.config.general.alwaysOnTop); await runtime.Window.SetAlwaysOnTop(state.config.general.alwaysOnTop); }, setAlwaysOnTop: (value: boolean) => updateConfig('alwaysOnTop', value), - - // 路径操作 setDataPath: (value: string) => updateConfigLocal('dataPath', value), - - // 保存配置相关方法 setAutoSaveDelay: (value: number) => updateConfig('autoSaveDelay', value), - - // 热键配置相关方法 setEnableGlobalHotkey: (value: boolean) => updateConfig('enableGlobalHotkey', value), setGlobalHotkey: (hotkey: any) => updateConfig('globalHotkey', hotkey), - - // 系统托盘配置相关方法 setEnableSystemTray: (value: boolean) => updateConfig('enableSystemTray', value), - - // 开机启动配置相关方法 setStartAtLogin: async (value: boolean) => { await updateConfig('startAtLogin', value); await StartupService.SetEnabled(value); }, - - // 窗口吸附配置相关方法 setEnableWindowSnap: (value: boolean) => updateConfig('enableWindowSnap', value), - - // 加载动画配置相关方法 setEnableLoadingAnimation: (value: boolean) => updateConfig('enableLoadingAnimation', value), - - // 标签页配置相关方法 setEnableTabs: (value: boolean) => updateConfig('enableTabs', value), - - // 内存监视器配置相关方法 setEnableMemoryMonitor: (value: boolean) => updateConfig('enableMemoryMonitor', value), - - // 快捷键模式配置相关方法 setKeymapMode: (value: any) => updateConfig('keymapMode', value), - - // 更新配置相关方法 setAutoUpdate: (value: boolean) => updateConfig('autoUpdate', value), - // 备份配置相关方法 - setEnableBackup: (value: boolean) => updateConfig('enabled', value), - setAutoBackup: (value: boolean) => updateConfig('auto_backup', value), - setRepoUrl: (value: string) => updateConfig('repo_url', value), - setAuthMethod: (value: AuthMethod) => updateConfig('auth_method', value), - setUsername: (value: string) => updateConfig('username', value), - setPassword: (value: string) => updateConfig('password', value), - setToken: (value: string) => updateConfig('token', value), - setSshKeyPath: (value: string) => updateConfig('ssh_key_path', value), - setSshKeyPassphrase: (value: string) => updateConfig('ssh_key_passphrase', value), - setBackupInterval: (value: number) => updateConfig('backup_interval', value), + setSyncTarget: (value: SyncTarget) => updateConfig('sync_target', value), + setEnableSync: (value: boolean) => updateConfig(activeSyncKey('git_enabled', 'localfs_enabled'), value), + setAutoSync: (value: boolean) => updateConfig(activeSyncKey('git_auto_sync', 'localfs_auto_sync'), value), + setSyncInterval: (value: number) => updateConfig( + activeSyncKey('git_sync_interval', 'localfs_sync_interval'), + Math.max(1, value) + ), + setRepoUrl: (value: string) => updateConfig('git_repo_url', value), + setAuthMethod: (value: AuthMethod) => updateConfig('git_auth_method', value), + setUsername: (value: string) => updateConfig('git_username', value), + setPassword: (value: string) => updateConfig('git_password', value), + setToken: (value: string) => updateConfig('git_token', value), + setSshKeyPath: (value: string) => updateConfig('git_ssh_key_path', value), + setSshKeyPassphrase: (value: string) => updateConfig('git_ssh_key_passphrase', value), + setLocalFSRootPath: (value: string) => updateConfig('localfs_root_path', value), }; -}); \ No newline at end of file +}); diff --git a/frontend/src/stores/backupStore.ts b/frontend/src/stores/syncStore.ts similarity index 61% rename from frontend/src/stores/backupStore.ts rename to frontend/src/stores/syncStore.ts index 7ab02b6c..8cbe4859 100644 --- a/frontend/src/stores/backupStore.ts +++ b/frontend/src/stores/syncStore.ts @@ -1,8 +1,8 @@ import { defineStore } from 'pinia'; import { ref } from 'vue'; -import { BackupService } from '@/../bindings/voidraft/internal/services'; +import { SyncService } from '@/../bindings/voidraft/internal/services'; -export const useBackupStore = defineStore('backup', () => { +export const useSyncStore = defineStore('sync', () => { const isSyncing = ref(false); const sync = async (): Promise => { @@ -11,11 +11,8 @@ export const useBackupStore = defineStore('backup', () => { } isSyncing.value = true; - try { - await BackupService.Sync(); - } catch (e) { - throw e; + await SyncService.Sync(); } finally { isSyncing.value = false; } @@ -25,4 +22,4 @@ export const useBackupStore = defineStore('backup', () => { isSyncing, sync }; -}); \ No newline at end of file +}); diff --git a/frontend/src/views/settings/Settings.vue b/frontend/src/views/settings/Settings.vue index 640f1a49..242301ea 100644 --- a/frontend/src/views/settings/Settings.vue +++ b/frontend/src/views/settings/Settings.vue @@ -18,7 +18,7 @@ const navItems = [ { id: 'general', icon: '⚙️', route: '/settings/general' }, { id: 'editing', icon: '✏️', route: '/settings/editing' }, { id: 'appearance', icon: '🎨', route: '/settings/appearance' }, - { id: 'backupPage', icon: '🔗', route: '/settings/backup' }, + { id: 'syncPage', icon: '🔗', route: '/settings/sync' }, { id: 'extensions', icon: '🧩', route: '/settings/extensions' }, { id: 'keyBindings', icon: '⌨️', route: '/settings/key-bindings' }, { id: 'updates', icon: '🔄', route: '/settings/updates' } @@ -212,4 +212,4 @@ const goBackToEditor = async () => { } - \ No newline at end of file + diff --git a/frontend/src/views/settings/pages/BackupPage.vue b/frontend/src/views/settings/pages/BackupPage.vue deleted file mode 100644 index 36c37f1c..00000000 --- a/frontend/src/views/settings/pages/BackupPage.vue +++ /dev/null @@ -1,321 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/src/views/settings/pages/SyncPage.vue b/frontend/src/views/settings/pages/SyncPage.vue new file mode 100644 index 00000000..b777537b --- /dev/null +++ b/frontend/src/views/settings/pages/SyncPage.vue @@ -0,0 +1,368 @@ + + + + + diff --git a/internal/models/config.go b/internal/models/config.go index 17294581..c3e558ac 100644 --- a/internal/models/config.go +++ b/internal/models/config.go @@ -110,7 +110,7 @@ type UpdatesConfig struct { Github GithubConfig `json:"github"` // GitHub配置 } -// Git备份相关类型定义 +// Git同步相关类型定义 type ( // AuthMethod 定义Git认证方式 AuthMethod string @@ -123,18 +123,43 @@ const ( UserPass AuthMethod = "user_pass" ) -// GitBackupConfig Git备份配置 -type GitBackupConfig struct { - Enabled bool `json:"enabled"` - RepoURL string `json:"repo_url"` - AuthMethod AuthMethod `json:"auth_method"` - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - Token string `json:"token,omitempty"` - SSHKeyPath string `json:"ssh_key_path,omitempty"` - SSHKeyPass string `json:"ssh_key_passphrase,omitempty"` - BackupInterval int `json:"backup_interval"` // 分钟 - AutoBackup bool `json:"auto_backup"` +// SyncTarget 定义当前可选择的同步目标。 +type SyncTarget string + +const ( + // SyncTargetGit 表示 Git 同步。 + SyncTargetGit SyncTarget = "git" + // SyncTargetLocalFS 表示本地文件系统同步。 + SyncTargetLocalFS SyncTarget = "localfs" +) + +// GitSyncConfig 描述 Git 同步配置。 +type GitSyncConfig struct { + Enabled bool `json:"enabled"` + AutoSync bool `json:"auto_sync"` + SyncInterval int `json:"sync_interval"` // 分钟 + RepoURL string `json:"repo_url"` + AuthMethod AuthMethod `json:"auth_method"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Token string `json:"token,omitempty"` + SSHKeyPath string `json:"ssh_key_path,omitempty"` + SSHKeyPass string `json:"ssh_key_passphrase,omitempty"` +} + +// LocalFSSyncConfig 描述本地文件系统同步配置。 +type LocalFSSyncConfig struct { + Enabled bool `json:"enabled"` + AutoSync bool `json:"auto_sync"` + SyncInterval int `json:"sync_interval"` // 分钟 + RootPath string `json:"root_path"` +} + +// SyncConfig 描述同步模块配置。 +type SyncConfig struct { + Target SyncTarget `json:"target"` + Git GitSyncConfig `json:"git"` + LocalFS LocalFSSyncConfig `json:"localfs"` } // AppConfig 应用配置 - 按照前端设置页面分类组织 @@ -143,7 +168,7 @@ type AppConfig struct { Editing EditingConfig `json:"editing"` // 编辑设置 Appearance AppearanceConfig `json:"appearance"` // 外观设置 Updates UpdatesConfig `json:"updates"` // 更新设置 - Backup GitBackupConfig `json:"backup"` // Git备份设置 + Sync SyncConfig `json:"sync"` // 同步设置 Metadata ConfigMetadata `json:"metadata"` // 配置元数据 } @@ -208,16 +233,26 @@ func NewDefaultAppConfig() *AppConfig { Repo: "voidraft", }, }, - Backup: GitBackupConfig{ - Enabled: false, - RepoURL: "", - AuthMethod: UserPass, - Username: "", - Password: "", - Token: "", - SSHKeyPath: "", - BackupInterval: 60, - AutoBackup: false, + Sync: SyncConfig{ + Target: SyncTargetGit, + Git: GitSyncConfig{ + Enabled: false, + AutoSync: false, + SyncInterval: 60, + RepoURL: "", + AuthMethod: UserPass, + Username: "", + Password: "", + Token: "", + SSHKeyPath: "", + SSHKeyPass: "", + }, + LocalFS: LocalFSSyncConfig{ + Enabled: false, + AutoSync: false, + SyncInterval: 60, + RootPath: "", + }, }, Metadata: ConfigMetadata{ LastUpdated: time.Now().Format(time.RFC3339), diff --git a/internal/services/backup_service.go b/internal/services/backup_service.go deleted file mode 100644 index 9e649ffa..00000000 --- a/internal/services/backup_service.go +++ /dev/null @@ -1,1246 +0,0 @@ -package services - -import ( - "bufio" - "context" - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "strings" - "sync" - "time" - "voidraft/internal/common/helper" - - "github.com/go-git/go-git/v5" - gitConfig "github.com/go-git/go-git/v5/config" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/plumbing/transport" - "github.com/go-git/go-git/v5/plumbing/transport/http" - "github.com/go-git/go-git/v5/plumbing/transport/ssh" - "github.com/wailsapp/wails/v3/pkg/application" - "github.com/wailsapp/wails/v3/pkg/services/log" - - "voidraft/internal/models" - "voidraft/internal/models/ent" - "voidraft/internal/models/ent/document" - "voidraft/internal/models/ent/extension" - "voidraft/internal/models/ent/keybinding" - "voidraft/internal/models/ent/theme" - "voidraft/internal/models/schema/mixin" -) - -const ( - backupDir = "backup" // Git 仓库目录,JSONL 文件直接放这里 - remoteName = "origin" - branchName = "master" - maxRetries = 3 - jsonlSuffix = ".jsonl" - - // 通用字段名 - fieldUUID = "uuid" - fieldUpdatedAt = "updated_at" -) - -// 定义错误 -var ( - ErrNotInitialized = errors.New("backup service not initialized") - ErrDisabled = errors.New("backup is disabled") - ErrPushFailed = errors.New("push failed after max retries") -) - -// BackupService 提供基于Git的备份同步功能 -type BackupService struct { - configService *ConfigService - dbService *DatabaseService - repository *git.Repository - logger *log.LogService - isInitialized bool - autoBackupTicker *time.Ticker - autoBackupStop chan bool - autoBackupWg sync.WaitGroup - mu sync.Mutex - cancelObservers []helper.CancelFunc -} - -// NewBackupService 创建新的备份服务实例 -func NewBackupService(configService *ConfigService, dbService *DatabaseService, logger *log.LogService) *BackupService { - return &BackupService{ - configService: configService, - dbService: dbService, - logger: logger, - } -} - -func (s *BackupService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { - // 监听 backup 配置变化 - s.cancelObservers = []helper.CancelFunc{ - s.configService.Watch("backup", s.onBackupConfigChange), - s.configService.Watch("general.dataPath", s.onDataPathChange), - } - if err := s.Initialize(); err != nil { - s.logger.Error("initializing backup service: %v", err) - } - return nil -} - -func (s *BackupService) onBackupConfigChange(oldValue, newValue interface{}) { - config, err := s.configService.GetConfig() - if err != nil { - return - } - _ = s.HandleConfigChange(&config.Backup) -} - -func (s *BackupService) onDataPathChange(oldValue, newValue interface{}) { - if err := s.Reinitialize(); err != nil { - s.logger.Error("Failed to reinitialize backup service after data path change: %v", err) - } -} - -// Initialize 初始化备份服务 -func (s *BackupService) Initialize() error { - config, repoPath, err := s.getConfigAndPath() - if err != nil { - return fmt.Errorf("getting backup config: %w", err) - } - - if !config.Enabled { - return nil - } - - // 仓库地址为空时不初始化 - if strings.TrimSpace(config.RepoURL) == "" { - return nil - } - - if err := s.initializeRepository(config, repoPath); err != nil { - return fmt.Errorf("initializing repository: %w", err) - } - - if err := s.verifyRemoteConnection(config); err != nil { - return fmt.Errorf("verifying remote connection: %w", err) - } - - if config.AutoBackup && config.BackupInterval > 0 { - _ = s.StartAutoBackup() - } - - s.mu.Lock() - s.isInitialized = true - s.mu.Unlock() - - return nil -} - -func (s *BackupService) getConfigAndPath() (*models.GitBackupConfig, string, error) { - appConfig, err := s.configService.GetConfig() - if err != nil { - return nil, "", fmt.Errorf("getting app config: %w", err) - } - // 返回 backup 目录作为 Git 仓库路径 - repoPath := filepath.Join(appConfig.General.DataPath, backupDir) - return &appConfig.Backup, repoPath, nil -} - -func (s *BackupService) initializeRepository(config *models.GitBackupConfig, repoPath string) error { - // 确保父目录存在 - if err := os.MkdirAll(repoPath, 0755); err != nil { - return fmt.Errorf("creating backup directory: %w", err) - } - - gitPath := filepath.Join(repoPath, ".git") - if _, err := os.Stat(gitPath); os.IsNotExist(err) { - repo, err := git.PlainInit(repoPath, false) - if err != nil { - return fmt.Errorf("initializing repository: %w", err) - } - s.repository = repo - - // 创建 .gitignore - gitignorePath := filepath.Join(repoPath, ".gitignore") - if _, err := os.Stat(gitignorePath); os.IsNotExist(err) { - _ = os.WriteFile(gitignorePath, []byte("*.tmp\n*.log\n"), 0644) - } - } else if err != nil { - return fmt.Errorf("checking repository path: %w", err) - } else { - repo, err := git.PlainOpen(repoPath) - if err != nil { - return fmt.Errorf("opening repository: %w", err) - } - s.repository = repo - } - - return s.setupRemote(config.RepoURL) -} - -func (s *BackupService) setupRemote(repoURL string) error { - remote, err := s.repository.Remote(remoteName) - if errors.Is(err, git.ErrRemoteNotFound) { - _, err = s.repository.CreateRemote(&gitConfig.RemoteConfig{ - Name: remoteName, - URLs: []string{repoURL}, - }) - return err - } - if err != nil { - return err - } - - if len(remote.Config().URLs) > 0 && remote.Config().URLs[0] != repoURL { - if err := s.repository.DeleteRemote(remoteName); err != nil { - return err - } - _, err = s.repository.CreateRemote(&gitConfig.RemoteConfig{ - Name: remoteName, - URLs: []string{repoURL}, - }) - return err - } - return nil -} - -func (s *BackupService) verifyRemoteConnection(config *models.GitBackupConfig) error { - auth, err := s.getAuthMethod(config) - if err != nil { - return err - } - - remote, err := s.repository.Remote(remoteName) - if err != nil { - return err - } - - // 验证能否连接远程仓库,空仓库返回空列表是正常的 - _, err = remote.List(&git.ListOptions{Auth: auth}) - if err != nil { - // 空仓库或无引用是允许的(第一次同步场景) - if strings.Contains(err.Error(), "empty") || strings.Contains(err.Error(), "no reference") { - return nil - } - return err - } - return nil -} - -func (s *BackupService) getAuthMethod(config *models.GitBackupConfig) (transport.AuthMethod, error) { - switch config.AuthMethod { - case models.Token: - if config.Token == "" { - return nil, errors.New("token required") - } - return &http.BasicAuth{Username: "git", Password: config.Token}, nil - - case models.UserPass: - if config.Username == "" || config.Password == "" { - return nil, errors.New("username and password required") - } - return &http.BasicAuth{Username: config.Username, Password: config.Password}, nil - - case models.SSHKey: - if config.SSHKeyPath == "" { - return nil, errors.New("SSH key path required") - } - return ssh.NewPublicKeysFromFile("git", config.SSHKeyPath, config.SSHKeyPass) - - default: - return nil, fmt.Errorf("unsupported auth method: %s", config.AuthMethod) - } -} - -// Sync 执行完整的同步流程:导出 -> commit -> pull -> 解决冲突 -> push -> 导入 -func (s *BackupService) Sync() error { - config, repoPath, err := s.getConfigAndPath() - if err != nil { - return err - } - - if !config.Enabled { - return ErrDisabled - } - - // 检查仓库地址是否配置 - if strings.TrimSpace(config.RepoURL) == "" { - return errors.New("repository URL is not configured") - } - - // 如果未初始化,尝试初始化 - s.mu.Lock() - initialized := s.isInitialized - s.mu.Unlock() - - if !initialized { - if err := s.Initialize(); err != nil { - return fmt.Errorf("initializing backup service: %w", err) - } - s.mu.Lock() - initialized = s.isInitialized - s.mu.Unlock() - if !initialized { - return ErrNotInitialized - } - } - - s.mu.Lock() - defer s.mu.Unlock() - - ctx := context.Background() - - auth, err := s.getAuthMethod(config) - if err != nil { - return err - } - - // 1. 拉取远程更新到本地工作区 - if err := s.fetchAndMergeRemote(auth, repoPath); err != nil { - s.logger.Warning("fetch remote: %v", err) - } - - // 2. 先将远程 JSONL 导入本地数据库(用 updated_at 解决记录级冲突) - if err := s.importAll(ctx, repoPath); err != nil { - s.logger.Warning("importing remote data: %v", err) - } - - // 3. 导出合并后的本地数据库到 JSONL - if err := s.exportAll(ctx, repoPath); err != nil { - return fmt.Errorf("exporting data: %w", err) - } - - // 4. 提交更改 - if _, err := s.commitChanges(); err != nil { - return fmt.Errorf("committing changes: %w", err) - } - - // 5. 推送到远程(带重试) - if err := s.pushWithRetry(auth, repoPath); err != nil { - return fmt.Errorf("pushing: %w", err) - } - - return nil -} - -// exportAll 导出所有表到 JSONL 文件 -func (s *BackupService) exportAll(ctx context.Context, dataPath string) error { - // 使用 SkipSoftDelete 获取所有数据(包括已删除的) - ctx = mixin.SkipSoftDelete(ctx) - client := s.dbService.Client - - // 定义导出任务 - exports := []struct { - name string - fn func() error - }{ - {"documents", func() error { - docs, err := client.Document.Query().Order(document.ByUUID()).All(ctx) - if err != nil { - return err - } - return writeJSONLFile(filepath.Join(dataPath, "documents"+jsonlSuffix), docs) - }}, - {"extensions", func() error { - items, err := client.Extension.Query().Order(extension.ByUUID()).All(ctx) - if err != nil { - return err - } - return writeJSONLFile(filepath.Join(dataPath, "extensions"+jsonlSuffix), items) - }}, - {"keybindings", func() error { - items, err := client.KeyBinding.Query().Order(keybinding.ByUUID()).All(ctx) - if err != nil { - return err - } - return writeJSONLFile(filepath.Join(dataPath, "keybindings"+jsonlSuffix), items) - }}, - {"themes", func() error { - items, err := client.Theme.Query().Order(theme.ByUUID()).All(ctx) - if err != nil { - return err - } - return writeJSONLFile(filepath.Join(dataPath, "themes"+jsonlSuffix), items) - }}, - } - - for _, export := range exports { - if err := export.fn(); err != nil { - return fmt.Errorf("exporting %s: %w", export.name, err) - } - } - - return nil -} - -// writeJSONLFile 使用泛型写入 JSONL 文件 -func writeJSONLFile[T any](filePath string, items []T) error { - file, err := os.Create(filePath) - if err != nil { - return err - } - defer file.Close() - - writer := bufio.NewWriter(file) - defer writer.Flush() - - for _, item := range items { - data, err := json.Marshal(item) - if err != nil { - return err - } - if _, err := writer.Write(data); err != nil { - return err - } - if err := writer.WriteByte('\n'); err != nil { - return err - } - } - - return nil -} - -func (s *BackupService) commitChanges() (bool, error) { - w, err := s.repository.Worktree() - if err != nil { - return false, err - } - - // 添加所有变更 - if err := w.AddGlob("*.jsonl"); err != nil { - // 如果没有文件匹配,不是错误 - if !strings.Contains(err.Error(), "no matches found") { - return false, err - } - } - - status, err := w.Status() - if err != nil { - return false, err - } - - if status.IsClean() { - return false, nil - } - - _, err = w.Commit(fmt.Sprintf("Backup %s", time.Now().Format("2006-01-02 15:04:05")), &git.CommitOptions{ - Author: &object.Signature{ - Name: "voidraft", - Email: "backup@voidraft.app", - When: time.Now(), - }, - }) - if err != nil { - return false, err - } - - return true, nil -} - -// fetchAndMergeRemote 拉取远程更新并合并 -func (s *BackupService) fetchAndMergeRemote(auth transport.AuthMethod, dataPath string) error { - // 检查本地是否有 HEAD(是否有任何 commit) - head, err := s.repository.Head() - hasLocalCommits := err == nil && head != nil - - // 先 fetch 远程 - err = s.repository.Fetch(&git.FetchOptions{ - RemoteName: remoteName, - Auth: auth, - }) - if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { - // 远程分支不存在是正常的(首次推送) - if strings.Contains(err.Error(), "couldn't find remote ref") { - return nil - } - return fmt.Errorf("fetching: %w", err) - } - - // 获取远程分支引用 - remoteRef, err := s.repository.Reference(plumbing.NewRemoteReferenceName(remoteName, branchName), true) - if err != nil { - // 远程分支不存在,正常情况 - return nil - } - - // 如果本地没有 commit,直接 checkout 远程分支 - if !hasLocalCommits { - w, err := s.repository.Worktree() - if err != nil { - return err - } - - // 创建本地分支指向远程 - err = w.Checkout(&git.CheckoutOptions{ - Hash: remoteRef.Hash(), - Branch: plumbing.NewBranchReferenceName(branchName), - Create: true, - Force: true, - }) - if err != nil { - return fmt.Errorf("checkout remote: %w", err) - } - return nil - } - - // 本地有 commit,尝试 pull 合并 - w, err := s.repository.Worktree() - if err != nil { - return err - } - - err = w.Pull(&git.PullOptions{ - RemoteName: remoteName, - ReferenceName: plumbing.NewBranchReferenceName(branchName), - Auth: auth, - }) - - if err == nil || errors.Is(err, git.NoErrAlreadyUpToDate) { - return nil - } - - // 处理合并冲突 - if errors.Is(err, git.ErrNonFastForwardUpdate) || - strings.Contains(err.Error(), "conflict") || - strings.Contains(err.Error(), "merge") { - return s.resolveConflicts(dataPath) - } - - // 远程分支不存在(首次推送) - if strings.Contains(err.Error(), "reference not found") || - strings.Contains(err.Error(), "couldn't find remote ref") { - return nil - } - - return err -} - -// pushWithRetry 推送到远程,带重试逻辑 -func (s *BackupService) pushWithRetry(auth transport.AuthMethod, dataPath string) error { - for i := 0; i < maxRetries; i++ { - err := s.repository.Push(&git.PushOptions{ - RemoteName: remoteName, - Auth: auth, - }) - - switch { - case err == nil, errors.Is(err, git.NoErrAlreadyUpToDate): - return nil - - case errors.Is(err, git.ErrNonFastForwardUpdate): - // 非快进更新,需要先拉取合并 - if mergeErr := s.fetchAndMergeRemote(auth, dataPath); mergeErr != nil { - return fmt.Errorf("merge before push: %w", mergeErr) - } - _, _ = s.commitChanges() - continue - - default: - return err - } - } - - return ErrPushFailed -} - -// resolveConflicts 解决 JSONL 文件中的冲突(Last Write Wins) -func (s *BackupService) resolveConflicts(dataPath string) error { - files, err := filepath.Glob(filepath.Join(dataPath, "*.jsonl")) - if err != nil { - return err - } - - for _, file := range files { - content, err := os.ReadFile(file) - if err != nil { - continue - } - - // 检查是否有冲突标记 - if !strings.Contains(string(content), "<<<<<<<") { - continue - } - - resolved, err := s.resolveJSONLConflict(string(content)) - if err != nil { - return fmt.Errorf("resolving conflict in %s: %w", file, err) - } - - if err := os.WriteFile(file, []byte(resolved), 0644); err != nil { - return err - } - } - - // 提交解决后的冲突 - w, err := s.repository.Worktree() - if err != nil { - return err - } - - if err := w.AddGlob("*.jsonl"); err != nil { - return err - } - - _, err = w.Commit("Auto-resolve sync conflicts", &git.CommitOptions{ - Author: &object.Signature{ - Name: "voidraft", - Email: "backup@voidraft.app", - When: time.Now(), - }, - }) - - return err -} - -// resolveJSONLConflict 解析并解决 JSONL 文件中的 Git 冲突 -func (s *BackupService) resolveJSONLConflict(content string) (string, error) { - lines := strings.Split(content, "\n") - var result []string - - var localLines, remoteLines []string - inConflict := false - isLocal := true - - for _, line := range lines { - if strings.HasPrefix(line, "<<<<<<<") { - inConflict = true - isLocal = true - localLines = nil - remoteLines = nil - continue - } - if strings.HasPrefix(line, "=======") { - isLocal = false - continue - } - if strings.HasPrefix(line, ">>>>>>>") { - // 解决这个冲突块 - resolved := s.mergeConflictBlock(localLines, remoteLines) - result = append(result, resolved...) - inConflict = false - continue - } - - if inConflict { - if isLocal { - if line != "" { - localLines = append(localLines, line) - } - } else { - if line != "" { - remoteLines = append(remoteLines, line) - } - } - } else { - result = append(result, line) - } - } - - return strings.Join(result, "\n"), nil -} - -// mergeConflictBlock 合并冲突块,使用 Last Write Wins 策略 -func (s *BackupService) mergeConflictBlock(localLines, remoteLines []string) []string { - // 解析本地和远程的记录 - localRecords := s.parseRecords(localLines) - remoteRecords := s.parseRecords(remoteLines) - - // 合并:按 UUID 索引,updated_at 更新的记录获胜 - merged := make(map[string]map[string]interface{}) - mergedOrder := []string{} - - // 先添加本地记录 - for _, record := range localRecords { - uuid, ok := record[fieldUUID].(string) - if !ok { - continue - } - merged[uuid] = record - mergedOrder = append(mergedOrder, uuid) - } - - // 合并远程记录 - for _, record := range remoteRecords { - uuid, ok := record[fieldUUID].(string) - if !ok { - continue - } - - existing, exists := merged[uuid] - if !exists { - merged[uuid] = record - mergedOrder = append(mergedOrder, uuid) - } else { - // 比较 updated_at,更新的获胜 - localTime := s.parseTime(existing[fieldUpdatedAt]) - remoteTime := s.parseTime(record[fieldUpdatedAt]) - if remoteTime.After(localTime) { - merged[uuid] = record - } - } - } - - // 转回 JSON 行 - var result []string - for _, uuid := range mergedOrder { - if record, ok := merged[uuid]; ok { - data, _ := json.Marshal(record) - result = append(result, string(data)) - delete(merged, uuid) // 避免重复 - } - } - - return result -} - -func (s *BackupService) parseRecords(lines []string) []map[string]interface{} { - var records []map[string]interface{} - for _, line := range lines { - var record map[string]interface{} - if err := json.Unmarshal([]byte(line), &record); err == nil { - records = append(records, record) - } - } - return records -} - -func (s *BackupService) parseTime(v interface{}) time.Time { - if str, ok := v.(string); ok { - t, _ := time.Parse(time.RFC3339, str) - return t - } - return time.Time{} -} - -// importAll 从 JSONL 文件导入数据到数据库 -func (s *BackupService) importAll(ctx context.Context, dataPath string) error { - client := s.dbService.Client - - // 定义导入任务 - imports := []struct { - name string - fn func() error - }{ - {"documents", func() error { return s.importDocuments(ctx, client, dataPath) }}, - {"extensions", func() error { return s.importExtensions(ctx, client, dataPath) }}, - {"keybindings", func() error { return s.importKeyBindings(ctx, client, dataPath) }}, - {"themes", func() error { return s.importThemes(ctx, client, dataPath) }}, - } - - for _, imp := range imports { - if err := imp.fn(); err != nil { - s.logger.Error("importing %s: %v", imp.name, err) - } - } - - return nil -} - -func (s *BackupService) importDocuments(ctx context.Context, client *ent.Client, dataPath string) error { - filePath := filepath.Join(dataPath, "documents.jsonl") - records, err := s.readJSONL(filePath) - if err != nil { - return err - } - - // 跳过软删除过滤和自动更新时间 - importCtx := mixin.SkipAutoUpdate(mixin.SkipSoftDelete(ctx)) - - for _, record := range records { - uuid, _ := record[document.FieldUUID].(string) - if uuid == "" { - continue - } - - // 查找现有记录 - found, err := client.Document.Query(). - Where(document.UUIDEQ(uuid)). - First(importCtx) - - remoteTime := s.parseTime(record[document.FieldUpdatedAt]) - - if err != nil || found == nil { - // 新记录,创建 - if err := s.createDocument(importCtx, client, record); err != nil { - s.logger.Error("creating document: %v", err) - } - } else { - // 比较时间,更新的获胜 - localTime, _ := time.Parse(time.RFC3339, found.UpdatedAt) - if remoteTime.After(localTime) { - if err := s.updateDocument(importCtx, client, found.ID, record); err != nil { - s.logger.Error("updating document: %v", err) - } - } - } - } - - return nil -} - -func (s *BackupService) createDocument(ctx context.Context, client *ent.Client, record map[string]interface{}) error { - builder := client.Document.Create() - if v, ok := record[document.FieldUUID].(string); ok { - builder.SetUUID(v) - } - if v, ok := record[document.FieldTitle].(string); ok { - builder.SetTitle(v) - } - if v, ok := record[document.FieldContent].(string); ok { - builder.SetContent(v) - } - if v, ok := record[document.FieldLocked].(bool); ok { - builder.SetLocked(v) - } - if v, ok := record[document.FieldCreatedAt].(string); ok { - builder.SetCreatedAt(v) - } - if v, ok := record[document.FieldUpdatedAt].(string); ok { - builder.SetUpdatedAt(v) - } - if v, ok := record[document.FieldDeletedAt].(string); ok { - builder.SetDeletedAt(v) - } - return builder.Exec(ctx) -} - -func (s *BackupService) updateDocument(ctx context.Context, client *ent.Client, id int, record map[string]interface{}) error { - builder := client.Document.UpdateOneID(id) - if v, ok := record[document.FieldTitle].(string); ok { - builder.SetTitle(v) - } - if v, ok := record[document.FieldContent].(string); ok { - builder.SetContent(v) - } - if v, ok := record[document.FieldLocked].(bool); ok { - builder.SetLocked(v) - } - if v, ok := record[document.FieldUpdatedAt].(string); ok { - builder.SetUpdatedAt(v) - } - if v, ok := record[document.FieldDeletedAt].(string); ok { - builder.SetDeletedAt(v) - } else { - builder.ClearDeletedAt() - } - return builder.Exec(ctx) -} - -func (s *BackupService) importExtensions(ctx context.Context, client *ent.Client, dataPath string) error { - filePath := filepath.Join(dataPath, "extensions.jsonl") - records, err := s.readJSONL(filePath) - if err != nil { - return err - } - - importCtx := mixin.SkipAutoUpdate(mixin.SkipSoftDelete(ctx)) - - for _, record := range records { - uuid, _ := record[extension.FieldUUID].(string) - if uuid == "" { - continue - } - - found, err := client.Extension.Query(). - Where(extension.UUIDEQ(uuid)). - First(importCtx) - - remoteTime := s.parseTime(record[extension.FieldUpdatedAt]) - - if err != nil || found == nil { - if err := s.createExtension(importCtx, client, record); err != nil { - s.logger.Error("creating extension: %v", err) - } - } else { - localTime, _ := time.Parse(time.RFC3339, found.UpdatedAt) - if remoteTime.After(localTime) { - if err := s.updateExtension(importCtx, client, found.ID, record); err != nil { - s.logger.Error("updating extension: %v", err) - } - } - } - } - - return nil -} - -func (s *BackupService) createExtension(ctx context.Context, client *ent.Client, record map[string]interface{}) error { - builder := client.Extension.Create() - if v, ok := record[extension.FieldUUID].(string); ok { - builder.SetUUID(v) - } - if v, ok := record[extension.FieldName].(string); ok { - builder.SetName(v) - } - if v, ok := record[extension.FieldEnabled].(bool); ok { - builder.SetEnabled(v) - } - if v, ok := record[extension.FieldConfig].(map[string]interface{}); ok { - builder.SetConfig(v) - } - if v, ok := record[extension.FieldCreatedAt].(string); ok { - builder.SetCreatedAt(v) - } - if v, ok := record[extension.FieldUpdatedAt].(string); ok { - builder.SetUpdatedAt(v) - } - if v, ok := record[extension.FieldDeletedAt].(string); ok { - builder.SetDeletedAt(v) - } - return builder.Exec(ctx) -} - -func (s *BackupService) updateExtension(ctx context.Context, client *ent.Client, id int, record map[string]interface{}) error { - builder := client.Extension.UpdateOneID(id) - if v, ok := record[extension.FieldName].(string); ok { - builder.SetName(v) - } - if v, ok := record[extension.FieldEnabled].(bool); ok { - builder.SetEnabled(v) - } - if v, ok := record[extension.FieldConfig].(map[string]interface{}); ok { - builder.SetConfig(v) - } - if v, ok := record[extension.FieldUpdatedAt].(string); ok { - builder.SetUpdatedAt(v) - } - if v, ok := record[extension.FieldDeletedAt].(string); ok { - builder.SetDeletedAt(v) - } else { - builder.ClearDeletedAt() - } - return builder.Exec(ctx) -} - -func (s *BackupService) importKeyBindings(ctx context.Context, client *ent.Client, dataPath string) error { - filePath := filepath.Join(dataPath, "keybindings.jsonl") - records, err := s.readJSONL(filePath) - if err != nil { - return err - } - - importCtx := mixin.SkipAutoUpdate(mixin.SkipSoftDelete(ctx)) - - for _, record := range records { - uuid, _ := record[keybinding.FieldUUID].(string) - if uuid == "" { - continue - } - - found, err := client.KeyBinding.Query(). - Where(keybinding.UUIDEQ(uuid)). - First(importCtx) - - remoteTime := s.parseTime(record[keybinding.FieldUpdatedAt]) - - if err != nil || found == nil { - if err := s.createKeyBinding(importCtx, client, record); err != nil { - s.logger.Error("creating keybinding: %v", err) - } - } else { - localTime, _ := time.Parse(time.RFC3339, found.UpdatedAt) - if remoteTime.After(localTime) { - if err := s.updateKeyBinding(importCtx, client, found.ID, record); err != nil { - s.logger.Error("updating keybinding: %v", err) - } - } - } - } - - return nil -} - -func (s *BackupService) createKeyBinding(ctx context.Context, client *ent.Client, record map[string]interface{}) error { - builder := client.KeyBinding.Create() - if v, ok := record[keybinding.FieldUUID].(string); ok { - builder.SetUUID(v) - } - if v, ok := record[keybinding.FieldName].(string); ok { - builder.SetName(v) - } - if v, ok := record[keybinding.FieldKey].(string); ok { - builder.SetKey(v) - } - if v, ok := record[keybinding.FieldMacos].(string); ok { - builder.SetMacos(v) - } - if v, ok := record[keybinding.FieldWindows].(string); ok { - builder.SetWindows(v) - } - if v, ok := record[keybinding.FieldLinux].(string); ok { - builder.SetLinux(v) - } - if v, ok := record[keybinding.FieldExtension].(string); ok { - builder.SetExtension(v) - } - if v, ok := record[keybinding.FieldEnabled].(bool); ok { - builder.SetEnabled(v) - } - if v, ok := record[keybinding.FieldPreventDefault].(bool); ok { - builder.SetPreventDefault(v) - } - if v, ok := record[keybinding.FieldScope].(string); ok { - builder.SetScope(v) - } - if v, ok := record[keybinding.FieldCreatedAt].(string); ok { - builder.SetCreatedAt(v) - } - if v, ok := record[keybinding.FieldUpdatedAt].(string); ok { - builder.SetUpdatedAt(v) - } - if v, ok := record[keybinding.FieldDeletedAt].(string); ok { - builder.SetDeletedAt(v) - } - return builder.Exec(ctx) -} - -func (s *BackupService) updateKeyBinding(ctx context.Context, client *ent.Client, id int, record map[string]interface{}) error { - builder := client.KeyBinding.UpdateOneID(id) - if v, ok := record[keybinding.FieldName].(string); ok { - builder.SetName(v) - } - if v, ok := record[keybinding.FieldKey].(string); ok { - builder.SetKey(v) - } - if v, ok := record[keybinding.FieldMacos].(string); ok { - builder.SetMacos(v) - } - if v, ok := record[keybinding.FieldWindows].(string); ok { - builder.SetWindows(v) - } - if v, ok := record[keybinding.FieldLinux].(string); ok { - builder.SetLinux(v) - } - if v, ok := record[keybinding.FieldExtension].(string); ok { - builder.SetExtension(v) - } - if v, ok := record[keybinding.FieldEnabled].(bool); ok { - builder.SetEnabled(v) - } - if v, ok := record[keybinding.FieldPreventDefault].(bool); ok { - builder.SetPreventDefault(v) - } - if v, ok := record[keybinding.FieldScope].(string); ok { - builder.SetScope(v) - } - if v, ok := record[keybinding.FieldUpdatedAt].(string); ok { - builder.SetUpdatedAt(v) - } - if v, ok := record[keybinding.FieldDeletedAt].(string); ok { - builder.SetDeletedAt(v) - } else { - builder.ClearDeletedAt() - } - return builder.Exec(ctx) -} - -func (s *BackupService) importThemes(ctx context.Context, client *ent.Client, dataPath string) error { - filePath := filepath.Join(dataPath, "themes.jsonl") - records, err := s.readJSONL(filePath) - if err != nil { - return err - } - - importCtx := mixin.SkipAutoUpdate(mixin.SkipSoftDelete(ctx)) - - for _, record := range records { - uuid, _ := record[theme.FieldUUID].(string) - if uuid == "" { - continue - } - - found, err := client.Theme.Query(). - Where(theme.UUIDEQ(uuid)). - First(importCtx) - - remoteTime := s.parseTime(record[theme.FieldUpdatedAt]) - - if err != nil || found == nil { - if err := s.createTheme(importCtx, client, record); err != nil { - s.logger.Error("creating theme: %v", err) - } - } else { - localTime, _ := time.Parse(time.RFC3339, found.UpdatedAt) - if remoteTime.After(localTime) { - if err := s.updateTheme(importCtx, client, found.ID, record); err != nil { - s.logger.Error("updating theme: %v", err) - } - } - } - } - - return nil -} - -func (s *BackupService) createTheme(ctx context.Context, client *ent.Client, record map[string]interface{}) error { - builder := client.Theme.Create() - if v, ok := record[theme.FieldUUID].(string); ok { - builder.SetUUID(v) - } - if v, ok := record[theme.FieldName].(string); ok { - builder.SetName(v) - } - if v, ok := record[theme.FieldType].(string); ok { - builder.SetType(theme.Type(v)) - } - if v, ok := record[theme.FieldColors].(map[string]interface{}); ok { - builder.SetColors(v) - } - if v, ok := record[theme.FieldCreatedAt].(string); ok { - builder.SetCreatedAt(v) - } - if v, ok := record[theme.FieldUpdatedAt].(string); ok { - builder.SetUpdatedAt(v) - } - if v, ok := record[theme.FieldDeletedAt].(string); ok { - builder.SetDeletedAt(v) - } - return builder.Exec(ctx) -} - -func (s *BackupService) updateTheme(ctx context.Context, client *ent.Client, id int, record map[string]interface{}) error { - builder := client.Theme.UpdateOneID(id) - if v, ok := record[theme.FieldName].(string); ok { - builder.SetName(v) - } - if v, ok := record[theme.FieldType].(string); ok { - builder.SetType(theme.Type(v)) - } - if v, ok := record[theme.FieldColors].(map[string]interface{}); ok { - builder.SetColors(v) - } - if v, ok := record[theme.FieldUpdatedAt].(string); ok { - builder.SetUpdatedAt(v) - } - if v, ok := record[theme.FieldDeletedAt].(string); ok { - builder.SetDeletedAt(v) - } else { - builder.ClearDeletedAt() - } - return builder.Exec(ctx) -} - -func (s *BackupService) readJSONL(filePath string) ([]map[string]interface{}, error) { - file, err := os.Open(filePath) - if err != nil { - if os.IsNotExist(err) { - return nil, nil - } - return nil, err - } - defer file.Close() - - var records []map[string]interface{} - scanner := bufio.NewScanner(file) - // 增加 buffer 大小以处理大行 - scanner.Buffer(make([]byte, 1024*1024), 1024*1024) - - for scanner.Scan() { - line := scanner.Text() - if line == "" { - continue - } - var record map[string]interface{} - if err := json.Unmarshal([]byte(line), &record); err == nil { - records = append(records, record) - } - } - - return records, scanner.Err() -} - -// StartAutoBackup 启动自动备份 -func (s *BackupService) StartAutoBackup() error { - config, _, err := s.getConfigAndPath() - if err != nil { - return err - } - - if !config.AutoBackup || config.BackupInterval <= 0 { - return nil - } - - s.StopAutoBackup() - - s.autoBackupTicker = time.NewTicker(time.Duration(config.BackupInterval) * time.Minute) - s.autoBackupStop = make(chan bool) - - s.autoBackupWg.Add(1) - go func() { - defer s.autoBackupWg.Done() - for { - select { - case <-s.autoBackupTicker.C: - if err := s.Sync(); err != nil { - s.logger.Error("auto backup failed: %v", err) - } - case <-s.autoBackupStop: - return - } - } - }() - - return nil -} - -// StopAutoBackup 停止自动备份 -func (s *BackupService) StopAutoBackup() { - // 先停止 ticker - if s.autoBackupTicker != nil { - s.autoBackupTicker.Stop() - s.autoBackupTicker = nil - } - - // 安全关闭 channel(只关闭一次) - if s.autoBackupStop != nil { - select { - case <-s.autoBackupStop: - // channel 已关闭,不做任何事 - default: - close(s.autoBackupStop) - } - s.autoBackupWg.Wait() - s.autoBackupStop = nil - } -} - -// Reinitialize 重新初始化 -func (s *BackupService) Reinitialize() error { - s.StopAutoBackup() - - s.mu.Lock() - s.isInitialized = false - s.mu.Unlock() - - return s.Initialize() -} - -// HandleConfigChange 处理配置变更 -func (s *BackupService) HandleConfigChange(config *models.GitBackupConfig) error { - s.mu.Lock() - initialized := s.isInitialized - s.mu.Unlock() - - if !config.Enabled { - s.StopAutoBackup() - s.mu.Lock() - s.isInitialized = false - s.mu.Unlock() - return nil - } - - if initialized { - return s.Reinitialize() - } - - return s.Initialize() -} - -// ServiceShutdown 服务关闭 -func (s *BackupService) ServiceShutdown() { - for _, cancel := range s.cancelObservers { - if cancel != nil { - cancel() - } - } - s.StopAutoBackup() -} diff --git a/internal/services/service_manager.go b/internal/services/service_manager.go index 9314cdc4..395cb7bf 100644 --- a/internal/services/service_manager.go +++ b/internal/services/service_manager.go @@ -29,7 +29,7 @@ type ServiceManager struct { badgeService *dock.DockService notificationService *notifications.NotificationService testService *TestService // 测试服务(仅开发环境) - BackupService *BackupService + SyncService *SyncService httpClientService *HttpClientService // HTTP客户端服务 logger *log.LogService } @@ -95,8 +95,8 @@ func NewServiceManager() *ServiceManager { // 初始化主题服务 themeService := NewThemeService(databaseService, logger) - // 初始化备份服务 - backupService := NewBackupService(configService, databaseService, logger) + // 初始化同步服务 + syncService := NewSyncService(configService, databaseService, logger) // 初始化HTTP客户端服务 httpClientService := NewHttpClientService(logger) @@ -124,7 +124,7 @@ func NewServiceManager() *ServiceManager { badgeService: badgeService, notificationService: notificationService, testService: testService, - BackupService: backupService, + SyncService: syncService, httpClientService: httpClientService, logger: logger, } @@ -150,7 +150,7 @@ func (sm *ServiceManager) GetServices() []application.Service { application.NewService(sm.badgeService), application.NewService(sm.notificationService), application.NewService(sm.testService), - application.NewService(sm.BackupService), + application.NewService(sm.SyncService), application.NewService(sm.httpClientService), } return services diff --git a/internal/services/sync_service.go b/internal/services/sync_service.go new file mode 100644 index 00000000..6f310183 --- /dev/null +++ b/internal/services/sync_service.go @@ -0,0 +1,242 @@ +package services + +import ( + "context" + "fmt" + "path/filepath" + "time" + "voidraft/internal/common/helper" + "voidraft/internal/models" + "voidraft/internal/syncer" + + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/services/log" +) + +const ( + syncDir = "sync" + localFSHeadKey = "head.json" +) + +// SyncService 提供应用层同步服务入口。 +type SyncService struct { + configService *ConfigService + dbService *DatabaseService + logger *log.LogService + app *syncer.App + cancelObservers []helper.CancelFunc +} + +// NewSyncService 创建新的同步服务实例。 +func NewSyncService(configService *ConfigService, dbService *DatabaseService, logger *log.LogService) *SyncService { + return &SyncService{ + configService: configService, + dbService: dbService, + logger: logger, + } +} + +// ServiceStartup 在服务启动时初始化同步系统。 +func (s *SyncService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + _ = options + + if err := s.ensureApp(); err != nil { + return err + } + + s.cancelObservers = []helper.CancelFunc{ + s.configService.Watch("sync", s.onSyncConfigChange), + s.configService.Watch("general.dataPath", s.onDataPathChange), + } + + if err := s.Initialize(); err != nil { + s.logger.Error("initializing sync service: %v", err) + } + + return nil +} + +// Initialize 重新加载配置并启动自动同步。 +func (s *SyncService) Initialize() error { + if err := s.ensureApp(); err != nil { + return err + } + + config, err := s.buildConfig() + if err != nil { + return err + } + + if err := s.app.Reconfigure(context.Background(), config); err != nil { + return fmt.Errorf("reconfigure sync app: %w", err) + } + if err := s.app.Start(context.Background()); err != nil { + return fmt.Errorf("start sync app: %w", err) + } + return nil +} + +// Reinitialize 重新初始化同步服务。 +func (s *SyncService) Reinitialize() error { + return s.Initialize() +} + +// HandleConfigChange 在配置变化时重新应用配置。 +func (s *SyncService) HandleConfigChange(config *models.SyncConfig) error { + _ = config + return s.Initialize() +} + +// StartAutoSync 启动自动同步调度。 +func (s *SyncService) StartAutoSync() error { + if err := s.ensureApp(); err != nil { + return err + } + return s.app.Start(context.Background()) +} + +// StopAutoSync 停止自动同步调度。 +func (s *SyncService) StopAutoSync() { + if s.app == nil { + return + } + if err := s.app.Stop(context.Background()); err != nil { + s.logger.Warning("stop sync app: %v", err) + } +} + +// Sync 执行一次手动同步。 +func (s *SyncService) Sync() error { + if err := s.ensureApp(); err != nil { + return err + } + + targetID, err := s.selectedTargetID() + if err != nil { + return err + } + + if _, err := s.app.Sync(context.Background(), targetID); err != nil { + return err + } + return nil +} + +// ServiceShutdown 停止同步服务并释放资源。 +func (s *SyncService) ServiceShutdown() { + for _, cancel := range s.cancelObservers { + if cancel != nil { + cancel() + } + } + s.StopAutoSync() +} + +// onSyncConfigChange 响应 sync 配置变化。 +func (s *SyncService) onSyncConfigChange(oldValue interface{}, newValue interface{}) { + _, _ = oldValue, newValue + if err := s.Initialize(); err != nil { + s.logger.Error("reconfigure sync after sync config change: %v", err) + } +} + +// onDataPathChange 响应数据目录变化。 +func (s *SyncService) onDataPathChange(oldValue interface{}, newValue interface{}) { + _, _ = oldValue, newValue + if err := s.Reinitialize(); err != nil { + s.logger.Error("reconfigure sync after data path change: %v", err) + } +} + +// ensureApp 保证同步应用已被创建。 +func (s *SyncService) ensureApp() error { + if s.app != nil { + return nil + } + if s.dbService == nil || s.dbService.Client == nil { + return fmt.Errorf("sync database client is not ready") + } + + s.app = syncer.NewApp(s.dbService.Client, syncer.Options{ + Logger: s.logger, + MaxSyncAttempts: 3, + }) + return nil +} + +// buildConfig 将现有应用配置映射为同步核心配置。 +func (s *SyncService) buildConfig() (syncer.Config, error) { + appConfig, err := s.configService.GetConfig() + if err != nil { + return syncer.Config{}, err + } + + return syncer.Config{ + Targets: []syncer.TargetConfig{ + s.buildGitTargetConfig(appConfig.General.DataPath, appConfig.Sync.Git), + s.buildLocalFSTargetConfig(appConfig.Sync.LocalFS), + }, + }, nil +} + +// selectedTargetID 返回当前选中的同步目标标识。 +func (s *SyncService) selectedTargetID() (string, error) { + appConfig, err := s.configService.GetConfig() + if err != nil { + return "", err + } + + switch appConfig.Sync.Target { + case models.SyncTargetGit: + return string(models.SyncTargetGit), nil + case models.SyncTargetLocalFS: + return string(models.SyncTargetLocalFS), nil + default: + return "", fmt.Errorf("unsupported sync target: %s", appConfig.Sync.Target) + } +} + +// buildGitTargetConfig 将 Git 配置转换为同步核心目标配置。 +func (s *SyncService) buildGitTargetConfig(dataPath string, config models.GitSyncConfig) syncer.TargetConfig { + return syncer.TargetConfig{ + Kind: syncer.TargetKindGit, + Enabled: config.Enabled, + Schedule: syncer.ScheduleConfig{ + AutoSync: config.AutoSync, + Interval: time.Duration(config.SyncInterval) * time.Minute, + }, + Git: &syncer.GitTargetConfig{ + RepoPath: filepath.Join(dataPath, syncDir), + RepoURL: config.RepoURL, + Branch: syncer.DefaultBranch, + RemoteName: syncer.DefaultRemoteName, + AuthorName: "voidraft", + AuthorEmail: "sync@voidraft.app", + Auth: syncer.GitAuthConfig{ + Method: string(config.AuthMethod), + Username: config.Username, + Password: config.Password, + Token: config.Token, + SSHKeyPath: config.SSHKeyPath, + SSHKeyPassword: config.SSHKeyPass, + }, + }, + } +} + +// buildLocalFSTargetConfig 将 localfs 配置转换为同步核心目标配置。 +func (s *SyncService) buildLocalFSTargetConfig(config models.LocalFSSyncConfig) syncer.TargetConfig { + return syncer.TargetConfig{ + Kind: syncer.TargetKindLocalFS, + Enabled: config.Enabled, + Schedule: syncer.ScheduleConfig{ + AutoSync: config.AutoSync, + Interval: time.Duration(config.SyncInterval) * time.Minute, + }, + LocalFS: &syncer.LocalFSTargetConfig{ + Namespace: string(models.SyncTargetLocalFS), + HeadKey: localFSHeadKey, + RootPath: config.RootPath, + }, + } +} diff --git a/internal/syncer/app.go b/internal/syncer/app.go new file mode 100644 index 00000000..c9856e28 --- /dev/null +++ b/internal/syncer/app.go @@ -0,0 +1,283 @@ +package syncer + +import ( + "context" + "fmt" + "sync" + "time" + "voidraft/internal/models/ent" + "voidraft/internal/syncer/backend" + gitbackend "voidraft/internal/syncer/backend/git" + snapshotstorebackend "voidraft/internal/syncer/backend/snapshotstore" + localfsblob "voidraft/internal/syncer/backend/snapshotstore/blob/localfs" + "voidraft/internal/syncer/engine" + "voidraft/internal/syncer/merge" + "voidraft/internal/syncer/resource" + "voidraft/internal/syncer/scheduler" + "voidraft/internal/syncer/snapshot" +) + +const ( + defaultAuthorName = "voidraft" + defaultAuthorEmail = "sync@voidraft.app" + defaultSyncAttempts = 3 +) + +// Options 描述同步应用的构造选项。 +type Options struct { + Logger Logger + MaxSyncAttempts int +} + +// App 是同步系统的编排入口。 +type App struct { + logger Logger + snapshotter snapshot.Snapshotter + store snapshot.Store + merger merge.Merger + maxSyncAttempts int + + mu sync.RWMutex + syncMu sync.Mutex + config Config + schedulers map[string]*scheduler.Ticker +} + +// NewApp 创建新的同步应用实例。 +func NewApp(client *ent.Client, options Options) *App { + maxSyncAttempts := options.MaxSyncAttempts + if maxSyncAttempts <= 0 { + maxSyncAttempts = defaultSyncAttempts + } + + return &App{ + logger: options.Logger, + snapshotter: resource.NewRegistry( + resource.NewDocumentAdapter(client), + resource.NewExtensionAdapter(client), + resource.NewKeyBindingAdapter(client), + resource.NewThemeAdapter(client), + ), + store: snapshot.NewFileStore(), + merger: merge.NewUpdatedAtWinsMerger(), + maxSyncAttempts: maxSyncAttempts, + schedulers: make(map[string]*scheduler.Ticker), + } +} + +// Reconfigure 更新同步系统配置。 +func (a *App) Reconfigure(ctx context.Context, cfg Config) error { + _ = ctx + + normalized := cfg.Normalize() + for _, target := range normalized.Targets { + if err := target.Validate(); err != nil { + return fmt.Errorf("validate target %s: %w", target.Kind, err) + } + } + + a.mu.Lock() + a.config = normalized + a.mu.Unlock() + + return nil +} + +// Start 按当前配置启动自动同步调度。 +func (a *App) Start(ctx context.Context) error { + targets := a.targetsSnapshot() + if err := a.verifyTargets(ctx, targets); err != nil { + return err + } + + a.mu.Lock() + defer a.mu.Unlock() + + a.stopSchedulersLocked() + + for _, target := range targets { + if !target.Ready() || !target.Schedule.AutoSync || target.Schedule.Interval <= 0 { + continue + } + + currentTargetID := target.Kind + task := scheduler.NewTicker() + task.Start(target.Schedule.Interval, func(runCtx context.Context) error { + _, err := a.Sync(runCtx, currentTargetID) + if err != nil && a.logger != nil { + a.logger.Error("sync auto run failed for target %s: %v", currentTargetID, err) + } + return err + }) + a.schedulers[currentTargetID] = task + } + + return nil +} + +// Stop 停止所有自动同步调度。 +func (a *App) Stop(ctx context.Context) error { + _ = ctx + + a.mu.Lock() + defer a.mu.Unlock() + + a.stopSchedulersLocked() + return nil +} + +// Sync 执行指定目标的一次完整同步。 +func (a *App) Sync(ctx context.Context, targetID string) (*SyncResult, error) { + target, err := a.currentTarget(targetID) + if err != nil { + return nil, err + } + if !target.Enabled { + return nil, ErrTargetDisabled + } + if !target.Ready() { + return nil, ErrTargetNotReady + } + + backendInstance, err := a.newBackend(target) + if err != nil { + return nil, err + } + defer func() { + _ = backendInstance.Close() + }() + + syncEngine := engine.NewSyncEngine( + backendInstance, + a.store, + a.snapshotter, + a.merger, + engine.Options{ + Logger: a.logger, + MaxAttempts: a.maxSyncAttempts, + }, + ) + + a.syncMu.Lock() + defer a.syncMu.Unlock() + + result, err := syncEngine.Sync(ctx, engine.SyncOptions{ + CommitMessage: a.commitMessage(target), + }) + if err != nil { + return nil, err + } + + return &SyncResult{ + TargetID: target.Kind, + LocalChanged: result.LocalChanged, + RemoteChanged: result.RemoteChanged, + AppliedToLocal: result.AppliedToLocal, + Published: result.Published, + ConflictCount: result.ConflictCount, + Revision: result.Revision, + }, nil +} + +// commitMessage 生成提交信息。 +func (a *App) commitMessage(target TargetConfig) string { + return fmt.Sprintf("Sync %s %s", target.Kind, time.Now().Format(time.RFC3339)) +} + +// currentTarget 返回当前内存中的目标配置。 +func (a *App) currentTarget(targetID string) (TargetConfig, error) { + a.mu.RLock() + defer a.mu.RUnlock() + return a.config.Target(targetID) +} + +// targetsSnapshot 返回当前所有目标的快照。 +func (a *App) targetsSnapshot() []TargetConfig { + a.mu.RLock() + defer a.mu.RUnlock() + + targets := make([]TargetConfig, len(a.config.Targets)) + copy(targets, a.config.Targets) + return targets +} + +// verifyTargets 预先校验所有已就绪目标。 +func (a *App) verifyTargets(ctx context.Context, targets []TargetConfig) error { + for _, target := range targets { + if !target.Ready() { + continue + } + + backendInstance, err := a.newBackend(target) + if err != nil { + return err + } + + verifyErr := backendInstance.Verify(ctx) + closeErr := backendInstance.Close() + if verifyErr != nil { + return fmt.Errorf("verify target %s: %w", target.Kind, verifyErr) + } + if closeErr != nil { + return fmt.Errorf("close target %s backend: %w", target.Kind, closeErr) + } + } + return nil +} + +// newBackend 根据目标配置构造后端实例。 +func (a *App) newBackend(target TargetConfig) (backend.Backend, error) { + switch target.Kind { + case TargetKindGit: + if target.Git == nil { + return nil, fmt.Errorf("target %s: git config is nil", target.Kind) + } + return gitbackend.New(gitbackend.Config{ + RepoPath: target.Git.RepoPath, + RepoURL: target.Git.RepoURL, + Branch: target.Git.Branch, + RemoteName: target.Git.RemoteName, + AuthorName: fallbackString(target.Git.AuthorName, defaultAuthorName), + AuthorEmail: fallbackString(target.Git.AuthorEmail, defaultAuthorEmail), + Auth: gitbackend.AuthConfig{ + Method: target.Git.Auth.Method, + Username: target.Git.Auth.Username, + Password: target.Git.Auth.Password, + Token: target.Git.Auth.Token, + SSHKeyPath: target.Git.Auth.SSHKeyPath, + SSHKeyPassword: target.Git.Auth.SSHKeyPassword, + }, + }) + case TargetKindLocalFS: + if target.LocalFS == nil { + return nil, fmt.Errorf("target %s: localfs config is nil", target.Kind) + } + store, err := localfsblob.New(target.LocalFS.RootPath) + if err != nil { + return nil, err + } + return snapshotstorebackend.New(snapshotstorebackend.Config{ + Store: store, + Namespace: target.LocalFS.Namespace, + HeadKey: target.LocalFS.HeadKey, + }) + default: + return nil, fmt.Errorf("%w: %s", ErrUnsupportedBackend, target.Kind) + } +} + +// stopSchedulersLocked 停止所有调度器。 +func (a *App) stopSchedulersLocked() { + for targetID, task := range a.schedulers { + task.Stop() + delete(a.schedulers, targetID) + } +} + +// fallbackString 返回第一个非空字符串。 +func fallbackString(value string, fallback string) string { + if value == "" { + return fallback + } + return value +} diff --git a/internal/syncer/backend/backend.go b/internal/syncer/backend/backend.go new file mode 100644 index 00000000..0f83b63a --- /dev/null +++ b/internal/syncer/backend/backend.go @@ -0,0 +1,31 @@ +package backend + +import ( + "context" + "errors" +) + +var ( + // ErrRevisionConflict 表示远端版本已变化,需要重新拉取合并。 + ErrRevisionConflict = errors.New("sync revision conflict") +) + +// RemoteState 描述远端最新状态。 +type RemoteState struct { + Revision string + Exists bool +} + +// PublishOptions 描述一次发布操作的参数。 +type PublishOptions struct { + ExpectedRevision string + Message string +} + +// Backend 描述统一同步后端接口。 +type Backend interface { + Verify(ctx context.Context) error + DownloadLatest(ctx context.Context, dst string) (RemoteState, error) + Upload(ctx context.Context, src string, options PublishOptions) (RemoteState, error) + Close() error +} diff --git a/internal/syncer/backend/git/auth.go b/internal/syncer/backend/git/auth.go new file mode 100644 index 00000000..85c79d16 --- /dev/null +++ b/internal/syncer/backend/git/auth.go @@ -0,0 +1,60 @@ +package git + +import ( + "errors" + "fmt" + + "github.com/go-git/go-git/v5/plumbing/transport" + "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/go-git/go-git/v5/plumbing/transport/ssh" +) + +// AuthConfig 描述 Git 鉴权方式。 +type AuthConfig struct { + Method string + Username string + Password string + Token string + SSHKeyPath string + SSHKeyPassword string +} + +const ( + // AuthMethodToken 使用 Token 鉴权。 + AuthMethodToken = "token" + // AuthMethodSSHKey 使用 SSH Key 鉴权。 + AuthMethodSSHKey = "ssh_key" + // AuthMethodUserPass 使用用户名密码鉴权。 + AuthMethodUserPass = "user_pass" +) + +// authMethod 根据配置构造 go-git 鉴权实例。 +func authMethod(config AuthConfig) (transport.AuthMethod, error) { + switch config.Method { + case AuthMethodToken: + if config.Token == "" { + return nil, errors.New("git token is required") + } + return &http.BasicAuth{ + Username: "git", + Password: config.Token, + }, nil + case AuthMethodUserPass: + if config.Username == "" || config.Password == "" { + return nil, errors.New("git username and password are required") + } + return &http.BasicAuth{ + Username: config.Username, + Password: config.Password, + }, nil + case AuthMethodSSHKey: + if config.SSHKeyPath == "" { + return nil, errors.New("git ssh key path is required") + } + return ssh.NewPublicKeysFromFile("git", config.SSHKeyPath, config.SSHKeyPassword) + case "": + return nil, nil + default: + return nil, fmt.Errorf("unsupported git auth method: %s", config.Method) + } +} diff --git a/internal/syncer/backend/git/backend.go b/internal/syncer/backend/git/backend.go new file mode 100644 index 00000000..35f04ae1 --- /dev/null +++ b/internal/syncer/backend/git/backend.go @@ -0,0 +1,518 @@ +package git + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + "voidraft/internal/syncer/backend" + + "github.com/go-git/go-git/v5" + gitconfig "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" +) + +const defaultGitIgnore = "*.tmp\n*.log\n" + +// Config 描述 Git 后端配置。 +type Config struct { + RepoPath string + RepoURL string + Branch string + RemoteName string + AuthorName string + AuthorEmail string + Auth AuthConfig +} + +// Backend 提供基于 Git 的后端实现。 +type Backend struct { + config Config + repository *git.Repository +} + +// New 创建新的 Git 后端实例。 +func New(config Config) (*Backend, error) { + normalized, err := normalizeConfig(config) + if err != nil { + return nil, err + } + return &Backend{config: normalized}, nil +} + +// Verify 校验本地仓库和远端连接是否可用。 +func (b *Backend) Verify(ctx context.Context) error { + _ = ctx + + if err := b.ensureRepository(); err != nil { + return err + } + + auth, err := authMethod(b.config.Auth) + if err != nil { + return err + } + + remote, err := b.repository.Remote(b.config.RemoteName) + if err != nil { + return err + } + + _, err = remote.List(&git.ListOptions{Auth: auth}) + if err == nil { + return nil + } + if isEmptyRemoteError(err) { + return nil + } + return err +} + +// DownloadLatest 拉取远端最新快照并导出到目标目录。 +func (b *Backend) DownloadLatest(ctx context.Context, dst string) (backend.RemoteState, error) { + _ = ctx + + if err := b.ensureRepository(); err != nil { + return backend.RemoteState{}, err + } + + if err := recreateDir(dst); err != nil { + return backend.RemoteState{}, err + } + + remoteState, err := b.fetchRemoteState() + if err != nil { + return backend.RemoteState{}, err + } + if !remoteState.Exists { + return remoteState, nil + } + + if err := b.exportRemoteTree(remoteState.Revision, dst); err != nil { + return backend.RemoteState{}, err + } + + return remoteState, nil +} + +// Upload 将本地快照目录发布到远端 Git 仓库。 +func (b *Backend) Upload(ctx context.Context, src string, options backend.PublishOptions) (backend.RemoteState, error) { + _ = ctx + + if err := b.ensureRepository(); err != nil { + return backend.RemoteState{}, err + } + + remoteState, err := b.fetchRemoteState() + if err != nil { + return backend.RemoteState{}, err + } + if options.ExpectedRevision != "" && remoteState.Exists && remoteState.Revision != options.ExpectedRevision { + return backend.RemoteState{}, backend.ErrRevisionConflict + } + + if err := b.prepareBranch(remoteState); err != nil { + return backend.RemoteState{}, err + } + if err := syncDir(src, b.config.RepoPath); err != nil { + return backend.RemoteState{}, err + } + + worktree, err := b.repository.Worktree() + if err != nil { + return backend.RemoteState{}, err + } + + changed, err := stageAll(worktree) + if err != nil { + return backend.RemoteState{}, err + } + if !changed { + return b.currentLocalState() + } + + if _, err := worktree.Commit(options.Message, &git.CommitOptions{ + Author: &object.Signature{ + Name: b.config.AuthorName, + Email: b.config.AuthorEmail, + When: time.Now(), + }, + }); err != nil { + return backend.RemoteState{}, err + } + + auth, err := authMethod(b.config.Auth) + if err != nil { + return backend.RemoteState{}, err + } + + branchRef := plumbing.NewBranchReferenceName(b.config.Branch) + remoteRef := plumbing.NewRemoteReferenceName(b.config.RemoteName, b.config.Branch) + err = b.repository.Push(&git.PushOptions{ + RemoteName: b.config.RemoteName, + Auth: auth, + RefSpecs: []gitconfig.RefSpec{ + gitconfig.RefSpec(fmt.Sprintf("%s:%s", branchRef, remoteRef)), + }, + }) + if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { + if errors.Is(err, git.ErrNonFastForwardUpdate) { + return backend.RemoteState{}, backend.ErrRevisionConflict + } + return backend.RemoteState{}, err + } + + return b.currentLocalState() +} + +// Close 关闭后端。 +func (b *Backend) Close() error { + return nil +} + +// normalizeConfig 填充 Git 后端配置默认值。 +func normalizeConfig(config Config) (Config, error) { + normalized := config + if strings.TrimSpace(normalized.RepoPath) == "" { + return Config{}, errors.New("git repo path is required") + } + if strings.TrimSpace(normalized.Branch) == "" { + normalized.Branch = "master" + } + if strings.TrimSpace(normalized.RemoteName) == "" { + normalized.RemoteName = "origin" + } + if strings.TrimSpace(normalized.AuthorName) == "" { + normalized.AuthorName = "voidraft" + } + if strings.TrimSpace(normalized.AuthorEmail) == "" { + normalized.AuthorEmail = "sync@voidraft.app" + } + return normalized, nil +} + +// ensureRepository 确保本地 Git 仓库存在且远端配置正确。 +func (b *Backend) ensureRepository() error { + if b.repository != nil { + return b.ensureRemote() + } + + if err := os.MkdirAll(b.config.RepoPath, 0755); err != nil { + return fmt.Errorf("create git repo dir: %w", err) + } + + gitPath := filepath.Join(b.config.RepoPath, ".git") + if _, err := os.Stat(gitPath); os.IsNotExist(err) { + repository, initErr := git.PlainInit(b.config.RepoPath, false) + if initErr != nil { + return fmt.Errorf("init git repo: %w", initErr) + } + b.repository = repository + if err := ensureGitIgnore(b.config.RepoPath); err != nil { + return err + } + return b.ensureRemote() + } else if err != nil { + return fmt.Errorf("stat git repo: %w", err) + } + + repository, err := git.PlainOpen(b.config.RepoPath) + if err != nil { + return fmt.Errorf("open git repo: %w", err) + } + b.repository = repository + if err := ensureGitIgnore(b.config.RepoPath); err != nil { + return err + } + return b.ensureRemote() +} + +// ensureRemote 确保远端配置与当前目标一致。 +func (b *Backend) ensureRemote() error { + if strings.TrimSpace(b.config.RepoURL) == "" { + return nil + } + + remote, err := b.repository.Remote(b.config.RemoteName) + if errors.Is(err, git.ErrRemoteNotFound) { + _, err = b.repository.CreateRemote(&gitconfig.RemoteConfig{ + Name: b.config.RemoteName, + URLs: []string{b.config.RepoURL}, + }) + return err + } + if err != nil { + return err + } + + if len(remote.Config().URLs) > 0 && remote.Config().URLs[0] == b.config.RepoURL { + return nil + } + + if err := b.repository.DeleteRemote(b.config.RemoteName); err != nil { + return err + } + _, err = b.repository.CreateRemote(&gitconfig.RemoteConfig{ + Name: b.config.RemoteName, + URLs: []string{b.config.RepoURL}, + }) + return err +} + +// fetchRemoteState 拉取远端分支并返回最新状态。 +func (b *Backend) fetchRemoteState() (backend.RemoteState, error) { + auth, err := authMethod(b.config.Auth) + if err != nil { + return backend.RemoteState{}, err + } + + err = b.repository.Fetch(&git.FetchOptions{ + RemoteName: b.config.RemoteName, + Auth: auth, + Force: true, + }) + if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { + if isEmptyRemoteError(err) || isMissingRemoteRefError(err) { + return backend.RemoteState{}, nil + } + return backend.RemoteState{}, err + } + + ref, err := b.repository.Reference(plumbing.NewRemoteReferenceName(b.config.RemoteName, b.config.Branch), true) + if err != nil { + if errors.Is(err, plumbing.ErrReferenceNotFound) { + return backend.RemoteState{}, nil + } + return backend.RemoteState{}, err + } + + return backend.RemoteState{ + Exists: true, + Revision: ref.Hash().String(), + }, nil +} + +// exportRemoteTree 将指定提交的树内容导出为普通文件。 +func (b *Backend) exportRemoteTree(revision string, dst string) error { + commit, err := b.repository.CommitObject(plumbing.NewHash(revision)) + if err != nil { + return err + } + + tree, err := commit.Tree() + if err != nil { + return err + } + + return tree.Files().ForEach(func(file *object.File) error { + targetPath := filepath.Join(dst, filepath.FromSlash(file.Name)) + if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { + return err + } + + reader, err := file.Reader() + if err != nil { + return err + } + defer reader.Close() + + writer, err := os.Create(targetPath) + if err != nil { + return err + } + defer writer.Close() + + _, err = io.Copy(writer, reader) + return err + }) +} + +// prepareBranch 将本地分支重置到远端最新版本。 +func (b *Backend) prepareBranch(remoteState backend.RemoteState) error { + branchRef := plumbing.NewBranchReferenceName(b.config.Branch) + if remoteState.Exists { + if err := b.repository.Storer.SetReference(plumbing.NewHashReference(branchRef, plumbing.NewHash(remoteState.Revision))); err != nil { + return err + } + } + if err := b.repository.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, branchRef)); err != nil { + return err + } + + if !remoteState.Exists { + return nil + } + + worktree, err := b.repository.Worktree() + if err != nil { + return err + } + return worktree.Checkout(&git.CheckoutOptions{ + Branch: branchRef, + Force: true, + }) +} + +// currentLocalState 返回当前本地 HEAD 状态。 +func (b *Backend) currentLocalState() (backend.RemoteState, error) { + head, err := b.repository.Head() + if err != nil { + if errors.Is(err, plumbing.ErrReferenceNotFound) { + return backend.RemoteState{}, nil + } + return backend.RemoteState{}, err + } + return backend.RemoteState{ + Exists: true, + Revision: head.Hash().String(), + }, nil +} + +// ensureGitIgnore 保证仓库目录中存在默认 .gitignore。 +func ensureGitIgnore(repoPath string) error { + gitIgnorePath := filepath.Join(repoPath, ".gitignore") + if _, err := os.Stat(gitIgnorePath); err == nil { + return nil + } else if !os.IsNotExist(err) { + return err + } + return os.WriteFile(gitIgnorePath, []byte(defaultGitIgnore), 0644) +} + +// recreateDir 清空并重建目录。 +func recreateDir(dir string) error { + if err := os.RemoveAll(dir); err != nil { + return err + } + return os.MkdirAll(dir, 0755) +} + +// syncDir 将源目录内容同步到目标目录。 +func syncDir(src string, dst string) error { + sourceEntries, err := os.ReadDir(src) + if err != nil { + return err + } + if err := os.MkdirAll(dst, 0755); err != nil { + return err + } + + sourceIndex := make(map[string]os.DirEntry, len(sourceEntries)) + for _, entry := range sourceEntries { + sourceIndex[entry.Name()] = entry + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + if err := syncDir(srcPath, dstPath); err != nil { + return err + } + continue + } + + if err := copyFile(srcPath, dstPath); err != nil { + return err + } + } + + targetEntries, err := os.ReadDir(dst) + if err != nil { + return err + } + + for _, entry := range targetEntries { + if entry.Name() == ".git" || entry.Name() == ".gitignore" { + continue + } + if _, exists := sourceIndex[entry.Name()]; exists { + continue + } + if err := os.RemoveAll(filepath.Join(dst, entry.Name())); err != nil { + return err + } + } + + return nil +} + +// copyFile 复制单个文件并保留权限位。 +func copyFile(src string, dst string) error { + sourceFile, err := os.Open(src) + if err != nil { + return err + } + defer sourceFile.Close() + + info, err := sourceFile.Stat() + if err != nil { + return err + } + + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return err + } + + targetFile, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode().Perm()) + if err != nil { + return err + } + defer targetFile.Close() + + _, err = io.Copy(targetFile, sourceFile) + return err +} + +// stageAll 将工作区所有变化加入索引。 +func stageAll(worktree *git.Worktree) (bool, error) { + status, err := worktree.Status() + if err != nil { + return false, err + } + + for path, fileStatus := range status { + switch fileStatus.Worktree { + case git.Untracked, git.Modified, git.Added, git.Copied, git.Renamed: + if _, err := worktree.Add(path); err != nil { + return false, err + } + case git.Deleted: + if _, err := worktree.Remove(path); err != nil && !os.IsNotExist(err) { + return false, err + } + } + if fileStatus.Staging == git.Deleted && fileStatus.Worktree == git.Unmodified { + if _, err := worktree.Remove(path); err != nil && !os.IsNotExist(err) { + return false, err + } + } + } + + status, err = worktree.Status() + if err != nil { + return false, err + } + return !status.IsClean(), nil +} + +// isEmptyRemoteError 判断错误是否表示远端仓库为空。 +func isEmptyRemoteError(err error) bool { + if err == nil { + return false + } + message := err.Error() + return strings.Contains(message, "empty") || strings.Contains(message, "no reference") +} + +// isMissingRemoteRefError 判断错误是否表示远端分支不存在。 +func isMissingRemoteRefError(err error) bool { + if err == nil { + return false + } + message := err.Error() + return strings.Contains(message, "reference not found") || strings.Contains(message, "couldn't find remote ref") +} diff --git a/internal/syncer/backend/snapshotstore/backend.go b/internal/syncer/backend/snapshotstore/backend.go new file mode 100644 index 00000000..dc0e59b3 --- /dev/null +++ b/internal/syncer/backend/snapshotstore/backend.go @@ -0,0 +1,413 @@ +package snapshotstore + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path" + "path/filepath" + "sort" + "strings" + "time" + "voidraft/internal/syncer/backend" + "voidraft/internal/syncer/backend/snapshotstore/blob" +) + +const ( + defaultNamespace = "sync" + defaultHeadKey = "head.json" + bundleDirName = "bundles" +) + +var stableBundleTime = time.Unix(0, 0).UTC() + +// Config 描述 snapshot_store 后端配置。 +type Config struct { + Store blob.Store + Namespace string + HeadKey string +} + +type headDocument struct { + Revision string `json:"revision"` + BundleKey string `json:"bundle_key"` + UpdatedAt string `json:"updated_at"` +} + +type headState struct { + Document headDocument + Info blob.ObjectInfo +} + +// Backend 提供基于对象/文件存储的快照后端实现。 +type Backend struct { + config Config +} + +// New 创建新的 snapshot_store 后端。 +func New(config Config) (*Backend, error) { + if config.Store == nil { + return nil, errors.New("snapshot store blob backend is required") + } + if strings.TrimSpace(config.Namespace) == "" { + config.Namespace = defaultNamespace + } + if strings.TrimSpace(config.HeadKey) == "" { + config.HeadKey = defaultHeadKey + } + return &Backend{config: config}, nil +} + +// Verify 校验后端是否可读。 +func (b *Backend) Verify(ctx context.Context) error { + _, _, err := b.readHead(ctx) + return err +} + +// DownloadLatest 下载远端最新快照包并解压到目标目录。 +func (b *Backend) DownloadLatest(ctx context.Context, dst string) (backend.RemoteState, error) { + head, exists, err := b.readHead(ctx) + if err != nil { + return backend.RemoteState{}, err + } + if !exists { + return backend.RemoteState{}, nil + } + + reader, _, err := b.config.Store.Get(ctx, head.Document.BundleKey) + if err != nil { + if errors.Is(err, blob.ErrObjectNotFound) { + return backend.RemoteState{}, nil + } + return backend.RemoteState{}, err + } + defer reader.Close() + + if err := recreateDir(dst); err != nil { + return backend.RemoteState{}, err + } + if err := extractBundle(reader, dst); err != nil { + return backend.RemoteState{}, err + } + + return backend.RemoteState{ + Exists: true, + Revision: head.Document.Revision, + }, nil +} + +// Upload 打包并发布本地快照目录。 +func (b *Backend) Upload(ctx context.Context, src string, options backend.PublishOptions) (backend.RemoteState, error) { + currentHead, exists, err := b.readHead(ctx) + if err != nil { + return backend.RemoteState{}, err + } + + switch { + case options.ExpectedRevision != "" && !exists: + return backend.RemoteState{}, backend.ErrRevisionConflict + case options.ExpectedRevision != "" && currentHead.Document.Revision != options.ExpectedRevision: + return backend.RemoteState{}, backend.ErrRevisionConflict + } + + bundlePath, revision, err := createBundle(src) + if err != nil { + return backend.RemoteState{}, err + } + defer os.Remove(bundlePath) + + if exists && currentHead.Document.Revision == revision { + return backend.RemoteState{ + Exists: true, + Revision: revision, + }, nil + } + + bundleKey := b.bundleKey(revision) + file, err := os.Open(bundlePath) + if err != nil { + return backend.RemoteState{}, err + } + defer file.Close() + + if _, err := b.config.Store.Put(ctx, bundleKey, file, blob.PutOptions{}); err != nil { + return backend.RemoteState{}, err + } + + nextHead := headDocument{ + Revision: revision, + BundleKey: bundleKey, + UpdatedAt: time.Now().Format(time.RFC3339), + } + headPayload, err := json.MarshalIndent(nextHead, "", " ") + if err != nil { + return backend.RemoteState{}, err + } + headPayload = append(headPayload, '\n') + + putOptions := blob.PutOptions{} + if exists { + putOptions.IfMatch = currentHead.Info.Revision + } + + if _, err := b.config.Store.Put(ctx, b.headKey(), bytes.NewReader(headPayload), putOptions); err != nil { + if errors.Is(err, blob.ErrConditionNotMet) { + return backend.RemoteState{}, backend.ErrRevisionConflict + } + return backend.RemoteState{}, err + } + + return backend.RemoteState{ + Exists: true, + Revision: revision, + }, nil +} + +// Close 关闭后端。 +func (b *Backend) Close() error { + return nil +} + +// readHead 读取远端 head 指针。 +func (b *Backend) readHead(ctx context.Context) (headState, bool, error) { + reader, info, err := b.config.Store.Get(ctx, b.headKey()) + if err != nil { + if errors.Is(err, blob.ErrObjectNotFound) { + return headState{}, false, nil + } + return headState{}, false, err + } + defer reader.Close() + + data, err := io.ReadAll(reader) + if err != nil { + return headState{}, false, err + } + + var document headDocument + if err := json.Unmarshal(data, &document); err != nil { + return headState{}, false, err + } + if document.Revision == "" || document.BundleKey == "" { + return headState{}, false, errors.New("snapshot store head is invalid") + } + + return headState{ + Document: document, + Info: info, + }, true, nil +} + +// headKey 返回完整的 head 对象键。 +func (b *Backend) headKey() string { + return path.Join(b.config.Namespace, b.config.HeadKey) +} + +// bundleKey 返回 revision 对应的 bundle 键。 +func (b *Backend) bundleKey(revision string) string { + return path.Join(b.config.Namespace, bundleDirName, revision+".tar.gz") +} + +// createBundle 将目录稳定打包成 tar.gz,并返回文件路径与摘要。 +func createBundle(root string) (string, string, error) { + tempFile, err := os.CreateTemp("", "voidraft-snapshot-*.tar.gz") + if err != nil { + return "", "", err + } + tempName := tempFile.Name() + + hasher := sha256.New() + multiWriter := io.MultiWriter(tempFile, hasher) + + gzipWriter := gzip.NewWriter(multiWriter) + gzipWriter.ModTime = stableBundleTime + gzipWriter.Name = "" + gzipWriter.Comment = "" + + tarWriter := tar.NewWriter(gzipWriter) + + writeErr := writeBundle(root, tarWriter) + closeErr := tarWriter.Close() + gzipCloseErr := gzipWriter.Close() + fileCloseErr := tempFile.Close() + if writeErr != nil { + _ = os.Remove(tempName) + return "", "", writeErr + } + if closeErr != nil { + _ = os.Remove(tempName) + return "", "", closeErr + } + if gzipCloseErr != nil { + _ = os.Remove(tempName) + return "", "", gzipCloseErr + } + if fileCloseErr != nil { + _ = os.Remove(tempName) + return "", "", fileCloseErr + } + + revision := hex.EncodeToString(hasher.Sum(nil)) + return tempName, revision, nil +} + +// writeBundle 将目录内容按稳定顺序写入 tar。 +func writeBundle(root string, writer *tar.Writer) error { + paths, err := collectPaths(root) + if err != nil { + return err + } + + for _, entryPath := range paths { + info, err := os.Lstat(entryPath) + if err != nil { + return err + } + + relativePath, err := filepath.Rel(root, entryPath) + if err != nil { + return err + } + + header, err := tar.FileInfoHeader(info, "") + if err != nil { + return err + } + header.Name = filepath.ToSlash(relativePath) + header.ModTime = stableBundleTime + header.AccessTime = stableBundleTime + header.ChangeTime = stableBundleTime + header.Uid = 0 + header.Gid = 0 + header.Uname = "" + header.Gname = "" + + if info.IsDir() && !strings.HasSuffix(header.Name, "/") { + header.Name += "/" + } + + if err := writer.WriteHeader(header); err != nil { + return err + } + + if info.IsDir() { + continue + } + + file, err := os.Open(entryPath) + if err != nil { + return err + } + if _, err := io.Copy(writer, file); err != nil { + file.Close() + return err + } + if err := file.Close(); err != nil { + return err + } + } + + return nil +} + +// collectPaths 返回稳定排序后的目录项列表。 +func collectPaths(root string) ([]string, error) { + entries := make([]string, 0) + if err := filepath.WalkDir(root, func(entryPath string, entry os.DirEntry, err error) error { + if err != nil { + return err + } + if entryPath == root { + return nil + } + entries = append(entries, entryPath) + return nil + }); err != nil { + return nil, err + } + + sort.Strings(entries) + return entries, nil +} + +// extractBundle 将 tar.gz 包解压到目标目录。 +func extractBundle(reader io.Reader, dst string) error { + gzipReader, err := gzip.NewReader(reader) + if err != nil { + return err + } + defer gzipReader.Close() + + tarReader := tar.NewReader(gzipReader) + for { + header, err := tarReader.Next() + if errors.Is(err, io.EOF) { + return nil + } + if err != nil { + return err + } + + targetPath, err := resolveExtractPath(dst, header.Name) + if err != nil { + return err + } + + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(targetPath, 0755); err != nil { + return err + } + case tar.TypeReg: + if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { + return err + } + file, err := os.OpenFile(targetPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(header.Mode)) + if err != nil { + return err + } + if _, err := io.Copy(file, tarReader); err != nil { + file.Close() + return err + } + if err := file.Close(); err != nil { + return err + } + default: + return fmt.Errorf("unsupported tar entry type: %d", header.Typeflag) + } + } +} + +// recreateDir 清空并重建目录。 +func recreateDir(dir string) error { + if err := os.RemoveAll(dir); err != nil { + return err + } + return os.MkdirAll(dir, 0755) +} + +// resolveExtractPath 将归档路径安全映射到目标目录。 +func resolveExtractPath(root string, name string) (string, error) { + clean := filepath.Clean(filepath.FromSlash(name)) + if clean == "." { + return "", errors.New("invalid archive entry") + } + targetPath := filepath.Join(root, clean) + relativePath, err := filepath.Rel(root, targetPath) + if err != nil { + return "", err + } + if strings.HasPrefix(relativePath, "..") { + return "", errors.New("archive entry escapes target directory") + } + return targetPath, nil +} diff --git a/internal/syncer/backend/snapshotstore/backend_test.go b/internal/syncer/backend/snapshotstore/backend_test.go new file mode 100644 index 00000000..e3831fab --- /dev/null +++ b/internal/syncer/backend/snapshotstore/backend_test.go @@ -0,0 +1,109 @@ +package snapshotstore + +import ( + "context" + "errors" + "os" + "path/filepath" + "testing" + "voidraft/internal/syncer/backend" + localfsblob "voidraft/internal/syncer/backend/snapshotstore/blob/localfs" +) + +// TestBackendUploadDownload 验证 snapshot_store 后端可以发布并回放快照包。 +func TestBackendUploadDownload(t *testing.T) { + store, err := localfsblob.New(t.TempDir()) + if err != nil { + t.Fatalf("create blob store: %v", err) + } + + backendInstance, err := New(Config{ + Store: store, + Namespace: "tests", + }) + if err != nil { + t.Fatalf("create backend: %v", err) + } + + sourceDir := t.TempDir() + if err := os.MkdirAll(filepath.Join(sourceDir, "documents"), 0755); err != nil { + t.Fatalf("mkdir source dir: %v", err) + } + if err := os.WriteFile(filepath.Join(sourceDir, "documents", "doc-1.json"), []byte("{\"title\":\"v1\"}\n"), 0644); err != nil { + t.Fatalf("write source file: %v", err) + } + + firstState, err := backendInstance.Upload(context.Background(), sourceDir, backend.PublishOptions{}) + if err != nil { + t.Fatalf("upload snapshot: %v", err) + } + if !firstState.Exists || firstState.Revision == "" { + t.Fatalf("expected remote state after first upload") + } + + downloadDir := t.TempDir() + downloadState, err := backendInstance.DownloadLatest(context.Background(), downloadDir) + if err != nil { + t.Fatalf("download latest snapshot: %v", err) + } + if downloadState.Revision != firstState.Revision { + t.Fatalf("expected revision %s, got %s", firstState.Revision, downloadState.Revision) + } + + data, err := os.ReadFile(filepath.Join(downloadDir, "documents", "doc-1.json")) + if err != nil { + t.Fatalf("read downloaded file: %v", err) + } + if string(data) != "{\"title\":\"v1\"}\n" { + t.Fatalf("unexpected downloaded content: %s", string(data)) + } +} + +// TestBackendRevisionConflict 验证 snapshot_store 后端会在版本过期时返回冲突。 +func TestBackendRevisionConflict(t *testing.T) { + store, err := localfsblob.New(t.TempDir()) + if err != nil { + t.Fatalf("create blob store: %v", err) + } + + backendInstance, err := New(Config{ + Store: store, + Namespace: "tests", + }) + if err != nil { + t.Fatalf("create backend: %v", err) + } + + sourceDir := t.TempDir() + if err := os.WriteFile(filepath.Join(sourceDir, "state.json"), []byte("{\"value\":1}\n"), 0644); err != nil { + t.Fatalf("write source file: %v", err) + } + + firstState, err := backendInstance.Upload(context.Background(), sourceDir, backend.PublishOptions{}) + if err != nil { + t.Fatalf("upload first snapshot: %v", err) + } + + if err := os.WriteFile(filepath.Join(sourceDir, "state.json"), []byte("{\"value\":2}\n"), 0644); err != nil { + t.Fatalf("rewrite source file: %v", err) + } + secondState, err := backendInstance.Upload(context.Background(), sourceDir, backend.PublishOptions{ + ExpectedRevision: firstState.Revision, + }) + if err != nil { + t.Fatalf("upload second snapshot: %v", err) + } + + if err := os.WriteFile(filepath.Join(sourceDir, "state.json"), []byte("{\"value\":3}\n"), 0644); err != nil { + t.Fatalf("rewrite source file again: %v", err) + } + _, err = backendInstance.Upload(context.Background(), sourceDir, backend.PublishOptions{ + ExpectedRevision: firstState.Revision, + }) + if !errors.Is(err, backend.ErrRevisionConflict) { + t.Fatalf("expected ErrRevisionConflict, got %v", err) + } + if secondState.Revision == firstState.Revision { + t.Fatalf("expected revision to change after second upload") + } +} diff --git a/internal/syncer/backend/snapshotstore/blob/blob.go b/internal/syncer/backend/snapshotstore/blob/blob.go new file mode 100644 index 00000000..4758992f --- /dev/null +++ b/internal/syncer/backend/snapshotstore/blob/blob.go @@ -0,0 +1,34 @@ +package blob + +import ( + "context" + "errors" + "io" +) + +var ( + // ErrObjectNotFound 表示对象不存在。 + ErrObjectNotFound = errors.New("blob object not found") + // ErrConditionNotMet 表示条件写入失败。 + ErrConditionNotMet = errors.New("blob condition not met") +) + +// ObjectInfo 描述一个对象的元信息。 +type ObjectInfo struct { + Key string + Revision string + Size int64 +} + +// PutOptions 描述对象写入条件。 +type PutOptions struct { + IfMatch string +} + +// Store 描述 blob 存储的最小能力集。 +type Store interface { + Get(ctx context.Context, key string) (io.ReadCloser, ObjectInfo, error) + Put(ctx context.Context, key string, body io.Reader, options PutOptions) (ObjectInfo, error) + Stat(ctx context.Context, key string) (ObjectInfo, error) + Delete(ctx context.Context, key string) error +} diff --git a/internal/syncer/backend/snapshotstore/blob/localfs/store.go b/internal/syncer/backend/snapshotstore/blob/localfs/store.go new file mode 100644 index 00000000..12a1283d --- /dev/null +++ b/internal/syncer/backend/snapshotstore/blob/localfs/store.go @@ -0,0 +1,182 @@ +package localfs + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "voidraft/internal/syncer/backend/snapshotstore/blob" +) + +// Store 提供基于本地目录的 blob 存储实现。 +type Store struct { + rootPath string +} + +// New 创建新的 localfs blob 存储。 +func New(rootPath string) (*Store, error) { + if strings.TrimSpace(rootPath) == "" { + return nil, errors.New("localfs root path is required") + } + if err := os.MkdirAll(rootPath, 0755); err != nil { + return nil, fmt.Errorf("create localfs root path: %w", err) + } + return &Store{rootPath: rootPath}, nil +} + +// Get 读取对象内容。 +func (s *Store) Get(ctx context.Context, key string) (io.ReadCloser, blob.ObjectInfo, error) { + _ = ctx + + info, err := s.Stat(ctx, key) + if err != nil { + return nil, blob.ObjectInfo{}, err + } + + path, err := s.resolvePath(key) + if err != nil { + return nil, blob.ObjectInfo{}, err + } + + reader, err := os.Open(path) + if err != nil { + if os.IsNotExist(err) { + return nil, blob.ObjectInfo{}, blob.ErrObjectNotFound + } + return nil, blob.ObjectInfo{}, err + } + + return reader, info, nil +} + +// Put 写入对象内容。 +func (s *Store) Put(ctx context.Context, key string, body io.Reader, options blob.PutOptions) (blob.ObjectInfo, error) { + _ = ctx + + path, err := s.resolvePath(key) + if err != nil { + return blob.ObjectInfo{}, err + } + + if options.IfMatch != "" { + currentInfo, err := s.Stat(ctx, key) + if err != nil { + if errors.Is(err, blob.ErrObjectNotFound) { + return blob.ObjectInfo{}, blob.ErrConditionNotMet + } + return blob.ObjectInfo{}, err + } + if currentInfo.Revision != options.IfMatch { + return blob.ObjectInfo{}, blob.ErrConditionNotMet + } + } + + data, err := io.ReadAll(body) + if err != nil { + return blob.ObjectInfo{}, err + } + + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return blob.ObjectInfo{}, err + } + + tempFile, err := os.CreateTemp(filepath.Dir(path), "blob-put-*") + if err != nil { + return blob.ObjectInfo{}, err + } + tempName := tempFile.Name() + + if _, err := tempFile.Write(data); err != nil { + tempFile.Close() + _ = os.Remove(tempName) + return blob.ObjectInfo{}, err + } + if err := tempFile.Close(); err != nil { + _ = os.Remove(tempName) + return blob.ObjectInfo{}, err + } + if err := os.Rename(tempName, path); err != nil { + _ = os.Remove(tempName) + return blob.ObjectInfo{}, err + } + + return blob.ObjectInfo{ + Key: key, + Revision: digest(data), + Size: int64(len(data)), + }, nil +} + +// Stat 返回对象元信息。 +func (s *Store) Stat(ctx context.Context, key string) (blob.ObjectInfo, error) { + _ = ctx + + path, err := s.resolvePath(key) + if err != nil { + return blob.ObjectInfo{}, err + } + + file, err := os.Open(path) + if err != nil { + if os.IsNotExist(err) { + return blob.ObjectInfo{}, blob.ErrObjectNotFound + } + return blob.ObjectInfo{}, err + } + defer file.Close() + + hash := sha256.New() + size, err := io.Copy(hash, file) + if err != nil { + return blob.ObjectInfo{}, err + } + + return blob.ObjectInfo{ + Key: key, + Revision: hex.EncodeToString(hash.Sum(nil)), + Size: size, + }, nil +} + +// Delete 删除指定对象。 +func (s *Store) Delete(ctx context.Context, key string) error { + _ = ctx + + path, err := s.resolvePath(key) + if err != nil { + return err + } + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + +// resolvePath 将对象键转换为安全路径。 +func (s *Store) resolvePath(key string) (string, error) { + normalized := filepath.Clean(filepath.FromSlash(key)) + if normalized == "." || normalized == string(filepath.Separator) { + return "", errors.New("invalid blob key") + } + + path := filepath.Join(s.rootPath, normalized) + rel, err := filepath.Rel(s.rootPath, path) + if err != nil { + return "", err + } + if strings.HasPrefix(rel, "..") { + return "", errors.New("blob key escapes root path") + } + return path, nil +} + +// digest 计算内容摘要。 +func digest(data []byte) string { + sum := sha256.Sum256(data) + return hex.EncodeToString(sum[:]) +} diff --git a/internal/syncer/backend/snapshotstore/blob/localfs/store_test.go b/internal/syncer/backend/snapshotstore/blob/localfs/store_test.go new file mode 100644 index 00000000..a56c5671 --- /dev/null +++ b/internal/syncer/backend/snapshotstore/blob/localfs/store_test.go @@ -0,0 +1,73 @@ +package localfs + +import ( + "bytes" + "context" + "errors" + "io" + "testing" + "voidraft/internal/syncer/backend/snapshotstore/blob" +) + +// TestStorePutGetStat 验证 localfs blob 存储的基本读写流程。 +func TestStorePutGetStat(t *testing.T) { + store, err := New(t.TempDir()) + if err != nil { + t.Fatalf("create store: %v", err) + } + + info, err := store.Put(context.Background(), "nested/file.txt", bytes.NewReader([]byte("hello")), blob.PutOptions{}) + if err != nil { + t.Fatalf("put object: %v", err) + } + if info.Revision == "" { + t.Fatalf("expected revision to be generated") + } + + stat, err := store.Stat(context.Background(), "nested/file.txt") + if err != nil { + t.Fatalf("stat object: %v", err) + } + if stat.Revision != info.Revision { + t.Fatalf("expected stat revision %s, got %s", info.Revision, stat.Revision) + } + + reader, _, err := store.Get(context.Background(), "nested/file.txt") + if err != nil { + t.Fatalf("get object: %v", err) + } + defer reader.Close() + + data, err := io.ReadAll(reader) + if err != nil { + t.Fatalf("read object: %v", err) + } + if string(data) != "hello" { + t.Fatalf("expected object content hello, got %s", string(data)) + } +} + +// TestStorePutIfMatch 验证 localfs blob 存储的条件写入。 +func TestStorePutIfMatch(t *testing.T) { + store, err := New(t.TempDir()) + if err != nil { + t.Fatalf("create store: %v", err) + } + + info, err := store.Put(context.Background(), "file.txt", bytes.NewReader([]byte("v1")), blob.PutOptions{}) + if err != nil { + t.Fatalf("put initial object: %v", err) + } + + if _, err := store.Put(context.Background(), "file.txt", bytes.NewReader([]byte("v2")), blob.PutOptions{IfMatch: "stale"}); !errors.Is(err, blob.ErrConditionNotMet) { + t.Fatalf("expected ErrConditionNotMet, got %v", err) + } + + nextInfo, err := store.Put(context.Background(), "file.txt", bytes.NewReader([]byte("v2")), blob.PutOptions{IfMatch: info.Revision}) + if err != nil { + t.Fatalf("put with correct if-match: %v", err) + } + if nextInfo.Revision == info.Revision { + t.Fatalf("expected revision to change after overwrite") + } +} diff --git a/internal/syncer/config.go b/internal/syncer/config.go new file mode 100644 index 00000000..f02f00c4 --- /dev/null +++ b/internal/syncer/config.go @@ -0,0 +1,173 @@ +package syncer + +import ( + "errors" + "fmt" + "strings" + "time" +) + +const ( + // DefaultBranch 是默认 Git 分支名。 + DefaultBranch = "master" + // DefaultRemoteName 是默认 Git 远端名。 + DefaultRemoteName = "origin" + // DefaultHeadKey 是默认同步头文件名。 + DefaultHeadKey = "head.json" +) + +const ( + // TargetKindGit 表示 Git 同步目标。 + TargetKindGit = "git" + // TargetKindLocalFS 表示本地文件系统同步目标。 + TargetKindLocalFS = "localfs" +) + +// Config 描述整个同步系统的运行配置。 +type Config struct { + Targets []TargetConfig +} + +// TargetConfig 描述单个同步目标的配置。 +type TargetConfig struct { + Kind string + Enabled bool + Schedule ScheduleConfig + Git *GitTargetConfig + LocalFS *LocalFSTargetConfig +} + +// ScheduleConfig 描述自动同步调度配置。 +type ScheduleConfig struct { + AutoSync bool + Interval time.Duration +} + +// GitTargetConfig 描述 Git 同步目标配置。 +type GitTargetConfig struct { + RepoPath string + RepoURL string + Branch string + RemoteName string + AuthorName string + AuthorEmail string + Auth GitAuthConfig +} + +// GitAuthConfig 描述 Git 鉴权配置。 +type GitAuthConfig struct { + Method string + Username string + Password string + Token string + SSHKeyPath string + SSHKeyPassword string +} + +// LocalFSTargetConfig 描述本地文件系统同步目标配置。 +type LocalFSTargetConfig struct { + Namespace string + HeadKey string + RootPath string +} + +// Normalize 返回带默认值的配置副本。 +func (c Config) Normalize() Config { + if len(c.Targets) == 0 { + return Config{} + } + + targets := make([]TargetConfig, 0, len(c.Targets)) + for _, target := range c.Targets { + targets = append(targets, target.Normalize()) + } + + return Config{Targets: targets} +} + +// Target 返回指定 kind 的目标配置。 +func (c Config) Target(targetKind string) (TargetConfig, error) { + for _, target := range c.Targets { + if target.Kind == targetKind { + return target, nil + } + } + return TargetConfig{}, fmt.Errorf("%w: %s", ErrTargetNotFound, targetKind) +} + +// Normalize 返回带默认值的目标配置副本。 +func (t TargetConfig) Normalize() TargetConfig { + target := t + if target.Kind == "" { + target.Kind = TargetKindGit + } + if target.Schedule.Interval < 0 { + target.Schedule.Interval = 0 + } + if target.Kind == TargetKindGit && target.Git != nil { + gitConfig := *target.Git + if strings.TrimSpace(gitConfig.Branch) == "" { + gitConfig.Branch = DefaultBranch + } + if strings.TrimSpace(gitConfig.RemoteName) == "" { + gitConfig.RemoteName = DefaultRemoteName + } + target.Git = &gitConfig + } + if target.Kind == TargetKindLocalFS && target.LocalFS != nil { + storeConfig := *target.LocalFS + if strings.TrimSpace(storeConfig.Namespace) == "" { + storeConfig.Namespace = target.Kind + } + if strings.TrimSpace(storeConfig.HeadKey) == "" { + storeConfig.HeadKey = DefaultHeadKey + } + target.LocalFS = &storeConfig + } + return target +} + +// Validate 校验目标配置。 +func (t TargetConfig) Validate() error { + switch t.Kind { + case TargetKindGit: + if t.Git == nil { + return errors.New("git target config is required") + } + if strings.TrimSpace(t.Git.RepoPath) == "" { + return errors.New("git repo path is required") + } + case TargetKindLocalFS: + if t.LocalFS == nil { + return errors.New("localfs target config is required") + } + if strings.TrimSpace(t.LocalFS.RootPath) == "" { + return errors.New("localfs root path is required") + } + default: + return fmt.Errorf("%w: %s", ErrUnsupportedBackend, t.Kind) + } + return nil +} + +// Ready 判断目标是否具备执行同步的必要信息。 +func (t TargetConfig) Ready() bool { + if !t.Enabled { + return false + } + + switch t.Kind { + case TargetKindGit: + if t.Git == nil { + return false + } + return strings.TrimSpace(t.Git.RepoPath) != "" && strings.TrimSpace(t.Git.RepoURL) != "" + case TargetKindLocalFS: + if t.LocalFS == nil { + return false + } + return strings.TrimSpace(t.LocalFS.RootPath) != "" + default: + return false + } +} diff --git a/internal/syncer/engine/sync_engine.go b/internal/syncer/engine/sync_engine.go new file mode 100644 index 00000000..174b6025 --- /dev/null +++ b/internal/syncer/engine/sync_engine.go @@ -0,0 +1,184 @@ +package engine + +import ( + "context" + "errors" + "fmt" + "os" + "voidraft/internal/syncer/backend" + "voidraft/internal/syncer/merge" + "voidraft/internal/syncer/snapshot" +) + +const defaultMaxAttempts = 3 + +// Logger 描述同步引擎依赖的最小日志接口。 +type Logger interface { + Debug(message string, args ...interface{}) + Info(message string, args ...interface{}) + Warning(message string, args ...interface{}) + Error(message string, args ...interface{}) +} + +// Options 描述同步引擎构造选项。 +type Options struct { + Logger Logger + MaxAttempts int +} + +// SyncOptions 描述一次同步执行参数。 +type SyncOptions struct { + CommitMessage string +} + +// Result 描述同步引擎执行结果。 +type Result struct { + LocalChanged bool + RemoteChanged bool + AppliedToLocal bool + Published bool + ConflictCount int + Revision string +} + +// SyncEngine 负责执行一次完整的同步闭环。 +type SyncEngine struct { + backend backend.Backend + store snapshot.Store + snapshotter snapshot.Snapshotter + merger merge.Merger + logger Logger + maxAttempts int +} + +// NewSyncEngine 创建新的同步引擎实例。 +func NewSyncEngine( + backendInstance backend.Backend, + store snapshot.Store, + snapshotter snapshot.Snapshotter, + merger merge.Merger, + options Options, +) *SyncEngine { + maxAttempts := options.MaxAttempts + if maxAttempts <= 0 { + maxAttempts = defaultMaxAttempts + } + + return &SyncEngine{ + backend: backendInstance, + store: store, + snapshotter: snapshotter, + merger: merger, + logger: options.Logger, + maxAttempts: maxAttempts, + } +} + +// Sync 执行同步,并在远端版本竞争时自动重试。 +func (e *SyncEngine) Sync(ctx context.Context, options SyncOptions) (*Result, error) { + var lastErr error + + for attempt := 1; attempt <= e.maxAttempts; attempt++ { + result, retry, err := e.syncOnce(ctx, options) + if err == nil { + return result, nil + } + if retry && errors.Is(err, backend.ErrRevisionConflict) { + lastErr = err + if e.logger != nil { + e.logger.Warning("sync retry after revision conflict, attempt %d/%d", attempt, e.maxAttempts) + } + continue + } + return nil, err + } + + if lastErr == nil { + lastErr = backend.ErrRevisionConflict + } + return nil, lastErr +} + +// syncOnce 执行一次同步尝试。 +func (e *SyncEngine) syncOnce(ctx context.Context, options SyncOptions) (*Result, bool, error) { + localSnapshot, err := e.snapshotter.Export(ctx) + if err != nil { + return nil, false, fmt.Errorf("export local snapshot: %w", err) + } + + localDigest, err := snapshot.Digest(localSnapshot) + if err != nil { + return nil, false, fmt.Errorf("digest local snapshot: %w", err) + } + + remoteDir, err := os.MkdirTemp("", "voidraft-sync-remote-*") + if err != nil { + return nil, false, err + } + defer os.RemoveAll(remoteDir) + + remoteState, err := e.backend.DownloadLatest(ctx, remoteDir) + if err != nil { + return nil, false, fmt.Errorf("download remote snapshot: %w", err) + } + + remoteSnapshot := snapshot.New() + if remoteState.Exists { + remoteSnapshot, err = e.store.Read(ctx, remoteDir) + if err != nil { + return nil, false, fmt.Errorf("read remote snapshot: %w", err) + } + } + + remoteDigest, err := snapshot.Digest(remoteSnapshot) + if err != nil { + return nil, false, fmt.Errorf("digest remote snapshot: %w", err) + } + + mergedSnapshot, report, err := e.merger.Merge(ctx, localSnapshot, remoteSnapshot) + if err != nil { + return nil, false, fmt.Errorf("merge snapshot: %w", err) + } + + mergedDigest, err := snapshot.Digest(mergedSnapshot) + if err != nil { + return nil, false, fmt.Errorf("digest merged snapshot: %w", err) + } + + appliedToLocal := localDigest != mergedDigest + if appliedToLocal { + if err := e.snapshotter.Apply(ctx, mergedSnapshot); err != nil { + return nil, false, fmt.Errorf("apply merged snapshot: %w", err) + } + } + + stageDir, err := os.MkdirTemp("", "voidraft-sync-stage-*") + if err != nil { + return nil, false, err + } + defer os.RemoveAll(stageDir) + + if err := e.store.Write(ctx, stageDir, mergedSnapshot); err != nil { + return nil, false, fmt.Errorf("write merged snapshot: %w", err) + } + + publishedState, err := e.backend.Upload(ctx, stageDir, backend.PublishOptions{ + ExpectedRevision: remoteState.Revision, + Message: options.CommitMessage, + }) + if err != nil { + if errors.Is(err, backend.ErrRevisionConflict) { + return nil, true, err + } + return nil, false, fmt.Errorf("upload merged snapshot: %w", err) + } + + return &Result{ + LocalChanged: appliedToLocal, + RemoteChanged: remoteDigest != mergedDigest, + AppliedToLocal: appliedToLocal, + Published: remoteState != publishedState, + ConflictCount: report.Conflicts, + Revision: publishedState.Revision, + }, false, nil +} diff --git a/internal/syncer/errors.go b/internal/syncer/errors.go new file mode 100644 index 00000000..408158f0 --- /dev/null +++ b/internal/syncer/errors.go @@ -0,0 +1,16 @@ +package syncer + +import "errors" + +var ( + // ErrTargetNotFound 表示目标不存在。 + ErrTargetNotFound = errors.New("sync target not found") + // ErrTargetDisabled 表示目标未启用。 + ErrTargetDisabled = errors.New("sync target is disabled") + // ErrTargetNotReady 表示目标缺少必要配置。 + ErrTargetNotReady = errors.New("sync target is not ready") + // ErrUnsupportedBackend 表示后端类型未实现。 + ErrUnsupportedBackend = errors.New("sync backend is not supported") + // ErrUnsupportedDriver 表示后端驱动未实现。 + ErrUnsupportedDriver = errors.New("sync driver is not supported") +) diff --git a/internal/syncer/merge/merger.go b/internal/syncer/merge/merger.go new file mode 100644 index 00000000..c89b61ec --- /dev/null +++ b/internal/syncer/merge/merger.go @@ -0,0 +1,19 @@ +package merge + +import ( + "context" + "voidraft/internal/syncer/snapshot" +) + +// Report 描述一次合并中的统计信息。 +type Report struct { + Added int + Updated int + Deleted int + Conflicts int +} + +// Merger 描述快照合并策略。 +type Merger interface { + Merge(ctx context.Context, local *snapshot.Snapshot, remote *snapshot.Snapshot) (*snapshot.Snapshot, Report, error) +} diff --git a/internal/syncer/merge/updated_at_wins.go b/internal/syncer/merge/updated_at_wins.go new file mode 100644 index 00000000..f47652e3 --- /dev/null +++ b/internal/syncer/merge/updated_at_wins.go @@ -0,0 +1,98 @@ +package merge + +import ( + "context" + "sort" + "time" + "voidraft/internal/syncer/snapshot" +) + +// UpdatedAtWinsMerger 使用 updated_at 作为默认冲突解决依据。 +type UpdatedAtWinsMerger struct{} + +// NewUpdatedAtWinsMerger 创建新的默认合并器。 +func NewUpdatedAtWinsMerger() *UpdatedAtWinsMerger { + return &UpdatedAtWinsMerger{} +} + +// Merge 合并本地与远端快照。 +func (m *UpdatedAtWinsMerger) Merge(ctx context.Context, local *snapshot.Snapshot, remote *snapshot.Snapshot) (*snapshot.Snapshot, Report, error) { + _ = ctx + + localSnapshot := snapshot.Clone(local) + remoteSnapshot := snapshot.Clone(remote) + + index := make(map[string]snapshot.Record) + report := Report{} + + for _, kind := range sortedKinds(localSnapshot, remoteSnapshot) { + for _, record := range localSnapshot.Resources[kind] { + index[recordKey(kind, record.ID)] = snapshot.CloneRecord(record) + } + for _, remoteRecord := range remoteSnapshot.Resources[kind] { + key := recordKey(kind, remoteRecord.ID) + localRecord, exists := index[key] + if !exists { + index[key] = snapshot.CloneRecord(remoteRecord) + report.Added++ + continue + } + + switch { + case remoteRecord.UpdatedAt.After(localRecord.UpdatedAt): + index[key] = snapshot.CloneRecord(remoteRecord) + report.Updated++ + case remoteRecord.UpdatedAt.Equal(localRecord.UpdatedAt): + if snapshot.RecordDigest(localRecord) != snapshot.RecordDigest(remoteRecord) { + report.Conflicts++ + } + default: + if remoteRecord.DeletedAt != nil && localRecord.DeletedAt == nil { + report.Deleted++ + } + } + } + } + + merged := snapshot.New() + for _, key := range sortedKeys(index) { + record := index[key] + merged.Resources[record.Kind] = append(merged.Resources[record.Kind], snapshot.CloneRecord(record)) + } + merged.CreatedAt = time.Now() + + return merged, report, nil +} + +// sortedKinds 返回两个快照内的全部资源类型集合。 +func sortedKinds(local *snapshot.Snapshot, remote *snapshot.Snapshot) []string { + index := make(map[string]struct{}) + for kind := range local.Resources { + index[kind] = struct{}{} + } + for kind := range remote.Resources { + index[kind] = struct{}{} + } + + kinds := make([]string, 0, len(index)) + for kind := range index { + kinds = append(kinds, kind) + } + sort.Strings(kinds) + return kinds +} + +// sortedKeys 返回稳定排序后的索引键集合。 +func sortedKeys(index map[string]snapshot.Record) []string { + keys := make([]string, 0, len(index)) + for key := range index { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +// recordKey 生成 record 的稳定索引键。 +func recordKey(kind string, id string) string { + return kind + ":" + id +} diff --git a/internal/syncer/merge/updated_at_wins_test.go b/internal/syncer/merge/updated_at_wins_test.go new file mode 100644 index 00000000..13bd7793 --- /dev/null +++ b/internal/syncer/merge/updated_at_wins_test.go @@ -0,0 +1,50 @@ +package merge + +import ( + "context" + "testing" + "time" + "voidraft/internal/syncer/snapshot" +) + +// TestUpdatedAtWinsMergerMerge 验证较新的记录会覆盖较旧记录。 +func TestUpdatedAtWinsMergerMerge(t *testing.T) { + localRecord, err := snapshot.NewRecord("documents", "doc-1", map[string]interface{}{ + "uuid": "doc-1", + "updated_at": time.Date(2026, 3, 29, 9, 0, 0, 0, time.UTC).Format(time.RFC3339), + "title": "local", + }, nil) + if err != nil { + t.Fatalf("build local record: %v", err) + } + + remoteRecord, err := snapshot.NewRecord("documents", "doc-1", map[string]interface{}{ + "uuid": "doc-1", + "updated_at": time.Date(2026, 3, 29, 10, 0, 0, 0, time.UTC).Format(time.RFC3339), + "title": "remote", + }, nil) + if err != nil { + t.Fatalf("build remote record: %v", err) + } + + localSnapshot := snapshot.New() + localSnapshot.Resources["documents"] = []snapshot.Record{localRecord} + + remoteSnapshot := snapshot.New() + remoteSnapshot.Resources["documents"] = []snapshot.Record{remoteRecord} + + merger := NewUpdatedAtWinsMerger() + merged, report, err := merger.Merge(context.Background(), localSnapshot, remoteSnapshot) + if err != nil { + t.Fatalf("merge snapshot: %v", err) + } + + if report.Updated != 1 { + t.Fatalf("expected updated report to be 1, got %d", report.Updated) + } + + record := merged.Resources["documents"][0] + if got := record.Values["title"]; got != "remote" { + t.Fatalf("expected remote title, got %v", got) + } +} diff --git a/internal/syncer/resource/adapter.go b/internal/syncer/resource/adapter.go new file mode 100644 index 00000000..a9948825 --- /dev/null +++ b/internal/syncer/resource/adapter.go @@ -0,0 +1,61 @@ +package resource + +import ( + "context" + "sort" + "voidraft/internal/syncer/snapshot" +) + +// Adapter 描述单类资源的导出与应用能力。 +type Adapter interface { + Kind() string + Export(ctx context.Context) ([]snapshot.Record, error) + Apply(ctx context.Context, records []snapshot.Record) error +} + +// Registry 聚合所有资源适配器,并实现快照导入导出接口。 +type Registry struct { + adapters []Adapter +} + +// NewRegistry 创建新的资源注册表。 +func NewRegistry(adapters ...Adapter) *Registry { + return &Registry{adapters: adapters} +} + +// Export 导出所有已注册资源的快照。 +func (r *Registry) Export(ctx context.Context) (*snapshot.Snapshot, error) { + snap := snapshot.New() + + for _, adapter := range r.adapters { + records, err := adapter.Export(ctx) + if err != nil { + return nil, err + } + if len(records) == 0 { + continue + } + sort.Slice(records, func(i int, j int) bool { + return records[i].ID < records[j].ID + }) + snap.Resources[adapter.Kind()] = records + } + + return snap, nil +} + +// Apply 将快照内容应用到本地资源。 +func (r *Registry) Apply(ctx context.Context, snap *snapshot.Snapshot) error { + if snap == nil { + return nil + } + + for _, adapter := range r.adapters { + records := snap.Resources[adapter.Kind()] + if err := adapter.Apply(ctx, records); err != nil { + return err + } + } + + return nil +} diff --git a/internal/syncer/resource/document_adapter.go b/internal/syncer/resource/document_adapter.go new file mode 100644 index 00000000..2f6d8edd --- /dev/null +++ b/internal/syncer/resource/document_adapter.go @@ -0,0 +1,117 @@ +package resource + +import ( + "context" + "fmt" + "voidraft/internal/models/ent" + "voidraft/internal/models/ent/document" + "voidraft/internal/syncer/snapshot" +) + +const documentContentBlob = "content.md" + +// DocumentAdapter 负责文档资源的快照导入导出。 +type DocumentAdapter struct { + client *ent.Client +} + +// NewDocumentAdapter 创建文档适配器。 +func NewDocumentAdapter(client *ent.Client) *DocumentAdapter { + return &DocumentAdapter{client: client} +} + +// Kind 返回适配器负责的资源类型。 +func (a *DocumentAdapter) Kind() string { + return "documents" +} + +// Export 导出文档快照记录。 +func (a *DocumentAdapter) Export(ctx context.Context) ([]snapshot.Record, error) { + documents, err := a.client.Document.Query().Order(document.ByUUID()).All(exportContext(ctx)) + if err != nil { + return nil, err + } + + records := make([]snapshot.Record, 0, len(documents)) + for _, item := range documents { + values := map[string]interface{}{ + document.FieldUUID: item.UUID, + document.FieldCreatedAt: item.CreatedAt, + document.FieldUpdatedAt: item.UpdatedAt, + document.FieldTitle: item.Title, + document.FieldLocked: item.Locked, + } + if item.DeletedAt != nil { + values[document.FieldDeletedAt] = *item.DeletedAt + } + + record, err := snapshot.NewRecord(a.Kind(), item.UUID, values, map[string][]byte{ + documentContentBlob: []byte(item.Content), + }) + if err != nil { + return nil, fmt.Errorf("build document record %s: %w", item.UUID, err) + } + records = append(records, record) + } + + return records, nil +} + +// Apply 将快照记录应用到本地文档表。 +func (a *DocumentAdapter) Apply(ctx context.Context, records []snapshot.Record) error { + applyCtx := importContext(ctx) + + for _, record := range records { + found, err := a.client.Document.Query().Where(document.UUIDEQ(record.ID)).First(applyCtx) + switch { + case ent.IsNotFound(err): + if err := a.create(applyCtx, record); err != nil { + return err + } + case err != nil: + return err + default: + if shouldApplyRecord(found.UpdatedAt, record) { + if err := a.update(applyCtx, found.ID, record); err != nil { + return err + } + } + } + } + + return nil +} + +// create 创建新的文档记录。 +func (a *DocumentAdapter) create(ctx context.Context, record snapshot.Record) error { + builder := a.client.Document.Create(). + SetUUID(record.ID). + SetTitle(stringValue(record, document.FieldTitle)). + SetContent(blobString(record, documentContentBlob)). + SetLocked(boolValue(record, document.FieldLocked)). + SetCreatedAt(stringValue(record, document.FieldCreatedAt)). + SetUpdatedAt(stringValue(record, document.FieldUpdatedAt)) + + if deletedAt := recordDeletedAtString(record); deletedAt != nil { + builder.SetDeletedAt(*deletedAt) + } + + return builder.Exec(ctx) +} + +// update 更新已有文档记录。 +func (a *DocumentAdapter) update(ctx context.Context, id int, record snapshot.Record) error { + builder := a.client.Document.UpdateOneID(id). + SetTitle(stringValue(record, document.FieldTitle)). + SetContent(blobString(record, documentContentBlob)). + SetLocked(boolValue(record, document.FieldLocked)). + SetUpdatedAt(stringValue(record, document.FieldUpdatedAt)) + + if deletedAt := recordDeletedAtString(record); deletedAt != nil { + builder.SetDeletedAt(*deletedAt) + } else { + builder.ClearDeletedAt() + } + + return builder.Exec(ctx) +} diff --git a/internal/syncer/resource/extension_adapter.go b/internal/syncer/resource/extension_adapter.go new file mode 100644 index 00000000..ae2c4ad6 --- /dev/null +++ b/internal/syncer/resource/extension_adapter.go @@ -0,0 +1,114 @@ +package resource + +import ( + "context" + "fmt" + "voidraft/internal/models/ent" + "voidraft/internal/models/ent/extension" + "voidraft/internal/syncer/snapshot" +) + +// ExtensionAdapter 负责扩展资源的快照导入导出。 +type ExtensionAdapter struct { + client *ent.Client +} + +// NewExtensionAdapter 创建扩展适配器。 +func NewExtensionAdapter(client *ent.Client) *ExtensionAdapter { + return &ExtensionAdapter{client: client} +} + +// Kind 返回适配器负责的资源类型。 +func (a *ExtensionAdapter) Kind() string { + return "extensions" +} + +// Export 导出扩展快照记录。 +func (a *ExtensionAdapter) Export(ctx context.Context) ([]snapshot.Record, error) { + extensions, err := a.client.Extension.Query().Order(extension.ByUUID()).All(exportContext(ctx)) + if err != nil { + return nil, err + } + + records := make([]snapshot.Record, 0, len(extensions)) + for _, item := range extensions { + values := map[string]interface{}{ + extension.FieldUUID: item.UUID, + extension.FieldCreatedAt: item.CreatedAt, + extension.FieldUpdatedAt: item.UpdatedAt, + extension.FieldName: item.Name, + extension.FieldEnabled: item.Enabled, + extension.FieldConfig: cloneMap(item.Config), + } + if item.DeletedAt != nil { + values[extension.FieldDeletedAt] = *item.DeletedAt + } + + record, err := snapshot.NewRecord(a.Kind(), item.UUID, values, nil) + if err != nil { + return nil, fmt.Errorf("build extension record %s: %w", item.UUID, err) + } + records = append(records, record) + } + + return records, nil +} + +// Apply 将快照记录应用到本地扩展表。 +func (a *ExtensionAdapter) Apply(ctx context.Context, records []snapshot.Record) error { + applyCtx := importContext(ctx) + + for _, record := range records { + found, err := a.client.Extension.Query().Where(extension.UUIDEQ(record.ID)).First(applyCtx) + switch { + case ent.IsNotFound(err): + if err := a.create(applyCtx, record); err != nil { + return err + } + case err != nil: + return err + default: + if shouldApplyRecord(found.UpdatedAt, record) { + if err := a.update(applyCtx, found.ID, record); err != nil { + return err + } + } + } + } + + return nil +} + +// create 创建新的扩展记录。 +func (a *ExtensionAdapter) create(ctx context.Context, record snapshot.Record) error { + builder := a.client.Extension.Create(). + SetUUID(record.ID). + SetName(stringValue(record, extension.FieldName)). + SetEnabled(boolValue(record, extension.FieldEnabled)). + SetConfig(mapValue(record, extension.FieldConfig)). + SetCreatedAt(stringValue(record, extension.FieldCreatedAt)). + SetUpdatedAt(stringValue(record, extension.FieldUpdatedAt)) + + if deletedAt := recordDeletedAtString(record); deletedAt != nil { + builder.SetDeletedAt(*deletedAt) + } + + return builder.Exec(ctx) +} + +// update 更新已有扩展记录。 +func (a *ExtensionAdapter) update(ctx context.Context, id int, record snapshot.Record) error { + builder := a.client.Extension.UpdateOneID(id). + SetName(stringValue(record, extension.FieldName)). + SetEnabled(boolValue(record, extension.FieldEnabled)). + SetConfig(mapValue(record, extension.FieldConfig)). + SetUpdatedAt(stringValue(record, extension.FieldUpdatedAt)) + + if deletedAt := recordDeletedAtString(record); deletedAt != nil { + builder.SetDeletedAt(*deletedAt) + } else { + builder.ClearDeletedAt() + } + + return builder.Exec(ctx) +} diff --git a/internal/syncer/resource/helpers.go b/internal/syncer/resource/helpers.go new file mode 100644 index 00000000..c333a779 --- /dev/null +++ b/internal/syncer/resource/helpers.go @@ -0,0 +1,75 @@ +package resource + +import ( + "context" + "maps" + "time" + "voidraft/internal/models/schema/mixin" + "voidraft/internal/syncer/snapshot" +) + +// importContext 构造同步导入所需的上下文。 +func importContext(ctx context.Context) context.Context { + return mixin.SkipAutoUpdate(mixin.SkipSoftDelete(ctx)) +} + +// exportContext 构造同步导出所需的上下文。 +func exportContext(ctx context.Context) context.Context { + return mixin.SkipSoftDelete(ctx) +} + +// cloneMap 返回 map 的安全副本。 +func cloneMap(value map[string]interface{}) map[string]interface{} { + if value == nil { + return nil + } + return maps.Clone(value) +} + +// recordDeletedAtString 返回记录中的删除时间字符串。 +func recordDeletedAtString(record snapshot.Record) *string { + if record.DeletedAt == nil { + return nil + } + value := record.DeletedAt.Format(time.RFC3339) + return &value +} + +// shouldApplyRecord 判断记录是否应该覆盖本地数据。 +func shouldApplyRecord(localUpdatedAt string, record snapshot.Record) bool { + if localUpdatedAt == "" { + return true + } + localTime, err := time.Parse(time.RFC3339, localUpdatedAt) + if err != nil { + return true + } + return record.UpdatedAt.After(localTime) +} + +// stringValue 从记录字段中读取字符串。 +func stringValue(record snapshot.Record, key string) string { + value, _ := record.Values[key].(string) + return value +} + +// boolValue 从记录字段中读取布尔值。 +func boolValue(record snapshot.Record, key string) bool { + value, _ := record.Values[key].(bool) + return value +} + +// mapValue 从记录字段中读取 map 值。 +func mapValue(record snapshot.Record, key string) map[string]interface{} { + value, _ := record.Values[key].(map[string]interface{}) + return cloneMap(value) +} + +// blobString 读取记录中的文本 blob。 +func blobString(record snapshot.Record, name string) string { + value, ok := record.Blobs[name] + if !ok { + return "" + } + return string(value) +} diff --git a/internal/syncer/resource/keybinding_adapter.go b/internal/syncer/resource/keybinding_adapter.go new file mode 100644 index 00000000..39d82002 --- /dev/null +++ b/internal/syncer/resource/keybinding_adapter.go @@ -0,0 +1,135 @@ +package resource + +import ( + "context" + "fmt" + "voidraft/internal/models/ent" + "voidraft/internal/models/ent/keybinding" + "voidraft/internal/syncer/snapshot" +) + +// KeyBindingAdapter 负责快捷键资源的快照导入导出。 +type KeyBindingAdapter struct { + client *ent.Client +} + +// NewKeyBindingAdapter 创建快捷键适配器。 +func NewKeyBindingAdapter(client *ent.Client) *KeyBindingAdapter { + return &KeyBindingAdapter{client: client} +} + +// Kind 返回适配器负责的资源类型。 +func (a *KeyBindingAdapter) Kind() string { + return "keybindings" +} + +// Export 导出快捷键快照记录。 +func (a *KeyBindingAdapter) Export(ctx context.Context) ([]snapshot.Record, error) { + keyBindings, err := a.client.KeyBinding.Query().Order(keybinding.ByUUID()).All(exportContext(ctx)) + if err != nil { + return nil, err + } + + records := make([]snapshot.Record, 0, len(keyBindings)) + for _, item := range keyBindings { + values := map[string]interface{}{ + keybinding.FieldUUID: item.UUID, + keybinding.FieldCreatedAt: item.CreatedAt, + keybinding.FieldUpdatedAt: item.UpdatedAt, + keybinding.FieldName: item.Name, + keybinding.FieldType: item.Type, + keybinding.FieldKey: item.Key, + keybinding.FieldMacos: item.Macos, + keybinding.FieldWindows: item.Windows, + keybinding.FieldLinux: item.Linux, + keybinding.FieldExtension: item.Extension, + keybinding.FieldEnabled: item.Enabled, + keybinding.FieldPreventDefault: item.PreventDefault, + keybinding.FieldScope: item.Scope, + } + if item.DeletedAt != nil { + values[keybinding.FieldDeletedAt] = *item.DeletedAt + } + + record, err := snapshot.NewRecord(a.Kind(), item.UUID, values, nil) + if err != nil { + return nil, fmt.Errorf("build keybinding record %s: %w", item.UUID, err) + } + records = append(records, record) + } + + return records, nil +} + +// Apply 将快照记录应用到本地快捷键表。 +func (a *KeyBindingAdapter) Apply(ctx context.Context, records []snapshot.Record) error { + applyCtx := importContext(ctx) + + for _, record := range records { + found, err := a.client.KeyBinding.Query().Where(keybinding.UUIDEQ(record.ID)).First(applyCtx) + switch { + case ent.IsNotFound(err): + if err := a.create(applyCtx, record); err != nil { + return err + } + case err != nil: + return err + default: + if shouldApplyRecord(found.UpdatedAt, record) { + if err := a.update(applyCtx, found.ID, record); err != nil { + return err + } + } + } + } + + return nil +} + +// create 创建新的快捷键记录。 +func (a *KeyBindingAdapter) create(ctx context.Context, record snapshot.Record) error { + builder := a.client.KeyBinding.Create(). + SetUUID(record.ID). + SetName(stringValue(record, keybinding.FieldName)). + SetType(stringValue(record, keybinding.FieldType)). + SetKey(stringValue(record, keybinding.FieldKey)). + SetMacos(stringValue(record, keybinding.FieldMacos)). + SetWindows(stringValue(record, keybinding.FieldWindows)). + SetLinux(stringValue(record, keybinding.FieldLinux)). + SetExtension(stringValue(record, keybinding.FieldExtension)). + SetEnabled(boolValue(record, keybinding.FieldEnabled)). + SetPreventDefault(boolValue(record, keybinding.FieldPreventDefault)). + SetScope(stringValue(record, keybinding.FieldScope)). + SetCreatedAt(stringValue(record, keybinding.FieldCreatedAt)). + SetUpdatedAt(stringValue(record, keybinding.FieldUpdatedAt)) + + if deletedAt := recordDeletedAtString(record); deletedAt != nil { + builder.SetDeletedAt(*deletedAt) + } + + return builder.Exec(ctx) +} + +// update 更新已有快捷键记录。 +func (a *KeyBindingAdapter) update(ctx context.Context, id int, record snapshot.Record) error { + builder := a.client.KeyBinding.UpdateOneID(id). + SetName(stringValue(record, keybinding.FieldName)). + SetType(stringValue(record, keybinding.FieldType)). + SetKey(stringValue(record, keybinding.FieldKey)). + SetMacos(stringValue(record, keybinding.FieldMacos)). + SetWindows(stringValue(record, keybinding.FieldWindows)). + SetLinux(stringValue(record, keybinding.FieldLinux)). + SetExtension(stringValue(record, keybinding.FieldExtension)). + SetEnabled(boolValue(record, keybinding.FieldEnabled)). + SetPreventDefault(boolValue(record, keybinding.FieldPreventDefault)). + SetScope(stringValue(record, keybinding.FieldScope)). + SetUpdatedAt(stringValue(record, keybinding.FieldUpdatedAt)) + + if deletedAt := recordDeletedAtString(record); deletedAt != nil { + builder.SetDeletedAt(*deletedAt) + } else { + builder.ClearDeletedAt() + } + + return builder.Exec(ctx) +} diff --git a/internal/syncer/resource/theme_adapter.go b/internal/syncer/resource/theme_adapter.go new file mode 100644 index 00000000..2f56328d --- /dev/null +++ b/internal/syncer/resource/theme_adapter.go @@ -0,0 +1,114 @@ +package resource + +import ( + "context" + "fmt" + "voidraft/internal/models/ent" + "voidraft/internal/models/ent/theme" + "voidraft/internal/syncer/snapshot" +) + +// ThemeAdapter 负责主题资源的快照导入导出。 +type ThemeAdapter struct { + client *ent.Client +} + +// NewThemeAdapter 创建主题适配器。 +func NewThemeAdapter(client *ent.Client) *ThemeAdapter { + return &ThemeAdapter{client: client} +} + +// Kind 返回适配器负责的资源类型。 +func (a *ThemeAdapter) Kind() string { + return "themes" +} + +// Export 导出主题快照记录。 +func (a *ThemeAdapter) Export(ctx context.Context) ([]snapshot.Record, error) { + themes, err := a.client.Theme.Query().Order(theme.ByUUID()).All(exportContext(ctx)) + if err != nil { + return nil, err + } + + records := make([]snapshot.Record, 0, len(themes)) + for _, item := range themes { + values := map[string]interface{}{ + theme.FieldUUID: item.UUID, + theme.FieldCreatedAt: item.CreatedAt, + theme.FieldUpdatedAt: item.UpdatedAt, + theme.FieldName: item.Name, + theme.FieldType: item.Type.String(), + theme.FieldColors: cloneMap(item.Colors), + } + if item.DeletedAt != nil { + values[theme.FieldDeletedAt] = *item.DeletedAt + } + + record, err := snapshot.NewRecord(a.Kind(), item.UUID, values, nil) + if err != nil { + return nil, fmt.Errorf("build theme record %s: %w", item.UUID, err) + } + records = append(records, record) + } + + return records, nil +} + +// Apply 将快照记录应用到本地主题表。 +func (a *ThemeAdapter) Apply(ctx context.Context, records []snapshot.Record) error { + applyCtx := importContext(ctx) + + for _, record := range records { + found, err := a.client.Theme.Query().Where(theme.UUIDEQ(record.ID)).First(applyCtx) + switch { + case ent.IsNotFound(err): + if err := a.create(applyCtx, record); err != nil { + return err + } + case err != nil: + return err + default: + if shouldApplyRecord(found.UpdatedAt, record) { + if err := a.update(applyCtx, found.ID, record); err != nil { + return err + } + } + } + } + + return nil +} + +// create 创建新的主题记录。 +func (a *ThemeAdapter) create(ctx context.Context, record snapshot.Record) error { + builder := a.client.Theme.Create(). + SetUUID(record.ID). + SetName(stringValue(record, theme.FieldName)). + SetType(theme.Type(stringValue(record, theme.FieldType))). + SetColors(mapValue(record, theme.FieldColors)). + SetCreatedAt(stringValue(record, theme.FieldCreatedAt)). + SetUpdatedAt(stringValue(record, theme.FieldUpdatedAt)) + + if deletedAt := recordDeletedAtString(record); deletedAt != nil { + builder.SetDeletedAt(*deletedAt) + } + + return builder.Exec(ctx) +} + +// update 更新已有主题记录。 +func (a *ThemeAdapter) update(ctx context.Context, id int, record snapshot.Record) error { + builder := a.client.Theme.UpdateOneID(id). + SetName(stringValue(record, theme.FieldName)). + SetType(theme.Type(stringValue(record, theme.FieldType))). + SetColors(mapValue(record, theme.FieldColors)). + SetUpdatedAt(stringValue(record, theme.FieldUpdatedAt)) + + if deletedAt := recordDeletedAtString(record); deletedAt != nil { + builder.SetDeletedAt(*deletedAt) + } else { + builder.ClearDeletedAt() + } + + return builder.Exec(ctx) +} diff --git a/internal/syncer/scheduler/ticker.go b/internal/syncer/scheduler/ticker.go new file mode 100644 index 00000000..98c124b7 --- /dev/null +++ b/internal/syncer/scheduler/ticker.go @@ -0,0 +1,75 @@ +package scheduler + +import ( + "context" + "sync" + "time" +) + +// Ticker 提供可重启的周期任务调度器。 +type Ticker struct { + mu sync.Mutex + cancel context.CancelFunc + done chan struct{} +} + +// NewTicker 创建新的调度器实例。 +func NewTicker() *Ticker { + return &Ticker{} +} + +// Start 启动周期任务。 +func (t *Ticker) Start(interval time.Duration, job func(context.Context) error) { + if interval <= 0 || job == nil { + return + } + + t.Stop() + + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan struct{}) + ticker := time.NewTicker(interval) + + t.mu.Lock() + t.cancel = cancel + t.done = done + t.mu.Unlock() + + go func() { + defer close(done) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + _ = job(ctx) + } + } + }() +} + +// Stop 停止当前任务。 +func (t *Ticker) Stop() { + t.mu.Lock() + cancel := t.cancel + done := t.done + t.cancel = nil + t.done = nil + t.mu.Unlock() + + if cancel != nil { + cancel() + } + if done != nil { + <-done + } +} + +// Running 返回调度器是否正在运行。 +func (t *Ticker) Running() bool { + t.mu.Lock() + defer t.mu.Unlock() + return t.cancel != nil +} diff --git a/internal/syncer/snapshot/snapshot.go b/internal/syncer/snapshot/snapshot.go new file mode 100644 index 00000000..31701d9e --- /dev/null +++ b/internal/syncer/snapshot/snapshot.go @@ -0,0 +1,248 @@ +package snapshot + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "maps" + "sort" + "strings" + "time" +) + +const ( + // CurrentVersion 是当前快照格式版本。 + CurrentVersion = 1 +) + +// Snapshot 描述一次完整的数据快照。 +type Snapshot struct { + Version int + CreatedAt time.Time + Resources map[string][]Record +} + +// Record 描述单条资源记录。 +type Record struct { + Kind string + ID string + UpdatedAt time.Time + DeletedAt *time.Time + Values map[string]interface{} + Blobs map[string][]byte +} + +// Snapshotter 描述快照导出与应用接口。 +type Snapshotter interface { + Export(ctx context.Context) (*Snapshot, error) + Apply(ctx context.Context, snap *Snapshot) error +} + +// New 创建新的空快照。 +func New() *Snapshot { + return &Snapshot{ + Version: CurrentVersion, + CreatedAt: time.Now(), + Resources: make(map[string][]Record), + } +} + +// NewRecord 根据业务字段构造规范化记录。 +func NewRecord(kind string, id string, values map[string]interface{}, blobs map[string][]byte) (Record, error) { + if strings.TrimSpace(kind) == "" { + return Record{}, errors.New("record kind is required") + } + + normalizedValues := cloneValues(values) + if id == "" { + uuid, _ := normalizedValues["uuid"].(string) + id = uuid + } + if id == "" { + return Record{}, errors.New("record id is required") + } + normalizedValues["uuid"] = id + + updatedAt, err := parseRequiredTime(normalizedValues["updated_at"]) + if err != nil { + return Record{}, fmt.Errorf("record %s updated_at: %w", id, err) + } + + deletedAt, err := parseOptionalTime(normalizedValues["deleted_at"]) + if err != nil { + return Record{}, fmt.Errorf("record %s deleted_at: %w", id, err) + } + + return Record{ + Kind: kind, + ID: id, + UpdatedAt: updatedAt, + DeletedAt: deletedAt, + Values: normalizedValues, + Blobs: cloneBlobs(blobs), + }, nil +} + +// Clone 返回快照的深拷贝。 +func Clone(snap *Snapshot) *Snapshot { + if snap == nil { + return New() + } + + cloned := &Snapshot{ + Version: snap.Version, + CreatedAt: snap.CreatedAt, + Resources: make(map[string][]Record, len(snap.Resources)), + } + for kind, records := range snap.Resources { + copied := make([]Record, 0, len(records)) + for _, record := range records { + copied = append(copied, CloneRecord(record)) + } + cloned.Resources[kind] = copied + } + return cloned +} + +// CloneRecord 返回记录的深拷贝。 +func CloneRecord(record Record) Record { + return Record{ + Kind: record.Kind, + ID: record.ID, + UpdatedAt: record.UpdatedAt, + DeletedAt: cloneTime(record.DeletedAt), + Values: cloneValues(record.Values), + Blobs: cloneBlobs(record.Blobs), + } +} + +// Digest 计算快照的稳定摘要。 +func Digest(snap *Snapshot) (string, error) { + normalized := Clone(snap) + + type digestRecord struct { + ID string `json:"id"` + UpdatedAt string `json:"updated_at"` + DeletedAt *string `json:"deleted_at,omitempty"` + Values map[string]interface{} `json:"values"` + Blobs map[string][]byte `json:"blobs,omitempty"` + } + + payload := struct { + Version int `json:"version"` + Resources map[string][]digestRecord `json:"resources"` + }{ + Version: normalized.Version, + Resources: make(map[string][]digestRecord, len(normalized.Resources)), + } + + for _, kind := range sortedKinds(normalized.Resources) { + records := normalized.Resources[kind] + sort.Slice(records, func(i int, j int) bool { + return records[i].ID < records[j].ID + }) + + items := make([]digestRecord, 0, len(records)) + for _, record := range records { + var deletedAt *string + if record.DeletedAt != nil { + value := record.DeletedAt.Format(time.RFC3339) + deletedAt = &value + } + items = append(items, digestRecord{ + ID: record.ID, + UpdatedAt: record.UpdatedAt.Format(time.RFC3339), + DeletedAt: deletedAt, + Values: record.Values, + Blobs: record.Blobs, + }) + } + payload.Resources[kind] = items + } + + data, err := json.Marshal(payload) + if err != nil { + return "", err + } + + sum := sha256.Sum256(data) + return hex.EncodeToString(sum[:]), nil +} + +// RecordDigest 计算单条记录的稳定摘要。 +func RecordDigest(record Record) string { + sum, err := Digest(&Snapshot{ + Version: CurrentVersion, + Resources: map[string][]Record{ + record.Kind: {CloneRecord(record)}, + }, + }) + if err != nil { + return "" + } + return sum +} + +// cloneValues 复制字段 map。 +func cloneValues(values map[string]interface{}) map[string]interface{} { + if values == nil { + return map[string]interface{}{} + } + return maps.Clone(values) +} + +// cloneBlobs 复制二进制 blob 集合。 +func cloneBlobs(blobs map[string][]byte) map[string][]byte { + if len(blobs) == 0 { + return nil + } + copied := make(map[string][]byte, len(blobs)) + for name, blob := range blobs { + copied[name] = append([]byte(nil), blob...) + } + return copied +} + +// cloneTime 复制时间指针。 +func cloneTime(value *time.Time) *time.Time { + if value == nil { + return nil + } + cloned := *value + return &cloned +} + +// parseRequiredTime 解析必填时间字段。 +func parseRequiredTime(value interface{}) (time.Time, error) { + text, _ := value.(string) + if text == "" { + return time.Time{}, errors.New("time value is required") + } + return time.Parse(time.RFC3339, text) +} + +// parseOptionalTime 解析可选时间字段。 +func parseOptionalTime(value interface{}) (*time.Time, error) { + text, _ := value.(string) + if text == "" { + return nil, nil + } + parsed, err := time.Parse(time.RFC3339, text) + if err != nil { + return nil, err + } + return &parsed, nil +} + +// sortedKinds 返回稳定排序后的资源类型列表。 +func sortedKinds(resources map[string][]Record) []string { + kinds := make([]string, 0, len(resources)) + for kind := range resources { + kinds = append(kinds, kind) + } + sort.Strings(kinds) + return kinds +} diff --git a/internal/syncer/snapshot/store.go b/internal/syncer/snapshot/store.go new file mode 100644 index 00000000..dddf4286 --- /dev/null +++ b/internal/syncer/snapshot/store.go @@ -0,0 +1,266 @@ +package snapshot + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "sort" + "strings" + "time" +) + +const manifestFileName = "manifest.json" + +// Store 描述快照落盘与读取能力。 +type Store interface { + Read(ctx context.Context, root string) (*Snapshot, error) + Write(ctx context.Context, root string, snap *Snapshot) error +} + +// FileStore 提供基于目录树的快照读写实现。 +type FileStore struct{} + +type manifest struct { + Version int `json:"version"` + CreatedAt string `json:"created_at"` +} + +// NewFileStore 创建新的文件快照存储。 +func NewFileStore() *FileStore { + return &FileStore{} +} + +// Read 从目录树读取快照。 +func (s *FileStore) Read(ctx context.Context, root string) (*Snapshot, error) { + _ = ctx + + info, err := os.Stat(root) + if os.IsNotExist(err) { + return New(), nil + } + if err != nil { + return nil, err + } + if !info.IsDir() { + return New(), nil + } + + snap := New() + if err := s.readManifest(root, snap); err != nil { + return nil, err + } + + entries, err := os.ReadDir(root) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + kind := entry.Name() + records, err := s.readKind(filepath.Join(root, kind), kind) + if err != nil { + return nil, err + } + if len(records) == 0 { + continue + } + sort.Slice(records, func(i int, j int) bool { + return records[i].ID < records[j].ID + }) + snap.Resources[kind] = records + } + + return snap, nil +} + +// Write 将快照写入目录树。 +func (s *FileStore) Write(ctx context.Context, root string, snap *Snapshot) error { + _ = ctx + + if err := os.RemoveAll(root); err != nil { + return err + } + if err := os.MkdirAll(root, 0755); err != nil { + return err + } + + if err := s.writeManifest(root, snap); err != nil { + return err + } + + for _, kind := range sortedKinds(snap.Resources) { + kindDir := filepath.Join(root, kind) + if err := os.MkdirAll(kindDir, 0755); err != nil { + return err + } + + records := append([]Record(nil), snap.Resources[kind]...) + sort.Slice(records, func(i int, j int) bool { + return records[i].ID < records[j].ID + }) + + for _, record := range records { + if len(record.Blobs) == 0 { + if err := writeJSON(filepath.Join(kindDir, record.ID+".json"), record.Values); err != nil { + return err + } + continue + } + + recordDir := filepath.Join(kindDir, record.ID) + if err := os.MkdirAll(recordDir, 0755); err != nil { + return err + } + if err := writeJSON(filepath.Join(recordDir, "record.json"), record.Values); err != nil { + return err + } + + blobNames := make([]string, 0, len(record.Blobs)) + for name := range record.Blobs { + blobNames = append(blobNames, name) + } + sort.Strings(blobNames) + + for _, blobName := range blobNames { + if err := os.WriteFile(filepath.Join(recordDir, blobName), record.Blobs[blobName], 0644); err != nil { + return err + } + } + } + } + + return nil +} + +// readManifest 读取快照 manifest。 +func (s *FileStore) readManifest(root string, snap *Snapshot) error { + data, err := os.ReadFile(filepath.Join(root, manifestFileName)) + if os.IsNotExist(err) { + return nil + } + if err != nil { + return err + } + + var current manifest + if err := json.Unmarshal(data, ¤t); err != nil { + return err + } + + snap.Version = current.Version + if current.CreatedAt != "" { + createdAt, err := time.Parse(time.RFC3339, current.CreatedAt) + if err != nil { + return err + } + snap.CreatedAt = createdAt + } + return nil +} + +// writeManifest 写入快照 manifest。 +func (s *FileStore) writeManifest(root string, snap *Snapshot) error { + payload := manifest{ + Version: snap.Version, + CreatedAt: snap.CreatedAt.Format(time.RFC3339), + } + return writeJSON(filepath.Join(root, manifestFileName), payload) +} + +// readKind 读取单类资源目录。 +func (s *FileStore) readKind(root string, kind string) ([]Record, error) { + entries, err := os.ReadDir(root) + if err != nil { + return nil, err + } + + records := make([]Record, 0, len(entries)) + for _, entry := range entries { + switch { + case entry.IsDir(): + record, err := s.readBlobRecord(filepath.Join(root, entry.Name()), kind) + if err != nil { + return nil, err + } + records = append(records, record) + case strings.HasSuffix(entry.Name(), ".json"): + record, err := s.readFlatRecord(filepath.Join(root, entry.Name()), kind) + if err != nil { + return nil, err + } + records = append(records, record) + } + } + return records, nil +} + +// readFlatRecord 读取单文件记录。 +func (s *FileStore) readFlatRecord(path string, kind string) (Record, error) { + values, err := readValues(path) + if err != nil { + return Record{}, err + } + + id := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) + return NewRecord(kind, id, values, nil) +} + +// readBlobRecord 读取目录型记录。 +func (s *FileStore) readBlobRecord(root string, kind string) (Record, error) { + values, err := readValues(filepath.Join(root, "record.json")) + if err != nil { + return Record{}, err + } + + entries, err := os.ReadDir(root) + if err != nil { + return Record{}, err + } + + blobs := make(map[string][]byte) + for _, entry := range entries { + if entry.IsDir() || entry.Name() == "record.json" { + continue + } + content, err := os.ReadFile(filepath.Join(root, entry.Name())) + if err != nil { + return Record{}, err + } + blobs[entry.Name()] = content + } + + return NewRecord(kind, filepath.Base(root), values, blobs) +} + +// readValues 读取 JSON 字段集合。 +func readValues(path string) (map[string]interface{}, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + values := make(map[string]interface{}) + if err := json.Unmarshal(data, &values); err != nil { + return nil, err + } + return values, nil +} + +// writeJSON 将结构体格式化写入 JSON 文件。 +func writeJSON(path string, value interface{}) error { + data, err := json.MarshalIndent(value, "", " ") + if err != nil { + return err + } + data = append(data, '\n') + + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return err + } + return os.WriteFile(path, data, 0644) +} diff --git a/internal/syncer/snapshot/store_test.go b/internal/syncer/snapshot/store_test.go new file mode 100644 index 00000000..d6085dda --- /dev/null +++ b/internal/syncer/snapshot/store_test.go @@ -0,0 +1,59 @@ +package snapshot + +import ( + "context" + "testing" + "time" +) + +// TestFileStoreReadWrite 验证目录树快照可以稳定往返。 +func TestFileStoreReadWrite(t *testing.T) { + root := t.TempDir() + + documentRecord, err := NewRecord("documents", "doc-1", map[string]interface{}{ + "uuid": "doc-1", + "updated_at": time.Date(2026, 3, 29, 10, 0, 0, 0, time.UTC).Format(time.RFC3339), + "title": "hello", + }, map[string][]byte{ + "content.md": []byte("world"), + }) + if err != nil { + t.Fatalf("build document record: %v", err) + } + + themeRecord, err := NewRecord("themes", "theme-1", map[string]interface{}{ + "uuid": "theme-1", + "updated_at": time.Date(2026, 3, 29, 10, 1, 0, 0, time.UTC).Format(time.RFC3339), + "name": "dark", + }, nil) + if err != nil { + t.Fatalf("build theme record: %v", err) + } + + snap := New() + snap.Resources["documents"] = []Record{documentRecord} + snap.Resources["themes"] = []Record{themeRecord} + + store := NewFileStore() + if err := store.Write(context.Background(), root, snap); err != nil { + t.Fatalf("write snapshot: %v", err) + } + + loaded, err := store.Read(context.Background(), root) + if err != nil { + t.Fatalf("read snapshot: %v", err) + } + + originalDigest, err := Digest(snap) + if err != nil { + t.Fatalf("digest original snapshot: %v", err) + } + loadedDigest, err := Digest(loaded) + if err != nil { + t.Fatalf("digest loaded snapshot: %v", err) + } + + if originalDigest != loadedDigest { + t.Fatalf("expected digests to match, got %s != %s", originalDigest, loadedDigest) + } +} diff --git a/internal/syncer/types.go b/internal/syncer/types.go new file mode 100644 index 00000000..f328e67b --- /dev/null +++ b/internal/syncer/types.go @@ -0,0 +1,20 @@ +package syncer + +// Logger 描述同步模块需要的最小日志接口。 +type Logger interface { + Debug(message string, args ...interface{}) + Info(message string, args ...interface{}) + Warning(message string, args ...interface{}) + Error(message string, args ...interface{}) +} + +// SyncResult 描述一次同步的结果。 +type SyncResult struct { + TargetID string + LocalChanged bool + RemoteChanged bool + AppliedToLocal bool + Published bool + ConflictCount int + Revision string +} From de7b1007744fd386e1ea6e8c5af84871b7a8f658 Mon Sep 17 00:00:00 2001 From: landaiqing Date: Mon, 30 Mar 2026 09:32:38 +0800 Subject: [PATCH 04/26] :bug: Fixed build issue --- .../wails/v3/pkg/application/index.ts | 4 - .../wails/v3/pkg/application/models.ts | 63 ------------- .../wails/v3/pkg/services/dock/dockservice.ts | 60 +++--------- .../wails/v3/pkg/services/dock/index.ts | 4 +- .../wails/v3/pkg/services/dock/models.ts | 2 +- .../v3/pkg/services/notifications/index.ts | 6 +- .../v3/pkg/services/notifications/models.ts | 4 +- .../notifications/notificationservice.ts | 94 +++++-------------- frontend/bindings/image/color/index.ts | 4 +- frontend/bindings/image/color/models.ts | 2 +- frontend/bindings/net/http/index.ts | 4 +- frontend/bindings/net/http/models.ts | 4 +- frontend/bindings/time/index.ts | 4 +- frontend/bindings/time/models.ts | 2 +- .../voidraft/internal/common/helper/index.ts | 5 +- .../voidraft/internal/common/helper/models.ts | 2 +- .../internal/common/translator/index.ts | 5 +- .../internal/common/translator/models.ts | 2 +- .../voidraft/internal/models/ent/index.ts | 7 +- .../voidraft/internal/models/ent/models.ts | 8 +- .../internal/models/ent/theme/index.ts | 4 +- .../internal/models/ent/theme/models.ts | 2 +- .../voidraft/internal/models/index.ts | 28 +++++- .../internal/services/configservice.ts | 60 ++++-------- .../internal/services/databaseservice.ts | 31 ------ .../internal/services/dialogservice.ts | 12 +-- .../internal/services/documentservice.ts | 65 ++++--------- .../internal/services/extensionservice.ts | 69 +++++--------- .../internal/services/hotkeyservice.ts | 55 +++-------- .../internal/services/httpclientservice.ts | 11 +-- .../internal/services/keybindingservice.ts | 65 ++++--------- .../internal/services/migrationservice.ts | 29 ++---- .../voidraft/internal/services/models.ts | 10 +- .../internal/services/selfupdateservice.ts | 25 ++--- .../internal/services/startupservice.ts | 7 +- .../internal/services/systemservice.ts | 30 +++--- .../voidraft/internal/services/testservice.ts | 34 ++----- .../internal/services/themeservice.ts | 32 ++----- .../internal/services/translationservice.ts | 30 +++--- .../internal/services/windowservice.ts | 32 +------ frontend/src/stores/translationStore.ts | 4 +- .../httpclient/widgets/run-gutter.ts | 2 +- go.mod | 2 +- 43 files changed, 284 insertions(+), 641 deletions(-) delete mode 100644 frontend/bindings/github.com/wailsapp/wails/v3/pkg/application/index.ts delete mode 100644 frontend/bindings/github.com/wailsapp/wails/v3/pkg/application/models.ts delete mode 100644 frontend/bindings/voidraft/internal/services/databaseservice.ts diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/application/index.ts b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/application/index.ts deleted file mode 100644 index c9d993ab..00000000 --- a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/application/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -export * from "./models.js"; diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/application/models.ts b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/application/models.ts deleted file mode 100644 index 60568305..00000000 --- a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/application/models.ts +++ /dev/null @@ -1,63 +0,0 @@ -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import {Create as $Create} from "@wailsio/runtime"; - -/** - * ServiceOptions provides optional parameters for calls to [NewService]. - */ -export class ServiceOptions { - /** - * Name can be set to override the name of the service - * for logging and debugging purposes. - * - * If empty, it will default - * either to the value obtained through the [ServiceName] interface, - * or to the type name. - */ - "Name": string; - - /** - * If the service instance implements [http.Handler], - * it will be mounted on the internal asset server - * at the prefix specified by Route. - */ - "Route": string; - - /** - * MarshalError will be called if non-nil - * to marshal to JSON the error values returned by this service's methods. - * - * MarshalError is not allowed to fail, - * but it may return a nil slice to fall back - * to the globally configured error handler. - * - * If the returned slice is not nil, it must contain valid JSON. - */ - "MarshalError": any; - - /** Creates a new ServiceOptions instance. */ - constructor($$source: Partial = {}) { - if (!("Name" in $$source)) { - this["Name"] = ""; - } - if (!("Route" in $$source)) { - this["Route"] = ""; - } - if (!("MarshalError" in $$source)) { - this["MarshalError"] = null; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new ServiceOptions instance from a string or object. - */ - static createFrom($$source: any = {}): ServiceOptions { - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - return new ServiceOptions($$parsedSource as Partial); - } -} diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/dock/dockservice.ts b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/dock/dockservice.ts index 58ceb2fd..7640ac6c 100644 --- a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/dock/dockservice.ts +++ b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/dock/dockservice.ts @@ -8,11 +8,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import * as application$0 from "../../application/models.js"; +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports @@ -21,71 +17,41 @@ import * as $models from "./models.js"; /** * GetBadge returns the badge label on the application icon. */ -export function GetBadge(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1150236961) as any; - return $resultPromise; +export function GetBadge(): $CancellablePromise { + return $Call.ByID(1150236961); } /** * HideAppIcon hides the app icon in the dock/taskbar. */ -export function HideAppIcon(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3413658144) as any; - return $resultPromise; +export function HideAppIcon(): $CancellablePromise { + return $Call.ByID(3413658144); } /** * RemoveBadge removes the badge label from the application icon. */ -export function RemoveBadge(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2752757297) as any; - return $resultPromise; -} - -/** - * ServiceName returns the name of the service. - */ -export function ServiceName(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2949906614) as any; - return $resultPromise; -} - -/** - * ServiceShutdown is called when the service is unloaded. - */ -export function ServiceShutdown(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(307064411) as any; - return $resultPromise; -} - -/** - * ServiceStartup is called when the service is loaded. - */ -export function ServiceStartup(options: application$0.ServiceOptions): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1350118426, options) as any; - return $resultPromise; +export function RemoveBadge(): $CancellablePromise { + return $Call.ByID(2752757297); } /** * SetBadge sets the badge label on the application icon. */ -export function SetBadge(label: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1717705661, label) as any; - return $resultPromise; +export function SetBadge(label: string): $CancellablePromise { + return $Call.ByID(1717705661, label); } /** * SetCustomBadge sets the badge label on the application icon with custom options. */ -export function SetCustomBadge(label: string, options: $models.BadgeOptions): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2730169760, label, options) as any; - return $resultPromise; +export function SetCustomBadge(label: string, options: $models.BadgeOptions): $CancellablePromise { + return $Call.ByID(2730169760, label, options); } /** * ShowAppIcon shows the app icon in the dock/taskbar. */ -export function ShowAppIcon(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3409697379) as any; - return $resultPromise; +export function ShowAppIcon(): $CancellablePromise { + return $Call.ByID(3409697379); } diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/dock/index.ts b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/dock/index.ts index 3c4c1286..fbdaf19f 100644 --- a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/dock/index.ts +++ b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/dock/index.ts @@ -6,4 +6,6 @@ export { DockService }; -export * from "./models.js"; +export { + BadgeOptions +} from "./models.js"; diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/dock/models.ts b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/dock/models.ts index d44e5a31..f97c8a4c 100644 --- a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/dock/models.ts +++ b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/dock/models.ts @@ -3,7 +3,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Create as $Create} from "@wailsio/runtime"; +import { Create as $Create } from "@wailsio/runtime"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/index.ts b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/index.ts index 0eead288..71eda3bb 100644 --- a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/index.ts +++ b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/index.ts @@ -6,4 +6,8 @@ export { NotificationService }; -export * from "./models.js"; +export { + NotificationAction, + NotificationCategory, + NotificationOptions +} from "./models.js"; diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/models.ts b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/models.ts index cfe33d83..3fbcb827 100644 --- a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/models.ts +++ b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/models.ts @@ -3,7 +3,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Create as $Create} from "@wailsio/runtime"; +import { Create as $Create } from "@wailsio/runtime"; /** * NotificationAction represents an action button for a notification. @@ -74,7 +74,7 @@ export class NotificationOptions { "subtitle"?: string; "body"?: string; "categoryId"?: string; - "data"?: { [_: string]: any }; + "data"?: { [_ in string]?: any }; /** Creates a new NotificationOptions instance. */ constructor($$source: Partial = {}) { diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/notificationservice.ts b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/notificationservice.ts index 16af4f6d..859f3570 100644 --- a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/notificationservice.ts +++ b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/notificationservice.ts @@ -8,103 +8,55 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import * as application$0 from "../../application/models.js"; +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports import * as $models from "./models.js"; -export function CheckNotificationAuthorization(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2216952893) as any; - return $resultPromise; -} - -/** - * OnNotificationResponse registers a callback function that will be called when - * a notification response is received from the user. - */ -export function OnNotificationResponse(callback: any): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1642697808, callback) as any; - return $resultPromise; +export function CheckNotificationAuthorization(): $CancellablePromise { + return $Call.ByID(2216952893); } -export function RegisterNotificationCategory(category: $models.NotificationCategory): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2917562919, category) as any; - return $resultPromise; +export function RegisterNotificationCategory(category: $models.NotificationCategory): $CancellablePromise { + return $Call.ByID(2917562919, category); } -export function RemoveAllDeliveredNotifications(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3956282340) as any; - return $resultPromise; +export function RemoveAllDeliveredNotifications(): $CancellablePromise { + return $Call.ByID(3956282340); } -export function RemoveAllPendingNotifications(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(108821341) as any; - return $resultPromise; +export function RemoveAllPendingNotifications(): $CancellablePromise { + return $Call.ByID(108821341); } -export function RemoveDeliveredNotification(identifier: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(975691940, identifier) as any; - return $resultPromise; +export function RemoveDeliveredNotification(identifier: string): $CancellablePromise { + return $Call.ByID(975691940, identifier); } -export function RemoveNotification(identifier: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3966653866, identifier) as any; - return $resultPromise; +export function RemoveNotification(identifier: string): $CancellablePromise { + return $Call.ByID(3966653866, identifier); } -export function RemoveNotificationCategory(categoryID: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2032615554, categoryID) as any; - return $resultPromise; +export function RemoveNotificationCategory(categoryID: string): $CancellablePromise { + return $Call.ByID(2032615554, categoryID); } -export function RemovePendingNotification(identifier: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3729049703, identifier) as any; - return $resultPromise; +export function RemovePendingNotification(identifier: string): $CancellablePromise { + return $Call.ByID(3729049703, identifier); } /** * Public methods that delegate to the implementation. */ -export function RequestNotificationAuthorization(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3933442950) as any; - return $resultPromise; +export function RequestNotificationAuthorization(): $CancellablePromise { + return $Call.ByID(3933442950); } -export function SendNotification(options: $models.NotificationOptions): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3968228732, options) as any; - return $resultPromise; +export function SendNotification(options: $models.NotificationOptions): $CancellablePromise { + return $Call.ByID(3968228732, options); } -export function SendNotificationWithActions(options: $models.NotificationOptions): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1886542847, options) as any; - return $resultPromise; -} - -/** - * ServiceName returns the name of the service. - */ -export function ServiceName(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2704532675) as any; - return $resultPromise; -} - -/** - * ServiceShutdown is called when the service is unloaded. - */ -export function ServiceShutdown(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2550195434) as any; - return $resultPromise; -} - -/** - * ServiceStartup is called when the service is loaded. - */ -export function ServiceStartup(options: application$0.ServiceOptions): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(4047820929, options) as any; - return $resultPromise; +export function SendNotificationWithActions(options: $models.NotificationOptions): $CancellablePromise { + return $Call.ByID(1886542847, options); } diff --git a/frontend/bindings/image/color/index.ts b/frontend/bindings/image/color/index.ts index c9d993ab..97b507b0 100644 --- a/frontend/bindings/image/color/index.ts +++ b/frontend/bindings/image/color/index.ts @@ -1,4 +1,6 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT -export * from "./models.js"; +export { + RGBA +} from "./models.js"; diff --git a/frontend/bindings/image/color/models.ts b/frontend/bindings/image/color/models.ts index d3659784..0d4eab56 100644 --- a/frontend/bindings/image/color/models.ts +++ b/frontend/bindings/image/color/models.ts @@ -3,7 +3,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Create as $Create} from "@wailsio/runtime"; +import { Create as $Create } from "@wailsio/runtime"; /** * RGBA represents a traditional 32-bit alpha-premultiplied color, having 8 diff --git a/frontend/bindings/net/http/index.ts b/frontend/bindings/net/http/index.ts index c9d993ab..d6c0fcef 100644 --- a/frontend/bindings/net/http/index.ts +++ b/frontend/bindings/net/http/index.ts @@ -1,4 +1,6 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT -export * from "./models.js"; +export type { + Header +} from "./models.js"; diff --git a/frontend/bindings/net/http/models.ts b/frontend/bindings/net/http/models.ts index afc5e67b..7185a76f 100644 --- a/frontend/bindings/net/http/models.ts +++ b/frontend/bindings/net/http/models.ts @@ -3,7 +3,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Create as $Create} from "@wailsio/runtime"; +import { Create as $Create } from "@wailsio/runtime"; /** * A Header represents the key-value pairs in an HTTP header. @@ -11,4 +11,4 @@ import {Create as $Create} from "@wailsio/runtime"; * The keys should be in canonical form, as returned by * [CanonicalHeaderKey]. */ -export type Header = { [_: string]: string[] }; +export type Header = { [_ in string]?: string[] }; diff --git a/frontend/bindings/time/index.ts b/frontend/bindings/time/index.ts index c9d993ab..8d4454a9 100644 --- a/frontend/bindings/time/index.ts +++ b/frontend/bindings/time/index.ts @@ -1,4 +1,6 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT -export * from "./models.js"; +export type { + Time +} from "./models.js"; diff --git a/frontend/bindings/time/models.ts b/frontend/bindings/time/models.ts index 66467384..49d054a5 100644 --- a/frontend/bindings/time/models.ts +++ b/frontend/bindings/time/models.ts @@ -3,7 +3,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Create as $Create} from "@wailsio/runtime"; +import { Create as $Create } from "@wailsio/runtime"; /** * A Time represents an instant in time with nanosecond precision. diff --git a/frontend/bindings/voidraft/internal/common/helper/index.ts b/frontend/bindings/voidraft/internal/common/helper/index.ts index c9d993ab..9c654689 100644 --- a/frontend/bindings/voidraft/internal/common/helper/index.ts +++ b/frontend/bindings/voidraft/internal/common/helper/index.ts @@ -1,4 +1,7 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT -export * from "./models.js"; +export type { + CancelFunc, + ObserverCallback +} from "./models.js"; diff --git a/frontend/bindings/voidraft/internal/common/helper/models.ts b/frontend/bindings/voidraft/internal/common/helper/models.ts index 163a030e..774e4204 100644 --- a/frontend/bindings/voidraft/internal/common/helper/models.ts +++ b/frontend/bindings/voidraft/internal/common/helper/models.ts @@ -3,7 +3,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Create as $Create} from "@wailsio/runtime"; +import { Create as $Create } from "@wailsio/runtime"; /** * CancelFunc 取消订阅函数 diff --git a/frontend/bindings/voidraft/internal/common/translator/index.ts b/frontend/bindings/voidraft/internal/common/translator/index.ts index c9d993ab..e2e80247 100644 --- a/frontend/bindings/voidraft/internal/common/translator/index.ts +++ b/frontend/bindings/voidraft/internal/common/translator/index.ts @@ -1,4 +1,7 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT -export * from "./models.js"; +export { + LanguageInfo, + TranslatorType +} from "./models.js"; diff --git a/frontend/bindings/voidraft/internal/common/translator/models.ts b/frontend/bindings/voidraft/internal/common/translator/models.ts index 56f57fde..335a55a3 100644 --- a/frontend/bindings/voidraft/internal/common/translator/models.ts +++ b/frontend/bindings/voidraft/internal/common/translator/models.ts @@ -3,7 +3,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Create as $Create} from "@wailsio/runtime"; +import { Create as $Create } from "@wailsio/runtime"; /** * LanguageInfo 语言信息结构体 diff --git a/frontend/bindings/voidraft/internal/models/ent/index.ts b/frontend/bindings/voidraft/internal/models/ent/index.ts index c9d993ab..4c219d21 100644 --- a/frontend/bindings/voidraft/internal/models/ent/index.ts +++ b/frontend/bindings/voidraft/internal/models/ent/index.ts @@ -1,4 +1,9 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT -export * from "./models.js"; +export { + Document, + Extension, + KeyBinding, + Theme +} from "./models.js"; diff --git a/frontend/bindings/voidraft/internal/models/ent/models.ts b/frontend/bindings/voidraft/internal/models/ent/models.ts index 9cd4bcb8..4252265a 100644 --- a/frontend/bindings/voidraft/internal/models/ent/models.ts +++ b/frontend/bindings/voidraft/internal/models/ent/models.ts @@ -3,7 +3,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Create as $Create} from "@wailsio/runtime"; +import { Create as $Create } from "@wailsio/runtime"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports @@ -128,7 +128,7 @@ export class Extension { /** * extension config */ - "config": { [_: string]: any }; + "config": { [_ in string]?: any }; /** Creates a new Extension instance. */ constructor($$source: Partial = {}) { @@ -327,7 +327,7 @@ export class Theme { /** * theme colors */ - "colors": { [_: string]: any }; + "colors": { [_ in string]?: any }; /** Creates a new Theme instance. */ constructor($$source: Partial = {}) { @@ -344,7 +344,7 @@ export class Theme { this["name"] = ""; } if (!("type" in $$source)) { - this["type"] = ("" as theme$0.Type); + this["type"] = theme$0.Type.$zero; } if (!("colors" in $$source)) { this["colors"] = {}; diff --git a/frontend/bindings/voidraft/internal/models/ent/theme/index.ts b/frontend/bindings/voidraft/internal/models/ent/theme/index.ts index c9d993ab..5a486456 100644 --- a/frontend/bindings/voidraft/internal/models/ent/theme/index.ts +++ b/frontend/bindings/voidraft/internal/models/ent/theme/index.ts @@ -1,4 +1,6 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT -export * from "./models.js"; +export { + Type +} from "./models.js"; diff --git a/frontend/bindings/voidraft/internal/models/ent/theme/models.ts b/frontend/bindings/voidraft/internal/models/ent/theme/models.ts index 7aa3dfc1..e0ea1753 100644 --- a/frontend/bindings/voidraft/internal/models/ent/theme/models.ts +++ b/frontend/bindings/voidraft/internal/models/ent/theme/models.ts @@ -3,7 +3,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Create as $Create} from "@wailsio/runtime"; +import { Create as $Create } from "@wailsio/runtime"; /** * Type defines the type for the "type" enum field. diff --git a/frontend/bindings/voidraft/internal/models/index.ts b/frontend/bindings/voidraft/internal/models/index.ts index c9d993ab..c0ad224f 100644 --- a/frontend/bindings/voidraft/internal/models/index.ts +++ b/frontend/bindings/voidraft/internal/models/index.ts @@ -1,4 +1,30 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT -export * from "./models.js"; +export { + AppConfig, + AppearanceConfig, + AuthMethod, + ConfigMetadata, + EditingConfig, + Extension, + ExtensionName, + GeneralConfig, + GitSyncConfig, + GithubConfig, + HotkeyCombo, + KeyBinding, + KeyBindingName, + KeyBindingType, + LanguageType, + LocalFSSyncConfig, + SyncConfig, + SyncTarget, + SystemThemeType, + TabType, + UpdatesConfig +} from "./models.js"; + +export type { + ExtensionConfig +} from "./models.js"; diff --git a/frontend/bindings/voidraft/internal/services/configservice.ts b/frontend/bindings/voidraft/internal/services/configservice.ts index f7d5ecf1..e7367580 100644 --- a/frontend/bindings/voidraft/internal/services/configservice.ts +++ b/frontend/bindings/voidraft/internal/services/configservice.ts @@ -8,11 +8,8 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports import * as helper$0 from "../common/helper/models.js"; @@ -23,77 +20,52 @@ import * as models$0 from "../models/models.js"; /** * Get 获取配置项 */ -export function Get(key: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(807201772, key) as any; - return $resultPromise; +export function Get(key: string): $CancellablePromise { + return $Call.ByID(807201772, key); } /** * GetConfig 获取完整应用配置 */ -export function GetConfig(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1013336538) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function GetConfig(): $CancellablePromise { + return $Call.ByID(1013336538).then(($result: any) => { return $$createType1($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * MigrateConfig 执行配置迁移 */ -export function MigrateConfig(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(434292783) as any; - return $resultPromise; +export function MigrateConfig(): $CancellablePromise { + return $Call.ByID(434292783); } /** * ResetConfig 重置所有配置为默认值 */ -export function ResetConfig(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3593047389) as any; - return $resultPromise; -} - -/** - * ServiceShutdown 关闭服务 - */ -export function ServiceShutdown(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3963562361) as any; - return $resultPromise; -} - -/** - * ServiceStartup 服务启动时初始化 - */ -export function ServiceStartup(options: application$0.ServiceOptions): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3311949428, options) as any; - return $resultPromise; +export function ResetConfig(): $CancellablePromise { + return $Call.ByID(3593047389); } /** * Set 设置配置项 */ -export function Set(key: string, value: any): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2921955968, key, value) as any; - return $resultPromise; +export function Set(key: string, value: any): $CancellablePromise { + return $Call.ByID(2921955968, key, value); } /** * Watch 注册配置变更监听器 */ -export function Watch(path: string, callback: helper$0.ObserverCallback): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1143583035, path, callback) as any; - return $resultPromise; +export function Watch(path: string, callback: helper$0.ObserverCallback): $CancellablePromise { + return $Call.ByID(1143583035, path, callback); } /** * WatchWithContext 使用 Context 注册监听器 */ -export function WatchWithContext(path: string, callback: helper$0.ObserverCallback): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1454973098, path, callback) as any; - return $resultPromise; +export function WatchWithContext(path: string, callback: helper$0.ObserverCallback): $CancellablePromise { + return $Call.ByID(1454973098, path, callback); } // Private type creation functions diff --git a/frontend/bindings/voidraft/internal/services/databaseservice.ts b/frontend/bindings/voidraft/internal/services/databaseservice.ts deleted file mode 100644 index 132aaa50..00000000 --- a/frontend/bindings/voidraft/internal/services/databaseservice.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -/** - * DatabaseService 数据库服务 - * @module - */ - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js"; - -/** - * ServiceShutdown 服务关闭 - */ -export function ServiceShutdown(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3907893632) as any; - return $resultPromise; -} - -/** - * ServiceStartup 服务启动 - */ -export function ServiceStartup(options: application$0.ServiceOptions): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2067840771, options) as any; - return $resultPromise; -} diff --git a/frontend/bindings/voidraft/internal/services/dialogservice.ts b/frontend/bindings/voidraft/internal/services/dialogservice.ts index f0c1a580..add58ebf 100644 --- a/frontend/bindings/voidraft/internal/services/dialogservice.ts +++ b/frontend/bindings/voidraft/internal/services/dialogservice.ts @@ -8,20 +8,18 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; /** * SelectDirectory 打开目录选择对话框 */ -export function SelectDirectory(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2249533621) as any; - return $resultPromise; +export function SelectDirectory(): $CancellablePromise { + return $Call.ByID(2249533621); } /** * SelectFile 打开文件选择对话框 */ -export function SelectFile(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(37302920) as any; - return $resultPromise; +export function SelectFile(): $CancellablePromise { + return $Call.ByID(37302920); } diff --git a/frontend/bindings/voidraft/internal/services/documentservice.ts b/frontend/bindings/voidraft/internal/services/documentservice.ts index 83e96c27..89b643b0 100644 --- a/frontend/bindings/voidraft/internal/services/documentservice.ts +++ b/frontend/bindings/voidraft/internal/services/documentservice.ts @@ -8,11 +8,8 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports import * as ent$0 from "../models/ent/models.js"; @@ -20,85 +17,63 @@ import * as ent$0 from "../models/ent/models.js"; /** * CreateDocument 创建文档 */ -export function CreateDocument(title: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3360680842, title) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function CreateDocument(title: string): $CancellablePromise { + return $Call.ByID(3360680842, title).then(($result: any) => { return $$createType1($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * DeleteDocument 删除文档 */ -export function DeleteDocument(id: number): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(412287269, id) as any; - return $resultPromise; +export function DeleteDocument(id: number): $CancellablePromise { + return $Call.ByID(412287269, id); } /** * GetDocumentByID 根据ID获取文档 */ -export function GetDocumentByID(id: number): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3468193232, id) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function GetDocumentByID(id: number): $CancellablePromise { + return $Call.ByID(3468193232, id).then(($result: any) => { return $$createType1($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * ListAllDocumentsMeta 列出所有文档 */ -export function ListAllDocumentsMeta(): Promise<(ent$0.Document | null)[]> & { cancel(): void } { - let $resultPromise = $Call.ByID(3073950297) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function ListAllDocumentsMeta(): $CancellablePromise<(ent$0.Document | null)[]> { + return $Call.ByID(3073950297).then(($result: any) => { return $$createType2($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * LockDocument 锁定文档 */ -export function LockDocument(id: number): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1889494473, id) as any; - return $resultPromise; -} - -/** - * ServiceStartup 服务启动 - */ -export function ServiceStartup(options: application$0.ServiceOptions): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1474135487, options) as any; - return $resultPromise; +export function LockDocument(id: number): $CancellablePromise { + return $Call.ByID(1889494473, id); } /** * UnlockDocument 解锁文档 */ -export function UnlockDocument(id: number): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(222307930, id) as any; - return $resultPromise; +export function UnlockDocument(id: number): $CancellablePromise { + return $Call.ByID(222307930, id); } /** * UpdateDocumentContent 更新文档内容 */ -export function UpdateDocumentContent(id: number, content: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3251897116, id, content) as any; - return $resultPromise; +export function UpdateDocumentContent(id: number, content: string): $CancellablePromise { + return $Call.ByID(3251897116, id, content); } /** * UpdateDocumentTitle 更新文档标题 */ -export function UpdateDocumentTitle(id: number, title: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2045530459, id, title) as any; - return $resultPromise; +export function UpdateDocumentTitle(id: number, title: string): $CancellablePromise { + return $Call.ByID(2045530459, id, title); } // Private type creation functions diff --git a/frontend/bindings/voidraft/internal/services/extensionservice.ts b/frontend/bindings/voidraft/internal/services/extensionservice.ts index 3aea86d8..353b1131 100644 --- a/frontend/bindings/voidraft/internal/services/extensionservice.ts +++ b/frontend/bindings/voidraft/internal/services/extensionservice.ts @@ -8,11 +8,8 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports import * as models$0 from "../models/models.js"; @@ -23,89 +20,65 @@ import * as ent$0 from "../models/ent/models.js"; /** * GetDefaultExtensions 获取默认扩展配置(用于前端绑定生成) */ -export function GetDefaultExtensions(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(4036328166) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function GetDefaultExtensions(): $CancellablePromise { + return $Call.ByID(4036328166).then(($result: any) => { return $$createType1($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * GetExtensionByID 根据ID获取扩展 */ -export function GetExtensionByID(id: number): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1521424252, id) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function GetExtensionByID(id: number): $CancellablePromise { + return $Call.ByID(1521424252, id).then(($result: any) => { return $$createType3($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * GetExtensionConfig 获取扩展配置 */ -export function GetExtensionConfig(id: number): Promise<{ [_: string]: any }> & { cancel(): void } { - let $resultPromise = $Call.ByID(1629559882, id) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function GetExtensionConfig(id: number): $CancellablePromise<{ [_ in string]?: any }> { + return $Call.ByID(1629559882, id).then(($result: any) => { return $$createType4($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * GetExtensions 获取所有扩展 */ -export function GetExtensions(): Promise<(ent$0.Extension | null)[]> & { cancel(): void } { - let $resultPromise = $Call.ByID(3179289021) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function GetExtensions(): $CancellablePromise<(ent$0.Extension | null)[]> { + return $Call.ByID(3179289021).then(($result: any) => { return $$createType5($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * ResetExtensionConfig 重置单个扩展到默认状态 */ -export function ResetExtensionConfig(id: number): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3990780299, id) as any; - return $resultPromise; -} - -/** - * ServiceStartup 服务启动 - */ -export function ServiceStartup(options: application$0.ServiceOptions): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(40324057, options) as any; - return $resultPromise; +export function ResetExtensionConfig(id: number): $CancellablePromise { + return $Call.ByID(3990780299, id); } /** * SyncExtensions 同步扩展配置 */ -export function SyncExtensions(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(167560004) as any; - return $resultPromise; +export function SyncExtensions(): $CancellablePromise { + return $Call.ByID(167560004); } /** * UpdateExtensionConfig 更新扩展配置 */ -export function UpdateExtensionConfig(id: number, config: { [_: string]: any }): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3184142503, id, config) as any; - return $resultPromise; +export function UpdateExtensionConfig(id: number, config: { [_ in string]?: any }): $CancellablePromise { + return $Call.ByID(3184142503, id, config); } /** * UpdateExtensionEnabled 更新扩展启用状态 */ -export function UpdateExtensionEnabled(id: number, enabled: boolean): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1067300094, id, enabled) as any; - return $resultPromise; +export function UpdateExtensionEnabled(id: number, enabled: boolean): $CancellablePromise { + return $Call.ByID(1067300094, id, enabled); } // Private type creation functions diff --git a/frontend/bindings/voidraft/internal/services/hotkeyservice.ts b/frontend/bindings/voidraft/internal/services/hotkeyservice.ts index 02e3b05b..38f9038f 100644 --- a/frontend/bindings/voidraft/internal/services/hotkeyservice.ts +++ b/frontend/bindings/voidraft/internal/services/hotkeyservice.ts @@ -8,11 +8,8 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports import * as models$0 from "../models/models.js"; @@ -20,69 +17,45 @@ import * as models$0 from "../models/models.js"; /** * GetSupportedKeys 返回系统支持的快捷键列表 */ -export function GetSupportedKeys(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1511528650) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function GetSupportedKeys(): $CancellablePromise { + return $Call.ByID(1511528650).then(($result: any) => { return $$createType0($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * Initialize 初始化热键服务 */ -export function Initialize(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3671360458) as any; - return $resultPromise; +export function Initialize(): $CancellablePromise { + return $Call.ByID(3671360458); } /** * IsRegistered 检查是否已注册 */ -export function IsRegistered(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(106954156) as any; - return $resultPromise; +export function IsRegistered(): $CancellablePromise { + return $Call.ByID(106954156); } /** * RegisterHotkey 注册全局热键 */ -export function RegisterHotkey(combo: models$0.HotkeyCombo | null): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1103945691, combo) as any; - return $resultPromise; -} - -/** - * ServiceShutdown 关闭服务 - */ -export function ServiceShutdown(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(157291181) as any; - return $resultPromise; -} - -/** - * ServiceStartup 服务启动时初始化 - */ -export function ServiceStartup(options: application$0.ServiceOptions): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3079990808, options) as any; - return $resultPromise; +export function RegisterHotkey(combo: models$0.HotkeyCombo | null): $CancellablePromise { + return $Call.ByID(1103945691, combo); } /** * UnregisterHotkey 取消注册全局热键 */ -export function UnregisterHotkey(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3544283732) as any; - return $resultPromise; +export function UnregisterHotkey(): $CancellablePromise { + return $Call.ByID(3544283732); } /** * UpdateHotkey 更新热键配置 */ -export function UpdateHotkey(enable: boolean, combo: models$0.HotkeyCombo | null): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(823285555, enable, combo) as any; - return $resultPromise; +export function UpdateHotkey(enable: boolean, combo: models$0.HotkeyCombo | null): $CancellablePromise { + return $Call.ByID(823285555, enable, combo); } // Private type creation functions diff --git a/frontend/bindings/voidraft/internal/services/httpclientservice.ts b/frontend/bindings/voidraft/internal/services/httpclientservice.ts index 0831c48d..1f6393dc 100644 --- a/frontend/bindings/voidraft/internal/services/httpclientservice.ts +++ b/frontend/bindings/voidraft/internal/services/httpclientservice.ts @@ -8,7 +8,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports @@ -17,13 +17,10 @@ import * as $models from "./models.js"; /** * ExecuteRequest 执行HTTP请求 */ -export function ExecuteRequest(request: $models.HttpRequest | null): Promise<$models.HttpResponse | null> & { cancel(): void } { - let $resultPromise = $Call.ByID(3143343977, request) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function ExecuteRequest(request: $models.HttpRequest | null): $CancellablePromise<$models.HttpResponse | null> { + return $Call.ByID(3143343977, request).then(($result: any) => { return $$createType1($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } // Private type creation functions diff --git a/frontend/bindings/voidraft/internal/services/keybindingservice.ts b/frontend/bindings/voidraft/internal/services/keybindingservice.ts index 89b3e904..46deb800 100644 --- a/frontend/bindings/voidraft/internal/services/keybindingservice.ts +++ b/frontend/bindings/voidraft/internal/services/keybindingservice.ts @@ -8,11 +8,8 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports import * as models$0 from "../models/models.js"; @@ -23,85 +20,63 @@ import * as ent$0 from "../models/ent/models.js"; /** * GetDefaultKeyBindings 获取默认快捷键配置 */ -export function GetDefaultKeyBindings(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3843471588) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function GetDefaultKeyBindings(): $CancellablePromise { + return $Call.ByID(3843471588).then(($result: any) => { return $$createType1($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * GetKeyBindingByID 根据ID获取快捷键 */ -export function GetKeyBindingByID(id: number): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1578192526, id) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function GetKeyBindingByID(id: number): $CancellablePromise { + return $Call.ByID(1578192526, id).then(($result: any) => { return $$createType3($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * GetKeyBindings 根据类型获取快捷键 */ -export function GetKeyBindings(kbType: models$0.KeyBindingType): Promise<(ent$0.KeyBinding | null)[]> & { cancel(): void } { - let $resultPromise = $Call.ByID(4253885163, kbType) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function GetKeyBindings(kbType: models$0.KeyBindingType): $CancellablePromise<(ent$0.KeyBinding | null)[]> { + return $Call.ByID(4253885163, kbType).then(($result: any) => { return $$createType4($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * ResetKeyBindings 重置所有快捷键到默认值 */ -export function ResetKeyBindings(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(4251626010) as any; - return $resultPromise; -} - -/** - * ServiceStartup 服务启动 - */ -export function ServiceStartup(options: application$0.ServiceOptions): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2057121990, options) as any; - return $resultPromise; +export function ResetKeyBindings(): $CancellablePromise { + return $Call.ByID(4251626010); } /** * SyncKeyBindings 同步快捷键配置 */ -export function SyncKeyBindings(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1522202638) as any; - return $resultPromise; +export function SyncKeyBindings(): $CancellablePromise { + return $Call.ByID(1522202638); } /** * UpdateKeyBindingEnabled 更新快捷键启用状态 */ -export function UpdateKeyBindingEnabled(id: number, enabled: boolean): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(843626124, id, enabled) as any; - return $resultPromise; +export function UpdateKeyBindingEnabled(id: number, enabled: boolean): $CancellablePromise { + return $Call.ByID(843626124, id, enabled); } /** * UpdateKeyBindingKeys 更新快捷键绑定 */ -export function UpdateKeyBindingKeys(id: number, key: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3432755175, id, key) as any; - return $resultPromise; +export function UpdateKeyBindingKeys(id: number, key: string): $CancellablePromise { + return $Call.ByID(3432755175, id, key); } /** * UpdateKeyBindingPreventDefault 更新快捷键 PreventDefault 状态 */ -export function UpdateKeyBindingPreventDefault(id: number, preventDefault: boolean): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(202386744, id, preventDefault) as any; - return $resultPromise; +export function UpdateKeyBindingPreventDefault(id: number, preventDefault: boolean): $CancellablePromise { + return $Call.ByID(202386744, id, preventDefault); } // Private type creation functions diff --git a/frontend/bindings/voidraft/internal/services/migrationservice.ts b/frontend/bindings/voidraft/internal/services/migrationservice.ts index 3c5dd7a1..fa3c126b 100644 --- a/frontend/bindings/voidraft/internal/services/migrationservice.ts +++ b/frontend/bindings/voidraft/internal/services/migrationservice.ts @@ -8,7 +8,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports @@ -17,37 +17,24 @@ import * as $models from "./models.js"; /** * CancelMigration 取消迁移 */ -export function CancelMigration(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1813274502) as any; - return $resultPromise; +export function CancelMigration(): $CancellablePromise { + return $Call.ByID(1813274502); } /** * GetProgress 获取当前进度 */ -export function GetProgress(): Promise<$models.MigrationProgress> & { cancel(): void } { - let $resultPromise = $Call.ByID(3413264131) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function GetProgress(): $CancellablePromise<$models.MigrationProgress> { + return $Call.ByID(3413264131).then(($result: any) => { return $$createType0($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * MigrateDirectory 迁移目录 */ -export function MigrateDirectory(srcPath: string, dstPath: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(311970580, srcPath, dstPath) as any; - return $resultPromise; -} - -/** - * ServiceShutdown 服务关闭 - */ -export function ServiceShutdown(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3472042605) as any; - return $resultPromise; +export function MigrateDirectory(srcPath: string, dstPath: string): $CancellablePromise { + return $Call.ByID(311970580, srcPath, dstPath); } // Private type creation functions diff --git a/frontend/bindings/voidraft/internal/services/models.ts b/frontend/bindings/voidraft/internal/services/models.ts index 85a31f07..3661aa0d 100644 --- a/frontend/bindings/voidraft/internal/services/models.ts +++ b/frontend/bindings/voidraft/internal/services/models.ts @@ -3,7 +3,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Create as $Create} from "@wailsio/runtime"; +import { Create as $Create } from "@wailsio/runtime"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports @@ -18,7 +18,7 @@ import * as time$0 from "../../../time/models.js"; export class HttpRequest { "method": string; "url": string; - "headers": { [_: string]: string }; + "headers": { [_ in string]?: string }; /** * json, formdata, urlencoded, text, params, xml, html, javascript, binary @@ -92,7 +92,7 @@ export class HttpResponse { this["body"] = null; } if (!("headers" in $$source)) { - this["headers"] = ({} as http$0.Header); + this["headers"] = {}; } if (!("timestamp" in $$source)) { this["timestamp"] = null; @@ -336,7 +336,7 @@ export class SystemInfo { "arch": string; "debug": boolean; "osInfo": OSInfo | null; - "platformInfo": { [_: string]: any }; + "platformInfo": { [_ in string]?: any }; /** Creates a new SystemInfo instance. */ constructor($$source: Partial = {}) { @@ -378,7 +378,7 @@ export class SystemInfo { // Private type creation functions const $$createType0 = $Create.Map($Create.Any, $Create.Any); -var $$createType1 = (function $$initCreateType1(...args): any { +var $$createType1 = (function $$initCreateType1(...args: any[]): any { if ($$createType1 === $$initCreateType1) { $$createType1 = $$createType3; } diff --git a/frontend/bindings/voidraft/internal/services/selfupdateservice.ts b/frontend/bindings/voidraft/internal/services/selfupdateservice.ts index c963f38d..9cf8572b 100644 --- a/frontend/bindings/voidraft/internal/services/selfupdateservice.ts +++ b/frontend/bindings/voidraft/internal/services/selfupdateservice.ts @@ -8,7 +8,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports @@ -17,33 +17,26 @@ import * as $models from "./models.js"; /** * ApplyUpdate 应用更新 */ -export function ApplyUpdate(): Promise<$models.SelfUpdateResult | null> & { cancel(): void } { - let $resultPromise = $Call.ByID(2009328394) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function ApplyUpdate(): $CancellablePromise<$models.SelfUpdateResult | null> { + return $Call.ByID(2009328394).then(($result: any) => { return $$createType1($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * CheckForUpdates 检查更新 */ -export function CheckForUpdates(): Promise<$models.SelfUpdateResult | null> & { cancel(): void } { - let $resultPromise = $Call.ByID(438757208) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function CheckForUpdates(): $CancellablePromise<$models.SelfUpdateResult | null> { + return $Call.ByID(438757208).then(($result: any) => { return $$createType1($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * RestartApplication 重启应用程序 */ -export function RestartApplication(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3341481538) as any; - return $resultPromise; +export function RestartApplication(): $CancellablePromise { + return $Call.ByID(3341481538); } // Private type creation functions diff --git a/frontend/bindings/voidraft/internal/services/startupservice.ts b/frontend/bindings/voidraft/internal/services/startupservice.ts index 7156b62a..596756f3 100644 --- a/frontend/bindings/voidraft/internal/services/startupservice.ts +++ b/frontend/bindings/voidraft/internal/services/startupservice.ts @@ -8,12 +8,11 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; /** * SetEnabled 设置开机启动状态 */ -export function SetEnabled(enabled: boolean): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2911601468, enabled) as any; - return $resultPromise; +export function SetEnabled(enabled: boolean): $CancellablePromise { + return $Call.ByID(2911601468, enabled); } diff --git a/frontend/bindings/voidraft/internal/services/systemservice.ts b/frontend/bindings/voidraft/internal/services/systemservice.ts index 7517ee18..530d00f4 100644 --- a/frontend/bindings/voidraft/internal/services/systemservice.ts +++ b/frontend/bindings/voidraft/internal/services/systemservice.ts @@ -8,7 +8,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports @@ -17,41 +17,33 @@ import * as $models from "./models.js"; /** * FormatBytes 格式化字节数为人类可读的格式 */ -export function FormatBytes(bytes: number): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1368998019, bytes) as any; - return $resultPromise; +export function FormatBytes(bytes: number): $CancellablePromise { + return $Call.ByID(1368998019, bytes); } /** * GetMemoryStats 获取当前内存统计信息 */ -export function GetMemoryStats(): Promise<$models.MemoryStats> & { cancel(): void } { - let $resultPromise = $Call.ByID(1678201009) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function GetMemoryStats(): $CancellablePromise<$models.MemoryStats> { + return $Call.ByID(1678201009).then(($result: any) => { return $$createType0($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * GetSystemInfo 获取系统环境信息 */ -export function GetSystemInfo(): Promise<$models.SystemInfo | null> & { cancel(): void } { - let $resultPromise = $Call.ByID(2629436820) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function GetSystemInfo(): $CancellablePromise<$models.SystemInfo | null> { + return $Call.ByID(2629436820).then(($result: any) => { return $$createType2($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * TriggerGC 手动触发垃圾回收 */ -export function TriggerGC(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(741882899) as any; - return $resultPromise; +export function TriggerGC(): $CancellablePromise { + return $Call.ByID(741882899); } // Private type creation functions diff --git a/frontend/bindings/voidraft/internal/services/testservice.ts b/frontend/bindings/voidraft/internal/services/testservice.ts index 6c8ed934..0a40663c 100644 --- a/frontend/bindings/voidraft/internal/services/testservice.ts +++ b/frontend/bindings/voidraft/internal/services/testservice.ts @@ -8,48 +8,32 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js"; +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; /** * ClearAll 清除所有测试状态 */ -export function ClearAll(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2179720854) as any; - return $resultPromise; -} - -/** - * ServiceStartup 服务启动时调用 - */ -export function ServiceStartup(options: application$0.ServiceOptions): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(617408198, options) as any; - return $resultPromise; +export function ClearAll(): $CancellablePromise { + return $Call.ByID(2179720854); } /** * TestBadge 测试Badge功能 */ -export function TestBadge(text: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(4242952145, text) as any; - return $resultPromise; +export function TestBadge(text: string): $CancellablePromise { + return $Call.ByID(4242952145, text); } /** * TestNotification 测试通知功能 */ -export function TestNotification(title: string, subtitle: string, body: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1697553289, title, subtitle, body) as any; - return $resultPromise; +export function TestNotification(title: string, subtitle: string, body: string): $CancellablePromise { + return $Call.ByID(1697553289, title, subtitle, body); } /** * TestUpdateNotification 测试更新通知 */ -export function TestUpdateNotification(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3091730060) as any; - return $resultPromise; +export function TestUpdateNotification(): $CancellablePromise { + return $Call.ByID(3091730060); } diff --git a/frontend/bindings/voidraft/internal/services/themeservice.ts b/frontend/bindings/voidraft/internal/services/themeservice.ts index a8241d22..53f38ef8 100644 --- a/frontend/bindings/voidraft/internal/services/themeservice.ts +++ b/frontend/bindings/voidraft/internal/services/themeservice.ts @@ -8,11 +8,8 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports import * as ent$0 from "../models/ent/models.js"; @@ -20,37 +17,24 @@ import * as ent$0 from "../models/ent/models.js"; /** * GetThemeByName 根据Key获取主题 */ -export function GetThemeByName(name: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1938954770, name) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function GetThemeByName(name: string): $CancellablePromise { + return $Call.ByID(1938954770, name).then(($result: any) => { return $$createType1($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * ResetTheme 删除主题 */ -export function ResetTheme(key: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1806334457, key) as any; - return $resultPromise; -} - -/** - * ServiceStartup 服务启动 - */ -export function ServiceStartup(options: application$0.ServiceOptions): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2915959937, options) as any; - return $resultPromise; +export function ResetTheme(key: string): $CancellablePromise { + return $Call.ByID(1806334457, key); } /** * UpdateTheme 保存或更新主题 */ -export function UpdateTheme(key: string, colors: { [_: string]: any }): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(70189749, key, colors) as any; - return $resultPromise; +export function UpdateTheme(key: string, colors: { [_ in string]?: any }): $CancellablePromise { + return $Call.ByID(70189749, key, colors); } // Private type creation functions diff --git a/frontend/bindings/voidraft/internal/services/translationservice.ts b/frontend/bindings/voidraft/internal/services/translationservice.ts index 04d8c19f..55d116bb 100644 --- a/frontend/bindings/voidraft/internal/services/translationservice.ts +++ b/frontend/bindings/voidraft/internal/services/translationservice.ts @@ -8,7 +8,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports @@ -20,34 +20,27 @@ import * as translator$0 from "../common/translator/models.js"; * @returns {map[string]string} 语言代码到名称的映射 * @returns {error} 可能的错误 */ -export function GetTranslatorLanguages(translatorType: translator$0.TranslatorType): Promise<{ [_: string]: translator$0.LanguageInfo }> & { cancel(): void } { - let $resultPromise = $Call.ByID(3976114458, translatorType) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function GetTranslatorLanguages(translatorType: translator$0.TranslatorType): $CancellablePromise<{ [_ in string]?: translator$0.LanguageInfo }> { + return $Call.ByID(3976114458, translatorType).then(($result: any) => { return $$createType1($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * GetTranslators 获取所有可用翻译器类型 * @returns {[]string} 翻译器类型列表 */ -export function GetTranslators(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3720069432) as any; - let $typingPromise = $resultPromise.then(($result: any) => { +export function GetTranslators(): $CancellablePromise { + return $Call.ByID(3720069432).then(($result: any) => { return $$createType2($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; + }); } /** * IsLanguageSupported 检查指定的语言代码是否受支持 */ -export function IsLanguageSupported(translatorType: translator$0.TranslatorType, languageCode: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2819945417, translatorType, languageCode) as any; - return $resultPromise; +export function IsLanguageSupported(translatorType: translator$0.TranslatorType, languageCode: string): $CancellablePromise { + return $Call.ByID(2819945417, translatorType, languageCode); } /** @@ -59,9 +52,8 @@ export function IsLanguageSupported(translatorType: translator$0.TranslatorType, * @returns {string} 翻译后的文本 * @returns {error} 可能的错误 */ -export function TranslateWith(text: string, $from: string, to: string, translatorType: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3577923623, text, $from, to, translatorType) as any; - return $resultPromise; +export function TranslateWith(text: string, $from: string, to: string, translatorType: string): $CancellablePromise { + return $Call.ByID(3577923623, text, $from, to, translatorType); } // Private type creation functions diff --git a/frontend/bindings/voidraft/internal/services/windowservice.ts b/frontend/bindings/voidraft/internal/services/windowservice.ts index 8e1781ca..88f8ff25 100644 --- a/frontend/bindings/voidraft/internal/services/windowservice.ts +++ b/frontend/bindings/voidraft/internal/services/windowservice.ts @@ -8,40 +8,18 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js"; +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; /** * IsDocumentWindowOpen 检查指定文档的窗口是否已打开 */ -export function IsDocumentWindowOpen(documentID: number): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1735611839, documentID) as any; - return $resultPromise; +export function IsDocumentWindowOpen(documentID: number): $CancellablePromise { + return $Call.ByID(1735611839, documentID); } /** * OpenDocumentWindow 为指定文档ID打开新窗口 */ -export function OpenDocumentWindow(documentID: number): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(494716471, documentID) as any; - return $resultPromise; -} - -/** - * ServiceShutdown 实现服务关闭接口 - */ -export function ServiceShutdown(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(202192783) as any; - return $resultPromise; -} - -/** - * ServiceStartup 服务启动时初始化 - */ -export function ServiceStartup(options: application$0.ServiceOptions): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2432987694, options) as any; - return $resultPromise; +export function OpenDocumentWindow(documentID: number): $CancellablePromise { + return $Call.ByID(494716471, documentID); } diff --git a/frontend/src/stores/translationStore.ts b/frontend/src/stores/translationStore.ts index 4bbcbf72..8bffaed6 100644 --- a/frontend/src/stores/translationStore.ts +++ b/frontend/src/stores/translationStore.ts @@ -42,7 +42,7 @@ export const useTranslationStore = defineStore('translation', () => { try { const languages = await TranslationService.GetTranslatorLanguages(translatorType as any); if (languages) { - translatorLanguages.value[translatorType] = languages; + translatorLanguages.value[translatorType] = languages as Record; } } catch (err) { console.error(`Failed to preload languages for ${translatorType}:`, err); @@ -63,7 +63,7 @@ export const useTranslationStore = defineStore('translation', () => { try { const languages = await TranslationService.GetTranslatorLanguages(translatorType as any); if (languages) { - translatorLanguages.value[translatorType] = languages; + translatorLanguages.value[translatorType] = languages as Record; } } catch (err) { console.error(`Failed to load languages for ${translatorType}:`, err); diff --git a/frontend/src/views/editor/extensions/httpclient/widgets/run-gutter.ts b/frontend/src/views/editor/extensions/httpclient/widgets/run-gutter.ts index 063ca0b6..7ce79c9f 100644 --- a/frontend/src/views/editor/extensions/httpclient/widgets/run-gutter.ts +++ b/frontend/src/views/editor/extensions/httpclient/widgets/run-gutter.ts @@ -173,7 +173,7 @@ class RunButtonMarker extends GutterMarker { time: response.time, requestSize: response.requestSize, body: response.body, - headers: response.headers, + headers: response.headers as { [_: string]: string[] }, timestamp: response.timestamp ? new Date(response.timestamp) : new Date(), error: response.error }; diff --git a/go.mod b/go.mod index 3d3ee6a4..dd83e154 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.37 github.com/stretchr/testify v1.11.1 github.com/wailsapp/wails/v3 v3.0.0-alpha.74 + golang.org/x/mod v0.34.0 golang.org/x/net v0.52.0 golang.org/x/sys v0.42.0 golang.org/x/text v0.35.0 @@ -86,7 +87,6 @@ require ( gitlab.com/gitlab-org/api/client-go v1.46.0 // indirect golang.org/x/crypto v0.49.0 // indirect golang.org/x/image v0.38.0 // indirect - golang.org/x/mod v0.34.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/time v0.15.0 // indirect From 6070cc600461bdbada79b0d9524d207173351036 Mon Sep 17 00:00:00 2001 From: landaiqing Date: Mon, 30 Mar 2026 11:36:17 +0800 Subject: [PATCH 05/26] :art: Optimize sync strategy --- .../wailsapp/wails/v3/internal/eventcreate.ts | 9 + .../wailsapp/wails/v3/internal/eventdata.d.ts | 2 + .../voidraft/internal/models/models.ts | 1412 +++++++++++++++++ .../voidraft/internal/services/index.ts | 49 + .../voidraft/internal/services/models.ts | 75 + .../voidraft/internal/services/syncservice.ts | 87 + frontend/src/i18n/locales/en-US.ts | 15 +- frontend/src/i18n/locales/zh-CN.ts | 11 + frontend/src/stores/syncStore.ts | 46 +- .../src/views/settings/pages/SyncPage.vue | 152 +- internal/services/sync_service.go | 169 +- internal/syncer/app.go | 78 +- internal/syncer/backend/git/backend.go | 213 ++- internal/syncer/backend/git/backend_test.go | 46 + .../syncer/backend/snapshotstore/backend.go | 7 + .../backend/snapshotstore/backend_test.go | 112 ++ internal/syncer/config.go | 35 +- internal/syncer/engine/sync_engine.go | 18 +- internal/syncer/merge/merger.go | 33 +- internal/syncer/merge/updated_at_wins.go | 300 +++- internal/syncer/merge/updated_at_wins_test.go | 190 ++- internal/syncer/resource/helpers.go | 28 +- internal/syncer/scheduler/ticker.go | 45 +- internal/syncer/scheduler/ticker_test.go | 28 + internal/syncer/types.go | 5 +- 25 files changed, 2903 insertions(+), 262 deletions(-) create mode 100644 frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts create mode 100644 frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts create mode 100644 frontend/bindings/voidraft/internal/models/models.ts create mode 100644 frontend/bindings/voidraft/internal/services/index.ts create mode 100644 frontend/bindings/voidraft/internal/services/syncservice.ts create mode 100644 internal/syncer/backend/git/backend_test.go create mode 100644 internal/syncer/scheduler/ticker_test.go diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts new file mode 100644 index 00000000..1ea10585 --- /dev/null +++ b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts @@ -0,0 +1,9 @@ +//@ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Create as $Create } from "@wailsio/runtime"; + +Object.freeze($Create.Events); diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts new file mode 100644 index 00000000..3dd1807b --- /dev/null +++ b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts @@ -0,0 +1,2 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT diff --git a/frontend/bindings/voidraft/internal/models/models.ts b/frontend/bindings/voidraft/internal/models/models.ts new file mode 100644 index 00000000..0ea2216c --- /dev/null +++ b/frontend/bindings/voidraft/internal/models/models.ts @@ -0,0 +1,1412 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Create as $Create } from "@wailsio/runtime"; + +/** + * AppConfig 应用配置 - 按照前端设置页面分类组织 + */ +export class AppConfig { + /** + * 通用设置 + */ + "general": GeneralConfig; + + /** + * 编辑设置 + */ + "editing": EditingConfig; + + /** + * 外观设置 + */ + "appearance": AppearanceConfig; + + /** + * 更新设置 + */ + "updates": UpdatesConfig; + + /** + * 同步设置 + */ + "sync": SyncConfig; + + /** + * 配置元数据 + */ + "metadata": ConfigMetadata; + + /** Creates a new AppConfig instance. */ + constructor($$source: Partial = {}) { + if (!("general" in $$source)) { + this["general"] = (new GeneralConfig()); + } + if (!("editing" in $$source)) { + this["editing"] = (new EditingConfig()); + } + if (!("appearance" in $$source)) { + this["appearance"] = (new AppearanceConfig()); + } + if (!("updates" in $$source)) { + this["updates"] = (new UpdatesConfig()); + } + if (!("sync" in $$source)) { + this["sync"] = (new SyncConfig()); + } + if (!("metadata" in $$source)) { + this["metadata"] = (new ConfigMetadata()); + } + + Object.assign(this, $$source); + } + + /** + * Creates a new AppConfig instance from a string or object. + */ + static createFrom($$source: any = {}): AppConfig { + const $$createField0_0 = $$createType0; + const $$createField1_0 = $$createType1; + const $$createField2_0 = $$createType2; + const $$createField3_0 = $$createType3; + const $$createField4_0 = $$createType4; + const $$createField5_0 = $$createType5; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("general" in $$parsedSource) { + $$parsedSource["general"] = $$createField0_0($$parsedSource["general"]); + } + if ("editing" in $$parsedSource) { + $$parsedSource["editing"] = $$createField1_0($$parsedSource["editing"]); + } + if ("appearance" in $$parsedSource) { + $$parsedSource["appearance"] = $$createField2_0($$parsedSource["appearance"]); + } + if ("updates" in $$parsedSource) { + $$parsedSource["updates"] = $$createField3_0($$parsedSource["updates"]); + } + if ("sync" in $$parsedSource) { + $$parsedSource["sync"] = $$createField4_0($$parsedSource["sync"]); + } + if ("metadata" in $$parsedSource) { + $$parsedSource["metadata"] = $$createField5_0($$parsedSource["metadata"]); + } + return new AppConfig($$parsedSource as Partial); + } +} + +/** + * AppearanceConfig 外观设置配置 + */ +export class AppearanceConfig { + /** + * 界面语言 + */ + "language": LanguageType; + + /** + * 系统界面主题 + */ + "systemTheme": SystemThemeType; + + /** + * 当前选择的预设主题名称 + */ + "currentTheme": string; + + /** Creates a new AppearanceConfig instance. */ + constructor($$source: Partial = {}) { + if (!("language" in $$source)) { + this["language"] = LanguageType.$zero; + } + if (!("systemTheme" in $$source)) { + this["systemTheme"] = SystemThemeType.$zero; + } + if (!("currentTheme" in $$source)) { + this["currentTheme"] = ""; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new AppearanceConfig instance from a string or object. + */ + static createFrom($$source: any = {}): AppearanceConfig { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new AppearanceConfig($$parsedSource as Partial); + } +} + +/** + * Git同步相关类型定义 + * + * AuthMethod 定义Git认证方式 + */ +export enum AuthMethod { + /** + * The Go zero value for the underlying type of the enum. + */ + $zero = "", + + /** + * 认证方式 + */ + Token = "token", + SSHKey = "ssh_key", + UserPass = "user_pass", +}; + +/** + * ConfigMetadata 配置元数据 + */ +export class ConfigMetadata { + /** + * 最后更新时间 + */ + "lastUpdated": string; + + /** + * 配置版本号 + */ + "version": string; + + /** Creates a new ConfigMetadata instance. */ + constructor($$source: Partial = {}) { + if (!("lastUpdated" in $$source)) { + this["lastUpdated"] = ""; + } + if (!("version" in $$source)) { + this["version"] = ""; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new ConfigMetadata instance from a string or object. + */ + static createFrom($$source: any = {}): ConfigMetadata { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new ConfigMetadata($$parsedSource as Partial); + } +} + +/** + * EditingConfig 编辑设置配置 + */ +export class EditingConfig { + /** + * 字体设置 + * 字体大小 + */ + "fontSize": number; + + /** + * 字体族 + */ + "fontFamily": string; + + /** + * 字体粗细 + */ + "fontWeight": string; + + /** + * 行高 + */ + "lineHeight": number; + + /** + * Tab设置 + * 是否启用Tab缩进 + */ + "enableTabIndent": boolean; + + /** + * Tab大小 + */ + "tabSize": number; + + /** + * Tab类型(空格或Tab) + */ + "tabType": TabType; + + /** + * 快捷键模式 + * 快捷键模式(standard 或 emacs) + */ + "keymapMode": KeyBindingType; + + /** + * 保存选项 + * 自动保存延迟(毫秒) + */ + "autoSaveDelay": number; + + /** Creates a new EditingConfig instance. */ + constructor($$source: Partial = {}) { + if (!("fontSize" in $$source)) { + this["fontSize"] = 0; + } + if (!("fontFamily" in $$source)) { + this["fontFamily"] = ""; + } + if (!("fontWeight" in $$source)) { + this["fontWeight"] = ""; + } + if (!("lineHeight" in $$source)) { + this["lineHeight"] = 0; + } + if (!("enableTabIndent" in $$source)) { + this["enableTabIndent"] = false; + } + if (!("tabSize" in $$source)) { + this["tabSize"] = 0; + } + if (!("tabType" in $$source)) { + this["tabType"] = TabType.$zero; + } + if (!("keymapMode" in $$source)) { + this["keymapMode"] = KeyBindingType.$zero; + } + if (!("autoSaveDelay" in $$source)) { + this["autoSaveDelay"] = 0; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new EditingConfig instance from a string or object. + */ + static createFrom($$source: any = {}): EditingConfig { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new EditingConfig($$parsedSource as Partial); + } +} + +/** + * Extension 扩展配置 + */ +export class Extension { + "key": ExtensionName; + "enabled": boolean; + "config": ExtensionConfig; + + /** Creates a new Extension instance. */ + constructor($$source: Partial = {}) { + if (!("key" in $$source)) { + this["key"] = ExtensionName.$zero; + } + if (!("enabled" in $$source)) { + this["enabled"] = false; + } + if (!("config" in $$source)) { + this["config"] = {}; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new Extension instance from a string or object. + */ + static createFrom($$source: any = {}): Extension { + const $$createField2_0 = $$createType6; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("config" in $$parsedSource) { + $$parsedSource["config"] = $$createField2_0($$parsedSource["config"]); + } + return new Extension($$parsedSource as Partial); + } +} + +/** + * ExtensionConfig 扩展配置项 + */ +export type ExtensionConfig = { [_ in string]?: any }; + +/** + * ExtensionName 扩展标识符 + */ +export enum ExtensionName { + /** + * The Go zero value for the underlying type of the enum. + */ + $zero = "", + + /** + * 彩虹括号 + */ + RainbowBrackets = "rainbowBrackets", + + /** + * 超链接 + */ + Hyperlink = "hyperlink", + + /** + * 颜色选择器 + */ + ColorSelector = "colorSelector", + + /** + * 代码折叠 + */ + Fold = "fold", + + /** + * 划词翻译 + */ + Translator = "translator", + + /** + * Markdown渲染 + */ + Markdown = "markdown", + + /** + * 显示空白字符 + */ + HighlightWhitespace = "highlightWhitespace", + + /** + * 高亮行尾空白 + */ + HighlightTrailingWhitespace = "highlightTrailingWhitespace", + + /** + * 小地图 + */ + Minimap = "minimap", + + /** + * 行号显示 + */ + LineNumbers = "lineNumbers", + + /** + * 上下文菜单 + */ + ContextMenu = "contextMenu", + + /** + * 搜索功能 + */ + Search = "search", + + /** + * HTTP 客户端 + */ + HttpClient = "httpClient", + + /** + * 代码块导出图片 + */ + BlockImage = "blockImage", +}; + +/** + * GeneralConfig 通用设置配置 + */ +export class GeneralConfig { + /** + * 窗口是否置顶 + */ + "alwaysOnTop": boolean; + + /** + * 数据存储路径 + */ + "dataPath": string; + + /** + * 是否启用系统托盘 + */ + "enableSystemTray": boolean; + + /** + * 开机启动设置 + */ + "startAtLogin": boolean; + + /** + * 窗口吸附设置 + * 是否启用窗口吸附功能(阈值现在是自适应的) + */ + "enableWindowSnap": boolean; + + /** + * 全局热键设置 + * 是否启用全局热键 + */ + "enableGlobalHotkey": boolean; + + /** + * 全局热键组合 + */ + "globalHotkey": HotkeyCombo; + + /** + * 界面设置 + * 是否启用加载动画 + */ + "enableLoadingAnimation": boolean; + + /** + * 是否启用标签页模式 + */ + "enableTabs": boolean; + + /** + * 是否启用内存监视器 + */ + "enableMemoryMonitor": boolean; + + /** Creates a new GeneralConfig instance. */ + constructor($$source: Partial = {}) { + if (!("alwaysOnTop" in $$source)) { + this["alwaysOnTop"] = false; + } + if (!("dataPath" in $$source)) { + this["dataPath"] = ""; + } + if (!("enableSystemTray" in $$source)) { + this["enableSystemTray"] = false; + } + if (!("startAtLogin" in $$source)) { + this["startAtLogin"] = false; + } + if (!("enableWindowSnap" in $$source)) { + this["enableWindowSnap"] = false; + } + if (!("enableGlobalHotkey" in $$source)) { + this["enableGlobalHotkey"] = false; + } + if (!("globalHotkey" in $$source)) { + this["globalHotkey"] = (new HotkeyCombo()); + } + if (!("enableLoadingAnimation" in $$source)) { + this["enableLoadingAnimation"] = false; + } + if (!("enableTabs" in $$source)) { + this["enableTabs"] = false; + } + if (!("enableMemoryMonitor" in $$source)) { + this["enableMemoryMonitor"] = false; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new GeneralConfig instance from a string or object. + */ + static createFrom($$source: any = {}): GeneralConfig { + const $$createField6_0 = $$createType8; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("globalHotkey" in $$parsedSource) { + $$parsedSource["globalHotkey"] = $$createField6_0($$parsedSource["globalHotkey"]); + } + return new GeneralConfig($$parsedSource as Partial); + } +} + +/** + * GitSyncConfig 描述 Git 同步配置。 + */ +export class GitSyncConfig { + "enabled": boolean; + "auto_sync": boolean; + + /** + * 分钟 + */ + "sync_interval": number; + "repo_url": string; + "auth_method": AuthMethod; + "username"?: string; + "password"?: string; + "token"?: string; + "ssh_key_path"?: string; + "ssh_key_passphrase"?: string; + + /** Creates a new GitSyncConfig instance. */ + constructor($$source: Partial = {}) { + if (!("enabled" in $$source)) { + this["enabled"] = false; + } + if (!("auto_sync" in $$source)) { + this["auto_sync"] = false; + } + if (!("sync_interval" in $$source)) { + this["sync_interval"] = 0; + } + if (!("repo_url" in $$source)) { + this["repo_url"] = ""; + } + if (!("auth_method" in $$source)) { + this["auth_method"] = AuthMethod.$zero; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new GitSyncConfig instance from a string or object. + */ + static createFrom($$source: any = {}): GitSyncConfig { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new GitSyncConfig($$parsedSource as Partial); + } +} + +/** + * GithubConfig GitHub配置 + */ +export class GithubConfig { + /** + * 仓库所有者 + */ + "owner": string; + + /** + * 仓库名称 + */ + "repo": string; + + /** Creates a new GithubConfig instance. */ + constructor($$source: Partial = {}) { + if (!("owner" in $$source)) { + this["owner"] = ""; + } + if (!("repo" in $$source)) { + this["repo"] = ""; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new GithubConfig instance from a string or object. + */ + static createFrom($$source: any = {}): GithubConfig { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new GithubConfig($$parsedSource as Partial); + } +} + +/** + * HotkeyCombo 热键组合定义 + */ +export class HotkeyCombo { + /** + * Ctrl键 + */ + "ctrl": boolean; + + /** + * Shift键 + */ + "shift": boolean; + + /** + * Alt键 + */ + "alt": boolean; + + /** + * Win键 + */ + "win": boolean; + + /** + * 主键(如 'X', 'F1' 等) + */ + "key": string; + + /** Creates a new HotkeyCombo instance. */ + constructor($$source: Partial = {}) { + if (!("ctrl" in $$source)) { + this["ctrl"] = false; + } + if (!("shift" in $$source)) { + this["shift"] = false; + } + if (!("alt" in $$source)) { + this["alt"] = false; + } + if (!("win" in $$source)) { + this["win"] = false; + } + if (!("key" in $$source)) { + this["key"] = ""; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new HotkeyCombo instance from a string or object. + */ + static createFrom($$source: any = {}): HotkeyCombo { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new HotkeyCombo($$parsedSource as Partial); + } +} + +/** + * KeyBinding 单个快捷键绑定 + */ +export class KeyBinding { + /** + * 命令唯一标识符 + */ + "name": KeyBindingName; + + /** + * 快捷键类型(standard 或 "emacs") + */ + "type": KeyBindingType; + + /** + * 通用快捷键(跨平台) + */ + "key"?: string; + + /** + * macOS 专用快捷键 + */ + "macos"?: string; + + /** + * windows 专用快捷键 + */ + "win"?: string; + + /** + * Linux 专用快捷键 + */ + "linux"?: string; + + /** + * 所属扩展 + */ + "extension": ExtensionName; + + /** + * 是否启用 + */ + "enabled": boolean; + + /** + * 阻止浏览器默认行为 + */ + "preventDefault": boolean; + + /** + * 作用域(默认 "editor") + */ + "scope"?: string; + + /** Creates a new KeyBinding instance. */ + constructor($$source: Partial = {}) { + if (!("name" in $$source)) { + this["name"] = KeyBindingName.$zero; + } + if (!("type" in $$source)) { + this["type"] = KeyBindingType.$zero; + } + if (!("extension" in $$source)) { + this["extension"] = ExtensionName.$zero; + } + if (!("enabled" in $$source)) { + this["enabled"] = false; + } + if (!("preventDefault" in $$source)) { + this["preventDefault"] = false; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new KeyBinding instance from a string or object. + */ + static createFrom($$source: any = {}): KeyBinding { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new KeyBinding($$parsedSource as Partial); + } +} + +/** + * KeyBindingName 快捷键命令标识符 + */ +export enum KeyBindingName { + /** + * The Go zero value for the underlying type of the enum. + */ + $zero = "", + + /** + * 显示搜索 + */ + ShowSearch = "showSearch", + + /** + * 隐藏搜索 + */ + HideSearch = "hideSearch", + + /** + * 块内选择全部 + */ + BlockSelectAll = "blockSelectAll", + + /** + * 在当前块后添加新块 + */ + BlockAddAfterCurrent = "blockAddAfterCurrent", + + /** + * 在最后添加新块 + */ + BlockAddAfterLast = "blockAddAfterLast", + + /** + * 在当前块前添加新块 + */ + BlockAddBeforeCurrent = "blockAddBeforeCurrent", + + /** + * 跳转到上一个块 + */ + BlockGotoPrevious = "blockGotoPrevious", + + /** + * 跳转到下一个块 + */ + BlockGotoNext = "blockGotoNext", + + /** + * 选择上一个块 + */ + BlockSelectPrevious = "blockSelectPrevious", + + /** + * 选择下一个块 + */ + BlockSelectNext = "blockSelectNext", + + /** + * 删除当前块 + */ + BlockDelete = "blockDelete", + + /** + * 向上移动当前块 + */ + BlockMoveUp = "blockMoveUp", + + /** + * 向下移动当前块 + */ + BlockMoveDown = "blockMoveDown", + + /** + * 删除行 + */ + BlockDeleteLine = "blockDeleteLine", + + /** + * 向上移动行 + */ + BlockMoveLineUp = "blockMoveLineUp", + + /** + * 向下移动行 + */ + BlockMoveLineDown = "blockMoveLineDown", + + /** + * 字符转置 + */ + BlockTransposeChars = "blockTransposeChars", + + /** + * 格式化代码块 + */ + BlockFormat = "blockFormat", + + /** + * 复制 + */ + BlockCopy = "blockCopy", + + /** + * 剪切 + */ + BlockCut = "blockCut", + + /** + * 粘贴 + */ + BlockPaste = "blockPaste", + + /** + * 折叠代码 + */ + FoldCode = "foldCode", + + /** + * 展开代码 + */ + UnfoldCode = "unfoldCode", + + /** + * 折叠全部 + */ + FoldAll = "foldAll", + + /** + * 展开全部 + */ + UnfoldAll = "unfoldAll", + + /** + * 光标按语法左移 + */ + CursorSyntaxLeft = "cursorSyntaxLeft", + + /** + * 光标按语法右移 + */ + CursorSyntaxRight = "cursorSyntaxRight", + + /** + * 按语法选择左侧 + */ + SelectSyntaxLeft = "selectSyntaxLeft", + + /** + * 按语法选择右侧 + */ + SelectSyntaxRight = "selectSyntaxRight", + + /** + * 向上复制行 + */ + CopyLineUp = "copyLineUp", + + /** + * 向下复制行 + */ + CopyLineDown = "copyLineDown", + + /** + * 插入空行 + */ + InsertBlankLine = "insertBlankLine", + + /** + * 选择行 + */ + SelectLine = "selectLine", + + /** + * 选择父级语法 + */ + SelectParentSyntax = "selectParentSyntax", + + /** + * 简化选择 + */ + SimplifySelection = "simplifySelection", + + /** + * 在上方添加光标 + */ + AddCursorAbove = "addCursorAbove", + + /** + * 在下方添加光标 + */ + AddCursorBelow = "addCursorBelow", + + /** + * 光标按单词左移 + */ + CursorGroupLeft = "cursorGroupLeft", + + /** + * 光标按单词右移 + */ + CursorGroupRight = "cursorGroupRight", + + /** + * 按单词选择左侧 + */ + SelectGroupLeft = "selectGroupLeft", + + /** + * 按单词选择右侧 + */ + SelectGroupRight = "selectGroupRight", + + /** + * 删除到行尾 + */ + DeleteToLineEnd = "deleteToLineEnd", + + /** + * 删除到行首 + */ + DeleteToLineStart = "deleteToLineStart", + + /** + * 移动到行首 + */ + CursorLineStart = "cursorLineStart", + + /** + * 移动到行尾 + */ + CursorLineEnd = "cursorLineEnd", + + /** + * 选择到行首 + */ + SelectLineStart = "selectLineStart", + + /** + * 选择到行尾 + */ + SelectLineEnd = "selectLineEnd", + + /** + * 跳转到文档开头 + */ + CursorDocStart = "cursorDocStart", + + /** + * 跳转到文档结尾 + */ + CursorDocEnd = "cursorDocEnd", + + /** + * 选择到文档开头 + */ + SelectDocStart = "selectDocStart", + + /** + * 选择到文档结尾 + */ + SelectDocEnd = "selectDocEnd", + + /** + * 选择到匹配括号 + */ + SelectMatchingBracket = "selectMatchingBracket", + + /** + * 分割行 + */ + SplitLine = "splitLine", + + /** + * 光标左移一个字符 + */ + CursorCharLeft = "cursorCharLeft", + + /** + * 光标右移一个字符 + */ + CursorCharRight = "cursorCharRight", + + /** + * 光标上移一行 + */ + CursorLineUp = "cursorLineUp", + + /** + * 光标下移一行 + */ + CursorLineDown = "cursorLineDown", + + /** + * 向上翻页 + */ + CursorPageUp = "cursorPageUp", + + /** + * 向下翻页 + */ + CursorPageDown = "cursorPageDown", + + /** + * 选择左移一个字符 + */ + SelectCharLeft = "selectCharLeft", + + /** + * 选择右移一个字符 + */ + SelectCharRight = "selectCharRight", + + /** + * 选择上移一行 + */ + SelectLineUp = "selectLineUp", + + /** + * 选择下移一行 + */ + SelectLineDown = "selectLineDown", + + /** + * 减少缩进 + */ + IndentLess = "indentLess", + + /** + * 增加缩进 + */ + IndentMore = "indentMore", + + /** + * 缩进选择 + */ + IndentSelection = "indentSelection", + + /** + * 光标到匹配括号 + */ + CursorMatchingBracket = "cursorMatchingBracket", + + /** + * 切换注释 + */ + ToggleComment = "toggleComment", + + /** + * 切换块注释 + */ + ToggleBlockComment = "toggleBlockComment", + + /** + * 插入新行并缩进 + */ + InsertNewlineAndIndent = "insertNewlineAndIndent", + + /** + * 向后删除字符 + */ + DeleteCharBackward = "deleteCharBackward", + + /** + * 向前删除字符 + */ + DeleteCharForward = "deleteCharForward", + + /** + * 向后删除组 + */ + DeleteGroupBackward = "deleteGroupBackward", + + /** + * 向前删除组 + */ + DeleteGroupForward = "deleteGroupForward", + + /** + * 撤销 + */ + HistoryUndo = "historyUndo", + + /** + * 重做 + */ + HistoryRedo = "historyRedo", + + /** + * 撤销选择 + */ + HistoryUndoSelection = "historyUndoSelection", + + /** + * 重做选择 + */ + HistoryRedoSelection = "historyRedoSelection", + + /** + * 复制块为图片 + */ + CopyBlockImage = "copyBlockImage", +}; + +export enum KeyBindingType { + /** + * The Go zero value for the underlying type of the enum. + */ + $zero = "", + + /** + * standard 标准快捷键 + */ + Standard = "standard", + + /** + * emacs 快捷键 + */ + Emacs = "emacs", +}; + +/** + * LanguageType 语言类型定义 + */ +export enum LanguageType { + /** + * The Go zero value for the underlying type of the enum. + */ + $zero = "", + + /** + * LangZhCN 中文简体 + */ + LangZhCN = "zh-CN", + + /** + * LangEnUS 英文-美国 + */ + LangEnUS = "en-US", +}; + +/** + * LocalFSSyncConfig 描述本地文件系统同步配置。 + */ +export class LocalFSSyncConfig { + "enabled": boolean; + "auto_sync": boolean; + + /** + * 分钟 + */ + "sync_interval": number; + "root_path": string; + + /** Creates a new LocalFSSyncConfig instance. */ + constructor($$source: Partial = {}) { + if (!("enabled" in $$source)) { + this["enabled"] = false; + } + if (!("auto_sync" in $$source)) { + this["auto_sync"] = false; + } + if (!("sync_interval" in $$source)) { + this["sync_interval"] = 0; + } + if (!("root_path" in $$source)) { + this["root_path"] = ""; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new LocalFSSyncConfig instance from a string or object. + */ + static createFrom($$source: any = {}): LocalFSSyncConfig { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new LocalFSSyncConfig($$parsedSource as Partial); + } +} + +/** + * SyncConfig 描述同步模块配置。 + */ +export class SyncConfig { + "target": SyncTarget; + "git": GitSyncConfig; + "localfs": LocalFSSyncConfig; + + /** Creates a new SyncConfig instance. */ + constructor($$source: Partial = {}) { + if (!("target" in $$source)) { + this["target"] = SyncTarget.$zero; + } + if (!("git" in $$source)) { + this["git"] = (new GitSyncConfig()); + } + if (!("localfs" in $$source)) { + this["localfs"] = (new LocalFSSyncConfig()); + } + + Object.assign(this, $$source); + } + + /** + * Creates a new SyncConfig instance from a string or object. + */ + static createFrom($$source: any = {}): SyncConfig { + const $$createField1_0 = $$createType9; + const $$createField2_0 = $$createType10; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("git" in $$parsedSource) { + $$parsedSource["git"] = $$createField1_0($$parsedSource["git"]); + } + if ("localfs" in $$parsedSource) { + $$parsedSource["localfs"] = $$createField2_0($$parsedSource["localfs"]); + } + return new SyncConfig($$parsedSource as Partial); + } +} + +/** + * SyncTarget 定义当前可选择的同步目标。 + */ +export enum SyncTarget { + /** + * The Go zero value for the underlying type of the enum. + */ + $zero = "", + + /** + * SyncTargetGit 表示 Git 同步。 + */ + SyncTargetGit = "git", + + /** + * SyncTargetLocalFS 表示本地文件系统同步。 + */ + SyncTargetLocalFS = "localfs", +}; + +/** + * SystemThemeType 系统主题类型定义 + */ +export enum SystemThemeType { + /** + * The Go zero value for the underlying type of the enum. + */ + $zero = "", + + /** + * SystemThemeDark 深色系统主题 + */ + SystemThemeDark = "dark", + + /** + * SystemThemeLight 浅色系统主题 + */ + SystemThemeLight = "light", + + /** + * SystemThemeAuto 跟随系统主题 + */ + SystemThemeAuto = "auto", +}; + +/** + * TabType 定义了制表符类型 + */ +export enum TabType { + /** + * The Go zero value for the underlying type of the enum. + */ + $zero = "", + + /** + * TabTypeSpaces 使用空格作为制表符 + */ + TabTypeSpaces = "spaces", + + /** + * TabTypeTab 使用Tab作为制表符 + */ + TabTypeTab = "tab", +}; + +/** + * UpdatesConfig 更新设置配置 + */ +export class UpdatesConfig { + /** + * 当前版本号 + */ + "version": string; + + /** + * 是否自动更新 + */ + "autoUpdate": boolean; + + /** + * 更新前是否备份 + */ + "backupBeforeUpdate": boolean; + + /** + * 更新超时时间(秒) + */ + "updateTimeout": number; + + /** + * GitHub配置 + */ + "github": GithubConfig; + + /** Creates a new UpdatesConfig instance. */ + constructor($$source: Partial = {}) { + if (!("version" in $$source)) { + this["version"] = ""; + } + if (!("autoUpdate" in $$source)) { + this["autoUpdate"] = false; + } + if (!("backupBeforeUpdate" in $$source)) { + this["backupBeforeUpdate"] = false; + } + if (!("updateTimeout" in $$source)) { + this["updateTimeout"] = 0; + } + if (!("github" in $$source)) { + this["github"] = (new GithubConfig()); + } + + Object.assign(this, $$source); + } + + /** + * Creates a new UpdatesConfig instance from a string or object. + */ + static createFrom($$source: any = {}): UpdatesConfig { + const $$createField4_0 = $$createType11; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("github" in $$parsedSource) { + $$parsedSource["github"] = $$createField4_0($$parsedSource["github"]); + } + return new UpdatesConfig($$parsedSource as Partial); + } +} + +// Private type creation functions +const $$createType0 = GeneralConfig.createFrom; +const $$createType1 = EditingConfig.createFrom; +const $$createType2 = AppearanceConfig.createFrom; +const $$createType3 = UpdatesConfig.createFrom; +const $$createType4 = SyncConfig.createFrom; +const $$createType5 = ConfigMetadata.createFrom; +var $$createType6 = (function $$initCreateType6(...args: any[]): any { + if ($$createType6 === $$initCreateType6) { + $$createType6 = $$createType7; + } + return $$createType6(...args); +}); +const $$createType7 = $Create.Map($Create.Any, $Create.Any); +const $$createType8 = HotkeyCombo.createFrom; +const $$createType9 = GitSyncConfig.createFrom; +const $$createType10 = LocalFSSyncConfig.createFrom; +const $$createType11 = GithubConfig.createFrom; diff --git a/frontend/bindings/voidraft/internal/services/index.ts b/frontend/bindings/voidraft/internal/services/index.ts new file mode 100644 index 00000000..1e2b6498 --- /dev/null +++ b/frontend/bindings/voidraft/internal/services/index.ts @@ -0,0 +1,49 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +import * as ConfigService from "./configservice.js"; +import * as DialogService from "./dialogservice.js"; +import * as DocumentService from "./documentservice.js"; +import * as ExtensionService from "./extensionservice.js"; +import * as HotkeyService from "./hotkeyservice.js"; +import * as HttpClientService from "./httpclientservice.js"; +import * as KeyBindingService from "./keybindingservice.js"; +import * as MigrationService from "./migrationservice.js"; +import * as SelfUpdateService from "./selfupdateservice.js"; +import * as StartupService from "./startupservice.js"; +import * as SyncService from "./syncservice.js"; +import * as SystemService from "./systemservice.js"; +import * as TestService from "./testservice.js"; +import * as ThemeService from "./themeservice.js"; +import * as TranslationService from "./translationservice.js"; +import * as WindowService from "./windowservice.js"; +export { + ConfigService, + DialogService, + DocumentService, + ExtensionService, + HotkeyService, + HttpClientService, + KeyBindingService, + MigrationService, + SelfUpdateService, + StartupService, + SyncService, + SystemService, + TestService, + ThemeService, + TranslationService, + WindowService +}; + +export { + HttpRequest, + HttpResponse, + MemoryStats, + MigrationProgress, + OSInfo, + SelfUpdateResult, + SyncConnectionResult, + SyncStatus, + SystemInfo +} from "./models.js"; diff --git a/frontend/bindings/voidraft/internal/services/models.ts b/frontend/bindings/voidraft/internal/services/models.ts index 3661aa0d..01bfb04f 100644 --- a/frontend/bindings/voidraft/internal/services/models.ts +++ b/frontend/bindings/voidraft/internal/services/models.ts @@ -328,6 +328,81 @@ export class SelfUpdateResult { } } +/** + * SyncConnectionResult describes a connection test result. + */ +export class SyncConnectionResult { + "target_id": string; + "resolved_branch"?: string; + + /** Creates a new SyncConnectionResult instance. */ + constructor($$source: Partial = {}) { + if (!("target_id" in $$source)) { + this["target_id"] = ""; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new SyncConnectionResult instance from a string or object. + */ + static createFrom($$source: any = {}): SyncConnectionResult { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new SyncConnectionResult($$parsedSource as Partial); + } +} + +/** + * SyncStatus describes the latest manual sync state. + */ +export class SyncStatus { + "target_id"?: string; + "last_sync_at"?: string; + "last_success_at"?: string; + "last_error"?: string; + "local_changed": boolean; + "remote_changed": boolean; + "applied_to_local": boolean; + "published": boolean; + "conflict_count": number; + "conflict_ids"?: string[]; + "revision"?: string; + + /** Creates a new SyncStatus instance. */ + constructor($$source: Partial = {}) { + if (!("local_changed" in $$source)) { + this["local_changed"] = false; + } + if (!("remote_changed" in $$source)) { + this["remote_changed"] = false; + } + if (!("applied_to_local" in $$source)) { + this["applied_to_local"] = false; + } + if (!("published" in $$source)) { + this["published"] = false; + } + if (!("conflict_count" in $$source)) { + this["conflict_count"] = 0; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new SyncStatus instance from a string or object. + */ + static createFrom($$source: any = {}): SyncStatus { + const $$createField9_0 = $$createType2; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("conflict_ids" in $$parsedSource) { + $$parsedSource["conflict_ids"] = $$createField9_0($$parsedSource["conflict_ids"]); + } + return new SyncStatus($$parsedSource as Partial); + } +} + /** * SystemInfo 系统信息 */ diff --git a/frontend/bindings/voidraft/internal/services/syncservice.ts b/frontend/bindings/voidraft/internal/services/syncservice.ts new file mode 100644 index 00000000..6092e592 --- /dev/null +++ b/frontend/bindings/voidraft/internal/services/syncservice.ts @@ -0,0 +1,87 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * SyncService exposes app-layer sync operations. + * @module + */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as models$0 from "../models/models.js"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as $models from "./models.js"; + +/** + * GetStatus returns the latest manual sync status. + */ +export function GetStatus(): $CancellablePromise<$models.SyncStatus | null> { + return $Call.ByID(2881223453).then(($result: any) => { + return $$createType1($result); + }); +} + +/** + * HandleConfigChange re-applies sync config changes. + */ +export function HandleConfigChange(config: models$0.SyncConfig | null): $CancellablePromise { + return $Call.ByID(1326966557, config); +} + +/** + * Initialize reloads config and restarts auto-sync. + */ +export function Initialize(): $CancellablePromise { + return $Call.ByID(1986210239); +} + +/** + * Reinitialize is an alias used by config watchers. + */ +export function Reinitialize(): $CancellablePromise { + return $Call.ByID(1904005378); +} + +/** + * StartAutoSync starts auto-sync scheduling. + */ +export function StartAutoSync(): $CancellablePromise { + return $Call.ByID(90694923); +} + +/** + * StopAutoSync stops auto-sync scheduling. + */ +export function StopAutoSync(): $CancellablePromise { + return $Call.ByID(972414697); +} + +/** + * Sync runs one manual sync and returns the latest status. + */ +export function Sync(): $CancellablePromise<$models.SyncStatus | null> { + return $Call.ByID(3361086502).then(($result: any) => { + return $$createType1($result); + }); +} + +/** + * TestConnection validates the selected target configuration immediately. + */ +export function TestConnection(): $CancellablePromise<$models.SyncConnectionResult | null> { + return $Call.ByID(389881917).then(($result: any) => { + return $$createType3($result); + }); +} + +// Private type creation functions +const $$createType0 = $models.SyncStatus.createFrom; +const $$createType1 = $Create.Nullable($$createType0); +const $$createType2 = $models.SyncConnectionResult.createFrom; +const $$createType3 = $Create.Nullable($$createType2); diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 77f60ed8..90b7f72e 100644 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -298,11 +298,22 @@ export default { sshKeyPassphrase: 'SSH Key Passphrase', sshKeyPassphrasePlaceholder: 'Enter SSH key passphrase', syncOperations: 'Sync Operations', - syncToRemote: 'Sync to Remote', - syncToTarget: 'Sync to Target', + syncToRemote: 'Sync to Remote', + syncToTarget: 'Sync to Target', + testingConnection: 'Testing...', syncing: 'Syncing...', syncSuccess: 'Sync completed', + syncSuccessWithConflicts: 'Sync completed with {count} conflicts', + testConnectionSuccess: 'Connection test passed', + testConnectionSuccessWithBranch: 'Connection test passed, branch: {branch}', + lastSync: 'Last Sync', + lastSuccess: 'Last Success', + lastError: 'Last Error', + revision: 'Revision', + conflicts: 'Conflicts', + conflictIds: 'Conflict IDs', actions: { + testConnection: 'Test Connection', sync: 'Sync', } }, diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 6b60d81d..da86a60c 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -302,9 +302,20 @@ export default { syncOperations: '同步操作', syncToRemote: '同步到远程', syncToTarget: '同步到目标', + testingConnection: '测试中...', syncing: '同步中...', syncSuccess: '同步成功', + syncSuccessWithConflicts: '同步完成,存在 {count} 个冲突', + testConnectionSuccess: '连接测试成功', + testConnectionSuccessWithBranch: '连接测试成功,分支:{branch}', + lastSync: '最近同步', + lastSuccess: '最近成功', + lastError: '最近错误', + revision: '版本', + conflicts: '冲突数', + conflictIds: '冲突对象 ID', actions: { + testConnection: '立即测试连接', sync: '同步', } }, diff --git a/frontend/src/stores/syncStore.ts b/frontend/src/stores/syncStore.ts index 8cbe4859..fb6dff7f 100644 --- a/frontend/src/stores/syncStore.ts +++ b/frontend/src/stores/syncStore.ts @@ -1,25 +1,57 @@ -import { defineStore } from 'pinia'; -import { ref } from 'vue'; -import { SyncService } from '@/../bindings/voidraft/internal/services'; +import {defineStore} from 'pinia'; +import {ref} from 'vue'; +import {SyncService} from '@/../bindings/voidraft/internal/services'; +import {SyncConnectionResult, SyncStatus} from '@/../bindings/voidraft/internal/services/models'; export const useSyncStore = defineStore('sync', () => { const isSyncing = ref(false); + const isTestingConnection = ref(false); + const status = ref(null); - const sync = async (): Promise => { + /** Loads the latest sync status. */ + const loadStatus = async (): Promise => { + status.value = await SyncService.GetStatus(); + return status.value; + }; + + /** Runs one sync and caches the latest status. */ + const sync = async (): Promise => { if (isSyncing.value) { - return; + return status.value; } isSyncing.value = true; try { - await SyncService.Sync(); + status.value = await SyncService.Sync(); + return status.value; + } catch (error) { + await loadStatus(); + throw error; } finally { isSyncing.value = false; } }; + /** Verifies the selected target config immediately. */ + const testConnection = async (): Promise => { + if (isTestingConnection.value) { + return null; + } + + isTestingConnection.value = true; + try { + return await SyncService.TestConnection(); + } finally { + isTestingConnection.value = false; + } + }; + return { isSyncing, - sync + isTestingConnection, + status, + loadStatus, + sync, + testConnection }; }); diff --git a/frontend/src/views/settings/pages/SyncPage.vue b/frontend/src/views/settings/pages/SyncPage.vue index b777537b..686ba47c 100644 --- a/frontend/src/views/settings/pages/SyncPage.vue +++ b/frontend/src/views/settings/pages/SyncPage.vue @@ -1,14 +1,14 @@ @@ -157,8 +184,8 @@ const handleSync = async () => { - - - + +
+ This panel imports a real image through MediaHTTPService.ImportImage and renders the returned /media/... URL with a normal <img>. The backend now uses a stable asset_id index and date-based storage folders. +
+ + + + + + +
+ {{ selectedFile ? (selectedFile.type || 'unknown mime') : 'Pick a local image file' }} +
+
+ + +
+ + + +
+
+ +
+ {{ mediaStatus.message }} +
+ +
+
+
+ Imported Images + {{ mediaItems.length }} +
+ +
+ No imported images yet. +
+ +
+ +
+
+ +
+
+ Preview + {{ selectedImage ? selectedImage.mime_type : 'none' }} +
+ +
+ + +
+
+ Asset ID + {{ selectedImage.id }} +
+
+ URL + {{ selectedImage.url }} +
+
+ Stored Name + {{ selectedImage.filename }} +
+
+ Relative Path + {{ selectedImage.relative_path }} +
+
+ Original Name + {{ selectedImage.original_filename || '-' }} +
+
+ Size + {{ formatFileSize(selectedImage.size) }} +
+
+ Dimensions + {{ selectedImage.width }} x {{ selectedImage.height }} +
+
+ SHA256 + {{ selectedImage.sha256 }} +
+
+
+ +
+ Select one imported image to preview. +
+
+
+
+ - @@ -189,89 +182,61 @@ {{ mediaStatus.message }} -
-
-
- Imported Images - {{ mediaItems.length }} -
- -
- No imported images yet. -
- -
- -
-
- -
-
- Preview - {{ selectedImage ? selectedImage.mime_type : 'none' }} -
+
+
+ Current Preview + {{ selectedImage ? selectedImage.mime_type : 'none' }} +
-
- - -
-
- Asset ID - {{ selectedImage.id }} -
-
- URL - {{ selectedImage.url }} -
-
- Stored Name - {{ selectedImage.filename }} -
-
- Relative Path - {{ selectedImage.relative_path }} -
-
- Original Name - {{ selectedImage.original_filename || '-' }} -
-
- Size - {{ formatFileSize(selectedImage.size) }} -
-
- Dimensions - {{ selectedImage.width }} x {{ selectedImage.height }} -
-
- SHA256 - {{ selectedImage.sha256 }} -
+
+ + +
+
+ Asset ID + {{ selectedImage.id }} +
+
+ URL + {{ selectedImage.url }} +
+
+ Stored Name + {{ selectedImage.filename }} +
+
+ Relative Path + {{ selectedImage.relative_path }} +
+
+ Original Name + {{ selectedImage.original_filename || '-' }} +
+
+ Size + {{ formatFileSize(selectedImage.size) }} +
+
+ Dimensions + {{ selectedImage.width }} x {{ selectedImage.height }} +
+
+ SHA256 + {{ selectedImage.sha256 }}
+
-
- Select one imported image to preview. -
-
-
+
+ Import one image to preview it here. +
+
@@ -288,7 +253,7 @@ From cdd1fee10192dd281dfac918f3385e0bb01e451b Mon Sep 17 00:00:00 2001 From: landaiqing Date: Thu, 2 Apr 2026 19:01:50 +0800 Subject: [PATCH 20/26] :sparkles: Added image code block --- frontend/package.json | 1 + frontend/src/i18n/locales/en-US.ts | 5 + frontend/src/i18n/locales/zh-CN.ts | 761 +++++++++--------- frontend/src/stores/configStore.ts | 13 + frontend/src/stores/editorStore.ts | 33 +- .../src/views/editor/basic/fontExtension.ts | 61 +- .../views/editor/basic/wheelZoomExtension.ts | 53 +- .../editor/extensions/blockImage/clipboard.ts | 5 +- .../extensions/blockImage/contextMenu.ts | 8 +- .../extensions/blockImage/exportPreset.ts | 2 + .../extensions/blockReadonly/contextMenu.ts | 8 +- .../editor/extensions/codeblock/copyPaste.ts | 328 ++++---- .../editor/extensions/codeblock/index.ts | 4 + .../codeblock/lang-parser/codeblock.grammar | 2 +- .../codeblock/lang-parser/languages.ts | 2 + .../codeblock/lang-parser/parser.ts | 8 +- .../extensions/codeblock/parser.test.ts | 8 + .../editor/extensions/codeblock/types.ts | 3 +- .../contextMenu/blockContext.test.ts | 58 ++ .../extensions/contextMenu/blockContext.ts | 40 + .../extensions/contextMenu/constants.ts | 2 + .../extensions/contextMenu/menuSchema.ts | 58 +- .../editor/extensions/imageblock/actions.ts | 46 ++ .../editor/extensions/imageblock/clipboard.ts | 159 ++++ .../editor/extensions/imageblock/constants.ts | 3 + .../extensions/imageblock/document.test.ts | 113 +++ .../editor/extensions/imageblock/document.ts | 236 ++++++ .../editor/extensions/imageblock/drag.ts | 211 +++++ .../editor/extensions/imageblock/extension.ts | 10 + .../editor/extensions/imageblock/index.ts | 3 + .../imageblock/language/build-parser.js | 34 + .../imageblock/language/image-language.ts | 17 + .../imageblock/language/image.grammar | 112 +++ .../imageblock/language/image.grammar.test.ts | 91 +++ .../imageblock/language/image.highlight.ts | 19 + .../imageblock/language/image.parser.terms.ts | 24 + .../imageblock/language/image.parser.ts | 21 + .../extensions/imageblock/language/index.ts | 3 + .../editor/extensions/imageblock/render.ts | 268 ++++++ .../editor/extensions/imageblock/selection.ts | 69 ++ .../extensions/imageblock/syntax.test.ts | 61 ++ .../editor/extensions/imageblock/syntax.ts | 182 +++++ .../editor/extensions/imageblock/types.ts | 13 + .../editor/extensions/imageblock/widget.ts | 303 +++++++ 44 files changed, 2855 insertions(+), 606 deletions(-) create mode 100644 frontend/src/views/editor/extensions/contextMenu/blockContext.test.ts create mode 100644 frontend/src/views/editor/extensions/contextMenu/blockContext.ts create mode 100644 frontend/src/views/editor/extensions/contextMenu/constants.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/actions.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/clipboard.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/constants.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/document.test.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/document.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/drag.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/extension.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/index.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/language/build-parser.js create mode 100644 frontend/src/views/editor/extensions/imageblock/language/image-language.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/language/image.grammar create mode 100644 frontend/src/views/editor/extensions/imageblock/language/image.grammar.test.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/language/image.highlight.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/language/image.parser.terms.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/language/image.parser.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/language/index.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/render.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/selection.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/syntax.test.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/syntax.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/types.ts create mode 100644 frontend/src/views/editor/extensions/imageblock/widget.ts diff --git a/frontend/package.json b/frontend/package.json index 83c457ac..7beb7fcf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "lint": "eslint", "lint:fix": "eslint --fix", "build:lang-parser": "node src/views/editor/extensions/codeblock/lang-parser/build-parser.js", + "build:image-parser": "node src/views/editor/extensions/imageblock/language/build-parser.js", "build:mermaid-parser": "node src/views/editor/language/mermaid/build-parsers.js", "test": "vitest", "docs:dev": "vitepress dev docs", diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index f2c43646..cd448dbf 100644 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -380,6 +380,11 @@ export default { description: 'Protect readonly code block ranges and allow toggling block access from the context menu', markReadonly: 'Set block readonly', markWritable: 'Set block writable' + }, + imageBlock: { + dragSort: 'Drag to reorder', + copyImage: 'Copy image to clipboard', + deleteImage: 'Delete image' } }, monitor: { diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index adbcf41a..5eefec45 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -1,391 +1,396 @@ export default { - locale: 'zh-CN', - common: { - ok: '确定', - cancel: '取消', - edit: '编辑', - delete: '删除', - confirm: '确认', - save: '保存', - reset: '重置' - }, - titlebar: { - minimize: '最小化', - maximize: '最大化', - restore: '向下还原', - close: '关闭' - }, - toolbar: { - editor: { - lines: 'Ln', - characters: 'Ch', - selected: 'Sel' + locale: 'zh-CN', + common: { + ok: '确定', + cancel: '取消', + edit: '编辑', + delete: '删除', + confirm: '确认', + save: '保存', + reset: '重置' }, - fontSizeTooltip: '字体大小 (Ctrl+滚轮调整)', - settings: '设置', - alwaysOnTop: '窗口置顶', - blockLanguage: '块语言', - searchLanguage: '搜索语言...', - noLanguageFound: '未找到匹配的语言', - formatHint: '点击格式化区块(Ctrl+Shift+F)', - previewMarkdown: '预览 Markdown', - closePreview: '关闭预览', - // 文档选择器 - selectDocument: '选择文档', - searchOrCreateDocument: '搜索或输入新文档名...', - createDocument: '创建', - noDocumentFound: '没有找到文档', - loading: '加载中...', - rename: '重命名', - delete: '删除', - confirm: '确认', - confirmDelete: '再次点击确认删除', - openInNewWindow: '在新窗口中打开', - alreadyOpenInNewWindow: '已在新窗口中打开', - }, - languages: { - 'zh-CN': '简体中文', - 'en-US': 'English' - }, - systemTheme: { - dark: '深色', - light: '浅色', - auto: '跟随系统' - }, - keybindings: { - keymapMode: '快捷键模式', - modes: { - standard: '标准模式', - emacs: 'Emacs 模式' + titlebar: { + minimize: '最小化', + maximize: '最大化', + restore: '向下还原', + close: '关闭' }, - headers: { - shortcut: '快捷键', - extension: '扩展', - description: '描述' + toolbar: { + editor: { + lines: 'Ln', + characters: 'Ch', + selected: 'Sel' + }, + fontSizeTooltip: '字体大小 (Ctrl+滚轮调整)', + settings: '设置', + alwaysOnTop: '窗口置顶', + blockLanguage: '块语言', + searchLanguage: '搜索语言...', + noLanguageFound: '未找到匹配的语言', + formatHint: '点击格式化区块(Ctrl+Shift+F)', + previewMarkdown: '预览 Markdown', + closePreview: '关闭预览', + // 文档选择器 + selectDocument: '选择文档', + searchOrCreateDocument: '搜索或输入新文档名...', + createDocument: '创建', + noDocumentFound: '没有找到文档', + loading: '加载中...', + rename: '重命名', + delete: '删除', + confirm: '确认', + confirmDelete: '再次点击确认删除', + openInNewWindow: '在新窗口中打开', + alreadyOpenInNewWindow: '已在新窗口中打开', }, - resetToDefault: '重置为默认', - confirmReset: '确认重置?', - noKeybinding: '未设置', - waitingForKey: '等待输入...', - clickToSet: '点击设置快捷键', - editKeybinding: '编辑快捷键', - config: { - enabled: '启用', - preventDefault: '阻止默认', - keybinding: '快捷键' + languages: { + 'zh-CN': '简体中文', + 'en-US': 'English' }, - keyPlaceholder: '输入键名, 回车添加', - invalidFormat: '格式错误', - conflict: '冲突: {command}', - maxKeysReached: '最多只能添加4个键', - commands: { - showSearch: '显示搜索面板', - hideSearch: '隐藏搜索面板', - searchToggleCase: '切换大小写敏感匹配', - searchToggleWord: '切换整词匹配', - searchToggleRegex: '切换正则表达式匹配', - searchShowReplace: '显示替换功能', - searchReplaceAll: '替换全部匹配项', - blockSelectAll: '块内选择全部', - blockAddAfterCurrent: '在当前块后添加新块', - blockAddAfterLast: '在最后添加新块', - blockAddBeforeCurrent: '在当前块前添加新块', - blockGotoPrevious: '跳转到上一个块', - blockGotoNext: '跳转到下一个块', - blockSelectPrevious: '选择上一个块', - blockSelectNext: '选择下一个块', - blockDelete: '删除当前块', - blockMoveUp: '向上移动当前块', - blockMoveDown: '向下移动当前块', - blockDeleteLine: '删除行', - blockMoveLineUp: '向上移动行', - blockMoveLineDown: '向下移动行', - blockTransposeChars: '字符转置', - blockFormat: '格式化代码块', - blockCopy: '复制', - blockCut: '剪切', - blockPaste: '粘贴', - copyBlockImage: '复制块图片', - historyUndo: '撤销', - historyRedo: '重做', - historyUndoSelection: '撤销选择', - historyRedoSelection: '重做选择', - foldCode: '折叠代码', - unfoldCode: '展开代码', - foldAll: '折叠全部', - unfoldAll: '展开全部', - cursorSyntaxLeft: '光标按语法左移', - cursorSyntaxRight: '光标按语法右移', - selectSyntaxLeft: '按语法选择左侧', - selectSyntaxRight: '按语法选择右侧', - copyLineUp: '向上复制行', - copyLineDown: '向下复制行', - insertBlankLine: '插入空行', - selectLine: '选择行', - selectParentSyntax: '选择父级语法', - simplifySelection: '简化选择', - addCursorAbove: '在上方添加光标', - addCursorBelow: '在下方添加光标', - cursorGroupLeft: '光标按单词左移', - cursorGroupRight: '光标按单词右移', - selectGroupLeft: '按单词选择左侧', - selectGroupRight: '按单词选择右侧', - deleteToLineEnd: '删除到行尾', - deleteToLineStart: '删除到行首', - cursorLineStart: '移动到行首', - cursorLineEnd: '移动到行尾', - selectLineStart: '选择到行首', - selectLineEnd: '选择到行尾', - cursorDocStart: '跳转到文档开头', - cursorDocEnd: '跳转到文档结尾', - selectDocStart: '选择到文档开头', - selectDocEnd: '选择到文档结尾', - selectMatchingBracket: '选择到匹配括号', - splitLine: '分割行', - indentLess: '减少缩进', - indentMore: '增加缩进', - indentSelection: '缩进选择', - cursorMatchingBracket: '光标到匹配括号', - toggleComment: '切换注释', - toggleBlockComment: '切换块注释', - insertNewlineAndIndent: '插入新行并缩进', - deleteCharBackward: '向后删除字符', - deleteCharForward: '向前删除字符', - deleteGroupBackward: '向后删除组', - deleteGroupForward: '向前删除组', - - // Emacs 模式额外的基础导航命令 - cursorCharLeft: '光标左移一个字符', - cursorCharRight: '光标右移一个字符', - cursorLineUp: '光标上移一行', - cursorLineDown: '光标下移一行', - cursorPageUp: '向上翻页', - cursorPageDown: '向下翻页', - selectCharLeft: '选择左移一个字符', - selectCharRight: '选择右移一个字符', - selectLineUp: '选择上移一行', - selectLineDown: '选择下移一行', - } - }, - tabs: { - contextMenu: { - closeTab: '关闭标签', - closeOthers: '关闭其他', - closeLeft: '关闭左侧', - closeRight: '关闭右侧' - } - }, - settings: { - title: '设置', - backToEditor: '返回编辑器', - systemInfo: '系统信息', - general: '常规', - editing: '编辑器', - appearance: '外观', - syncPage: '同步', - extensions: '扩展', - keyBindings: '快捷键', - updates: '更新', - reset: '重置', - apply: '应用', - cancel: '取消', - dangerZone: '危险操作', - resetAllSettings: '重置所有设置', - confirmReset: '确认重置?', - globalHotkey: '全局键盘快捷键', - enableGlobalHotkey: '启用全局热键', - window: '窗口/应用程序', - showInSystemTray: '在系统托盘中显示', - enableSystemTray: '启用系统托盘', - alwaysOnTop: '窗口始终置顶', - enableWindowSnap: '启用窗口吸附', - enableLoadingAnimation: '启用加载动画', - enableTabs: '启用标签页', - enableMemoryMonitor: '启用内存监视器', - startup: '启动设置', - startAtLogin: '开机自启动', - dataStorage: '数据存储', - dataPath: '数据存储路径', - clickToSelectPath: '点击选择路径', - resetDefault: '恢复默认', - resetToDefaultPath: '恢复为默认路径', - fontSize: '字体大小', - fontSettings: '字体设置', - fontFamily: '字体', - fontWeight: '字体粗细', - lineHeight: '行高', - tabSettings: 'Tab 设置', - tabSize: 'Tab 大小', - tabType: 'Tab 类型', - spaces: '空格', - tabs: '制表符', - enableTabIndent: '启用 Tab 缩进', - language: '界面语言', - systemTheme: '系统主题', - presetTheme: '预设主题', - saveOptions: '保存选项', - autoSaveDelay: '自动保存延迟(毫秒)', - updateSettings: '更新设置', - autoCheckUpdates: '自动检查更新', - autoCheckUpdatesDescription: '应用启动时自动检查更新', - manualCheck: '手动更新', - currentVersion: '当前版本', - checkForUpdates: '检查更新', - checking: '正在检查...', - checkFailed: '检查失败', - newVersionAvailable: '发现新版本', - upToDate: '已是最新版本', - viewUpdate: '查看更新', - releaseNotes: '更新日志', - networkError: '网络连接错误,请检查网络设置', - updateNow: '立即更新', - updating: '正在更新...', - updateSuccess: '更新成功', - updateSuccessRestartRequired: '更新已成功应用,请重启应用以生效', - updateSuccessNoRestart: '更新已完成,无需重启', - restartNow: '立即重启', - extensionsPage: { - loading: '加载中', - categoryEditing: '编辑增强', - categoryUI: '界面增强', - categoryTools: '工具扩展', - enabled: '启用', - configuration: '配置', - resetToDefault: '重置为默认配置', + systemTheme: { + dark: '深色', + light: '浅色', + auto: '跟随系统' }, + keybindings: { + keymapMode: '快捷键模式', + modes: { + standard: '标准模式', + emacs: 'Emacs 模式' + }, + headers: { + shortcut: '快捷键', + extension: '扩展', + description: '描述' + }, + resetToDefault: '重置为默认', + confirmReset: '确认重置?', + noKeybinding: '未设置', + waitingForKey: '等待输入...', + clickToSet: '点击设置快捷键', + editKeybinding: '编辑快捷键', + config: { + enabled: '启用', + preventDefault: '阻止默认', + keybinding: '快捷键' + }, + keyPlaceholder: '输入键名, 回车添加', + invalidFormat: '格式错误', + conflict: '冲突: {command}', + maxKeysReached: '最多只能添加4个键', + commands: { + showSearch: '显示搜索面板', + hideSearch: '隐藏搜索面板', + searchToggleCase: '切换大小写敏感匹配', + searchToggleWord: '切换整词匹配', + searchToggleRegex: '切换正则表达式匹配', + searchShowReplace: '显示替换功能', + searchReplaceAll: '替换全部匹配项', + blockSelectAll: '块内选择全部', + blockAddAfterCurrent: '在当前块后添加新块', + blockAddAfterLast: '在最后添加新块', + blockAddBeforeCurrent: '在当前块前添加新块', + blockGotoPrevious: '跳转到上一个块', + blockGotoNext: '跳转到下一个块', + blockSelectPrevious: '选择上一个块', + blockSelectNext: '选择下一个块', + blockDelete: '删除当前块', + blockMoveUp: '向上移动当前块', + blockMoveDown: '向下移动当前块', + blockDeleteLine: '删除行', + blockMoveLineUp: '向上移动行', + blockMoveLineDown: '向下移动行', + blockTransposeChars: '字符转置', + blockFormat: '格式化代码块', + blockCopy: '复制', + blockCut: '剪切', + blockPaste: '粘贴', + copyBlockImage: '复制块图片', + historyUndo: '撤销', + historyRedo: '重做', + historyUndoSelection: '撤销选择', + historyRedoSelection: '重做选择', + foldCode: '折叠代码', + unfoldCode: '展开代码', + foldAll: '折叠全部', + unfoldAll: '展开全部', + cursorSyntaxLeft: '光标按语法左移', + cursorSyntaxRight: '光标按语法右移', + selectSyntaxLeft: '按语法选择左侧', + selectSyntaxRight: '按语法选择右侧', + copyLineUp: '向上复制行', + copyLineDown: '向下复制行', + insertBlankLine: '插入空行', + selectLine: '选择行', + selectParentSyntax: '选择父级语法', + simplifySelection: '简化选择', + addCursorAbove: '在上方添加光标', + addCursorBelow: '在下方添加光标', + cursorGroupLeft: '光标按单词左移', + cursorGroupRight: '光标按单词右移', + selectGroupLeft: '按单词选择左侧', + selectGroupRight: '按单词选择右侧', + deleteToLineEnd: '删除到行尾', + deleteToLineStart: '删除到行首', + cursorLineStart: '移动到行首', + cursorLineEnd: '移动到行尾', + selectLineStart: '选择到行首', + selectLineEnd: '选择到行尾', + cursorDocStart: '跳转到文档开头', + cursorDocEnd: '跳转到文档结尾', + selectDocStart: '选择到文档开头', + selectDocEnd: '选择到文档结尾', + selectMatchingBracket: '选择到匹配括号', + splitLine: '分割行', + indentLess: '减少缩进', + indentMore: '增加缩进', + indentSelection: '缩进选择', + cursorMatchingBracket: '光标到匹配括号', + toggleComment: '切换注释', + toggleBlockComment: '切换块注释', + insertNewlineAndIndent: '插入新行并缩进', + deleteCharBackward: '向后删除字符', + deleteCharForward: '向前删除字符', + deleteGroupBackward: '向后删除组', + deleteGroupForward: '向前删除组', - customThemeColors: '自定义主题颜色', - resetToDefault: '重置为默认', - colorValue: '颜色值', - hotkeyPreview: '预览:', - none: '无', - sync: { - basicSettings: '基本设置', - enableSync: '启用同步', - targetType: '同步方式', - targetTypes: { - git: 'Git', - localfs: '本地文件系统' - }, - autoSync: '自动同步', - syncInterval: '同步间隔', - intervals: { - '5min': '5分钟', - '10min': '10分钟', - '15min': '15分钟', - '30min': '30分钟', - '1hour': '1小时' - }, - repositoryConfig: '仓库配置', - storageConfig: '存储配置', - repoUrl: '仓库地址', - repoUrlPlaceholder: '请输入Git仓库地址', - localfsRootPath: '本地存储目录', - localfsRootPathPlaceholder: '请选择本地同步目录', - authConfig: '认证配置', - authMethod: '认证方式', - authMethods: { - token: '访问令牌', - sshKey: 'SSH密钥', - userPass: '用户名密码' - }, - username: '用户名', - usernamePlaceholder: '请输入用户名', - password: '密码', - passwordPlaceholder: '请输入密码', - token: '访问令牌', - tokenPlaceholder: '请输入访问令牌', - sshKeyPath: 'SSH密钥路径', - sshKeyPathPlaceholder: '请选择SSH密钥文件', - sshKeyPassphrase: 'SSH密钥密码', - sshKeyPassphrasePlaceholder: '请输入SSH密钥密码', - syncOperations: '同步操作', - syncToTarget: '同步到目标', - testingConnection: '测试中...', - syncing: '同步中...', - syncSuccess: '同步成功', - testConnectionSuccess: '连接测试成功', - testConnectionSuccessWithBranch: '连接测试成功,分支:{branch}', - lastSync: '最近同步', - lastSuccess: '最近成功', - lastError: '最近错误', - actions: { - testConnection: '立即测试连接', - sync: '同步', - } - }, - }, - extensions: { - rainbowBrackets: { - name: '彩虹括号', - description: '用不同颜色显示嵌套括号' - }, - hyperlink: { - name: '超链接', - description: '识别并可点击超链接' - }, - colorSelector: { - name: '颜色选择器', - description: 'CSS代码块颜色值的可视化和选择' - }, - translator: { - name: '划词翻译', - description: '选择文本后显示翻译按钮,支持多种翻译服务' - }, - minimap: { - name: '小地图', - description: '显示小地图视图' + // Emacs 模式额外的基础导航命令 + cursorCharLeft: '光标左移一个字符', + cursorCharRight: '光标右移一个字符', + cursorLineUp: '光标上移一行', + cursorLineDown: '光标下移一行', + cursorPageUp: '向上翻页', + cursorPageDown: '向下翻页', + selectCharLeft: '选择左移一个字符', + selectCharRight: '选择右移一个字符', + selectLineUp: '选择上移一行', + selectLineDown: '选择下移一行', + } }, - search: { - name: '搜索功能', - description: '文本搜索和替换功能' + tabs: { + contextMenu: { + closeTab: '关闭标签', + closeOthers: '关闭其他', + closeLeft: '关闭左侧', + closeRight: '关闭右侧' + } }, - fold: { - name: '代码折叠', - description: '折叠和展开代码段以提高代码可读性' - }, - markdown: { - name: 'Markdown 渲染', - description: '渲染 Markdown 元素,“所见即所得”' - }, - codeblock: { - name: '代码块', - description: '代码块相关功能' - }, - lineNumbers: { - name: '行号显示', - description: '在编辑器左侧显示行号,并高亮当前行' - }, - contextMenu: { - name: '上下文菜单', - description: '在编辑器中右键点击时显示上下文菜单' - }, - highlightWhitespace: { - name: '显示空白字符', - description: '在编辑器中显示空格和制表符等空白字符' - }, - highlightTrailingWhitespace: { - name: '高亮行尾空白', - description: '高亮显示行尾的多余空白字符' - }, - httpClient: { - name: 'HTTP 客户端', - description: '在编辑器中直接发送 HTTP 请求并查看响应' + settings: { + title: '设置', + backToEditor: '返回编辑器', + systemInfo: '系统信息', + general: '常规', + editing: '编辑器', + appearance: '外观', + syncPage: '同步', + extensions: '扩展', + keyBindings: '快捷键', + updates: '更新', + reset: '重置', + apply: '应用', + cancel: '取消', + dangerZone: '危险操作', + resetAllSettings: '重置所有设置', + confirmReset: '确认重置?', + globalHotkey: '全局键盘快捷键', + enableGlobalHotkey: '启用全局热键', + window: '窗口/应用程序', + showInSystemTray: '在系统托盘中显示', + enableSystemTray: '启用系统托盘', + alwaysOnTop: '窗口始终置顶', + enableWindowSnap: '启用窗口吸附', + enableLoadingAnimation: '启用加载动画', + enableTabs: '启用标签页', + enableMemoryMonitor: '启用内存监视器', + startup: '启动设置', + startAtLogin: '开机自启动', + dataStorage: '数据存储', + dataPath: '数据存储路径', + clickToSelectPath: '点击选择路径', + resetDefault: '恢复默认', + resetToDefaultPath: '恢复为默认路径', + fontSize: '字体大小', + fontSettings: '字体设置', + fontFamily: '字体', + fontWeight: '字体粗细', + lineHeight: '行高', + tabSettings: 'Tab 设置', + tabSize: 'Tab 大小', + tabType: 'Tab 类型', + spaces: '空格', + tabs: '制表符', + enableTabIndent: '启用 Tab 缩进', + language: '界面语言', + systemTheme: '系统主题', + presetTheme: '预设主题', + saveOptions: '保存选项', + autoSaveDelay: '自动保存延迟(毫秒)', + updateSettings: '更新设置', + autoCheckUpdates: '自动检查更新', + autoCheckUpdatesDescription: '应用启动时自动检查更新', + manualCheck: '手动更新', + currentVersion: '当前版本', + checkForUpdates: '检查更新', + checking: '正在检查...', + checkFailed: '检查失败', + newVersionAvailable: '发现新版本', + upToDate: '已是最新版本', + viewUpdate: '查看更新', + releaseNotes: '更新日志', + networkError: '网络连接错误,请检查网络设置', + updateNow: '立即更新', + updating: '正在更新...', + updateSuccess: '更新成功', + updateSuccessRestartRequired: '更新已成功应用,请重启应用以生效', + updateSuccessNoRestart: '更新已完成,无需重启', + restartNow: '立即重启', + extensionsPage: { + loading: '加载中', + categoryEditing: '编辑增强', + categoryUI: '界面增强', + categoryTools: '工具扩展', + enabled: '启用', + configuration: '配置', + resetToDefault: '重置为默认配置', + }, + + customThemeColors: '自定义主题颜色', + resetToDefault: '重置为默认', + colorValue: '颜色值', + hotkeyPreview: '预览:', + none: '无', + sync: { + basicSettings: '基本设置', + enableSync: '启用同步', + targetType: '同步方式', + targetTypes: { + git: 'Git', + localfs: '本地文件系统' + }, + autoSync: '自动同步', + syncInterval: '同步间隔', + intervals: { + '5min': '5分钟', + '10min': '10分钟', + '15min': '15分钟', + '30min': '30分钟', + '1hour': '1小时' + }, + repositoryConfig: '仓库配置', + storageConfig: '存储配置', + repoUrl: '仓库地址', + repoUrlPlaceholder: '请输入Git仓库地址', + localfsRootPath: '本地存储目录', + localfsRootPathPlaceholder: '请选择本地同步目录', + authConfig: '认证配置', + authMethod: '认证方式', + authMethods: { + token: '访问令牌', + sshKey: 'SSH密钥', + userPass: '用户名密码' + }, + username: '用户名', + usernamePlaceholder: '请输入用户名', + password: '密码', + passwordPlaceholder: '请输入密码', + token: '访问令牌', + tokenPlaceholder: '请输入访问令牌', + sshKeyPath: 'SSH密钥路径', + sshKeyPathPlaceholder: '请选择SSH密钥文件', + sshKeyPassphrase: 'SSH密钥密码', + sshKeyPassphrasePlaceholder: '请输入SSH密钥密码', + syncOperations: '同步操作', + syncToTarget: '同步到目标', + testingConnection: '测试中...', + syncing: '同步中...', + syncSuccess: '同步成功', + testConnectionSuccess: '连接测试成功', + testConnectionSuccessWithBranch: '连接测试成功,分支:{branch}', + lastSync: '最近同步', + lastSuccess: '最近成功', + lastError: '最近错误', + actions: { + testConnection: '立即测试连接', + sync: '同步', + } + }, }, - blockImage: { - name: '代码块导出图片', - description: '将当前代码块渲染为图片并复制到剪贴板', - copyMenu: '复制块为图片' + extensions: { + rainbowBrackets: { + name: '彩虹括号', + description: '用不同颜色显示嵌套括号' + }, + hyperlink: { + name: '超链接', + description: '识别并可点击超链接' + }, + colorSelector: { + name: '颜色选择器', + description: 'CSS代码块颜色值的可视化和选择' + }, + translator: { + name: '划词翻译', + description: '选择文本后显示翻译按钮,支持多种翻译服务' + }, + minimap: { + name: '小地图', + description: '显示小地图视图' + }, + search: { + name: '搜索功能', + description: '文本搜索和替换功能' + }, + fold: { + name: '代码折叠', + description: '折叠和展开代码段以提高代码可读性' + }, + markdown: { + name: 'Markdown 渲染', + description: '渲染 Markdown 元素,“所见即所得”' + }, + codeblock: { + name: '代码块', + description: '代码块相关功能' + }, + lineNumbers: { + name: '行号显示', + description: '在编辑器左侧显示行号,并高亮当前行' + }, + contextMenu: { + name: '上下文菜单', + description: '在编辑器中右键点击时显示上下文菜单' + }, + highlightWhitespace: { + name: '显示空白字符', + description: '在编辑器中显示空格和制表符等空白字符' + }, + highlightTrailingWhitespace: { + name: '高亮行尾空白', + description: '高亮显示行尾的多余空白字符' + }, + httpClient: { + name: 'HTTP 客户端', + description: '在编辑器中直接发送 HTTP 请求并查看响应' + }, + blockImage: { + name: '代码块导出图片', + description: '将当前代码块渲染为图片并复制到剪贴板', + copyMenu: '复制块为图片' + }, + blockReadonly: { + name: '代码块局部只读', + description: '为只读代码块范围提供写保护,并支持在右键菜单中切换块状态', + markReadonly: '设为只读块', + markWritable: '设为可写块' + }, + imageBlock: { + dragSort: '拖拽排序', + copyImage: '复制图片到剪切板', + deleteImage: '删除图片' + }, }, - blockReadonly: { - name: '代码块局部只读', - description: '为只读代码块范围提供写保护,并支持在右键菜单中切换块状态', - markReadonly: '设为只读块', - markWritable: '设为可写块' + monitor: { + memory: '内存', + clickToClean: '点击清理内存' } - }, - monitor: { - memory: '内存', - clickToClean: '点击清理内存' - } }; diff --git a/frontend/src/stores/configStore.ts b/frontend/src/stores/configStore.ts index 3bbb1353..c7adaa21 100644 --- a/frontend/src/stores/configStore.ts +++ b/frontend/src/stores/configStore.ts @@ -169,6 +169,19 @@ export const useConfigStore = defineStore('config', () => { decreaseFontSizeLocal: () => { updateConfigLocal('fontSize', clampValue(state.config.editing.fontSize - 1, 'fontSize')); }, + adjustFontSizeLocal: (delta: number): boolean => { + if (!Number.isFinite(delta) || delta === 0) { + return false; + } + + const nextSize = clampValue(state.config.editing.fontSize + delta, 'fontSize'); + if (nextSize === state.config.editing.fontSize) { + return false; + } + + updateConfigLocal('fontSize', nextSize); + return true; + }, saveFontSize: async () => { await saveConfig('fontSize'); }, diff --git a/frontend/src/stores/editorStore.ts b/frontend/src/stores/editorStore.ts index 99a902c0..cf2c89ca 100644 --- a/frontend/src/stores/editorStore.ts +++ b/frontend/src/stores/editorStore.ts @@ -93,12 +93,11 @@ export const useEditorStore = defineStore('editor', () => { // 滚轮缩放扩展 const wheelZoomExtension = createWheelZoomExtension({ - increaseFontSize: () => { - configStore.increaseFontSizeLocal(); - applyFontSettings(); - }, - decreaseFontSize: () => { - configStore.decreaseFontSizeLocal(); + adjustFontSize: (delta) => { + const changed = configStore.adjustFontSizeLocal(delta); + if (!changed) { + return; + } applyFontSettings(); }, onSave: () => configStore.saveFontSize(), @@ -177,6 +176,17 @@ export const useEditorStore = defineStore('editor', () => { }; }; + const getFontConfigSnapshot = () => ({ + fontFamily: configStore.config.editing.fontFamily, + fontSize: configStore.config.editing.fontSize, + lineHeight: configStore.config.editing.lineHeight, + fontWeight: configStore.config.editing.fontWeight + }); + + const applyFontSettingsToView = (view: EditorView) => { + updateFontConfig(view, getFontConfigSnapshot()); + }; + // 更新编辑器内容 const updateEditorContent = (instance: EditorInstance, doc: Document) => { const currentContent = instance.view.state.doc.toString(); @@ -222,6 +232,7 @@ export const useEditorStore = defineStore('editor', () => { if (!containerElement.value) return; try { + applyFontSettingsToView(instance.view); // 移除当前编辑器 DOM const currentEditor = editorCache.get(currentEditorId.value || 0); if (currentEditor && currentEditor.view.dom && currentEditor.view.dom.parentElement) { @@ -444,12 +455,10 @@ export const useEditorStore = defineStore('editor', () => { // 应用字体设置 const applyFontSettings = () => { editorCache.values().forEach(instance => { - updateFontConfig(instance.view, { - fontFamily: configStore.config.editing.fontFamily, - fontSize: configStore.config.editing.fontSize, - lineHeight: configStore.config.editing.lineHeight, - fontWeight: configStore.config.editing.fontWeight - }); + if (!instance.view.dom.isConnected) { + return; + } + applyFontSettingsToView(instance.view); }); }; diff --git a/frontend/src/views/editor/basic/fontExtension.ts b/frontend/src/views/editor/basic/fontExtension.ts index 7df495bf..154375bd 100644 --- a/frontend/src/views/editor/basic/fontExtension.ts +++ b/frontend/src/views/editor/basic/fontExtension.ts @@ -1,7 +1,6 @@ -import { EditorView } from '@codemirror/view'; -import { Extension, Compartment } from '@codemirror/state'; +import {Compartment, Extension} from '@codemirror/state'; +import {EditorView} from '@codemirror/view'; -// 字体配置接口 export interface FontConfig { fontFamily: string; fontSize?: number; @@ -9,10 +8,8 @@ export interface FontConfig { fontWeight?: string; } -// 创建字体配置compartment export const fontCompartment = new Compartment(); -// 默认字体配置 export const DEFAULT_FONT_CONFIG: FontConfig = { fontFamily: 'HarmonyOS', fontSize: 13, @@ -20,7 +17,19 @@ export const DEFAULT_FONT_CONFIG: FontConfig = { fontWeight: '400' }; -// 从后端配置创建字体配置 +const appliedFontConfigCache = new WeakMap(); + +function normalizeFontConfig(config: Partial): FontConfig { + return {...DEFAULT_FONT_CONFIG, ...config}; +} + +function isSameFontConfig(previous: FontConfig | undefined, next: FontConfig): boolean { + return previous?.fontFamily === next.fontFamily + && previous?.fontSize === next.fontSize + && previous?.lineHeight === next.lineHeight + && previous?.fontWeight === next.fontWeight; +} + export function createFontConfigFromBackend(backendConfig: { fontFamily?: string; fontSize?: number; @@ -35,22 +44,21 @@ export function createFontConfigFromBackend(backendConfig: { }; } -// 创建字体样式扩展 export function createFontExtension(config: Partial = {}): Extension { - const fontConfig = { ...DEFAULT_FONT_CONFIG, ...config }; - + const fontConfig = normalizeFontConfig(config); + const styles: Record = { '&': { fontFamily: fontConfig.fontFamily, - ...(fontConfig.fontSize && { fontSize: `${fontConfig.fontSize}px` }), - ...(fontConfig.lineHeight && { lineHeight: fontConfig.lineHeight.toString() }), - ...(fontConfig.fontWeight && { fontWeight: fontConfig.fontWeight }), + ...(fontConfig.fontSize && {fontSize: `${fontConfig.fontSize}px`}), + ...(fontConfig.lineHeight && {lineHeight: fontConfig.lineHeight.toString()}), + ...(fontConfig.fontWeight && {fontWeight: fontConfig.fontWeight}), }, '.cm-content': { fontFamily: fontConfig.fontFamily, - ...(fontConfig.fontSize && { fontSize: `${fontConfig.fontSize}px` }), - ...(fontConfig.lineHeight && { lineHeight: fontConfig.lineHeight.toString() }), - ...(fontConfig.fontWeight && { fontWeight: fontConfig.fontWeight }), + ...(fontConfig.fontSize && {fontSize: `${fontConfig.fontSize}px`}), + ...(fontConfig.lineHeight && {lineHeight: fontConfig.lineHeight.toString()}), + ...(fontConfig.fontWeight && {fontWeight: fontConfig.fontWeight}), }, '.cm-editor': { fontFamily: fontConfig.fontFamily, @@ -60,15 +68,15 @@ export function createFontExtension(config: Partial = {}): Extension }, '.cm-gutters': { fontFamily: fontConfig.fontFamily, - ...(fontConfig.fontSize && { fontSize: `${fontConfig.fontSize}px` }), + ...(fontConfig.fontSize && {fontSize: `${fontConfig.fontSize}px`}), }, '.cm-lineNumbers': { fontFamily: fontConfig.fontFamily, - ...(fontConfig.fontSize && { fontSize: `${Math.max(10, fontConfig.fontSize - 1)}px` }), + ...(fontConfig.fontSize && {fontSize: `${Math.max(10, fontConfig.fontSize - 1)}px`}), }, '.cm-tooltip': { fontFamily: fontConfig.fontFamily, - ...(fontConfig.fontSize && { fontSize: `${Math.max(12, fontConfig.fontSize - 1)}px` }), + ...(fontConfig.fontSize && {fontSize: `${Math.max(12, fontConfig.fontSize - 1)}px`}), }, '.cm-completionLabel': { fontFamily: fontConfig.fontFamily, @@ -77,11 +85,10 @@ export function createFontExtension(config: Partial = {}): Extension fontFamily: fontConfig.fontFamily, } }; - + return EditorView.theme(styles); } -// 从后端配置创建字体扩展 export function createFontExtensionFromBackend(backendConfig: { fontFamily?: string; fontSize?: number; @@ -92,12 +99,14 @@ export function createFontExtensionFromBackend(backendConfig: { return fontCompartment.of(createFontExtension(fontConfig)); } -// 动态更新字体配置 export function updateFontConfig(view: EditorView, config: Partial): void { - const newFontExtension = createFontExtension(config); - - // 使用compartment重新配置字体扩展 + const nextFontConfig = normalizeFontConfig(config); + if (isSameFontConfig(appliedFontConfigCache.get(view), nextFontConfig)) { + return; + } + + appliedFontConfigCache.set(view, nextFontConfig); view.dispatch({ - effects: fontCompartment.reconfigure(newFontExtension) + effects: fontCompartment.reconfigure(createFontExtension(nextFontConfig)) }); -} \ No newline at end of file +} diff --git a/frontend/src/views/editor/basic/wheelZoomExtension.ts b/frontend/src/views/editor/basic/wheelZoomExtension.ts index 59110b31..4edd708d 100644 --- a/frontend/src/views/editor/basic/wheelZoomExtension.ts +++ b/frontend/src/views/editor/basic/wheelZoomExtension.ts @@ -1,25 +1,22 @@ -import {EditorView} from '@codemirror/view'; import type {Extension} from '@codemirror/state'; +import {EditorView} from '@codemirror/view'; import {createDebounce} from '@/common/utils/debounce'; type FontAdjuster = () => void; +type FontDeltaAdjuster = (delta: number) => void; type SaveCallback = () => Promise | void; export interface WheelZoomOptions { - /** 增加字体大小的回调(立即执行) */ - increaseFontSize: FontAdjuster; - /** 减少字体大小的回调(立即执行) */ - decreaseFontSize: FontAdjuster; - /** 保存回调(防抖执行),在滚动结束后调用 */ + increaseFontSize?: FontAdjuster; + decreaseFontSize?: FontAdjuster; + adjustFontSize?: FontDeltaAdjuster; onSave?: SaveCallback; - /** 保存防抖延迟(毫秒),默认 300ms */ saveDelay?: number; } export const createWheelZoomExtension = (options: WheelZoomOptions): Extension => { - const {increaseFontSize, decreaseFontSize, onSave, saveDelay = 300} = options; + const {increaseFontSize, decreaseFontSize, adjustFontSize, onSave, saveDelay = 300} = options; - // 如果有 onSave 回调,创建防抖版本 const {debouncedFn: debouncedSave} = onSave ? createDebounce(() => { try { @@ -35,6 +32,34 @@ export const createWheelZoomExtension = (options: WheelZoomOptions): Extension = }, {delay: saveDelay}) : {debouncedFn: null}; + let pendingDelta = 0; + let frameId: number | null = null; + + const flushPendingDelta = () => { + frameId = null; + + if (pendingDelta === 0) { + return; + } + + const delta = pendingDelta; + pendingDelta = 0; + + if (adjustFontSize) { + adjustFontSize(delta); + return; + } + + const applyStep = delta > 0 ? increaseFontSize : decreaseFontSize; + if (!applyStep) { + return; + } + + for (let index = 0; index < Math.abs(delta); index++) { + applyStep(); + } + }; + return EditorView.domEventHandlers({ wheel(event) { if (!event.ctrlKey) { @@ -43,14 +68,16 @@ export const createWheelZoomExtension = (options: WheelZoomOptions): Extension = event.preventDefault(); - // 立即更新字体大小 if (event.deltaY < 0) { - increaseFontSize(); + pendingDelta += 1; } else if (event.deltaY > 0) { - decreaseFontSize(); + pendingDelta -= 1; + } + + if (pendingDelta !== 0 && frameId === null) { + frameId = requestAnimationFrame(flushPendingDelta); } - // 防抖保存 if (debouncedSave) { debouncedSave(); } diff --git a/frontend/src/views/editor/extensions/blockImage/clipboard.ts b/frontend/src/views/editor/extensions/blockImage/clipboard.ts index b027e4ba..dd3a4754 100644 --- a/frontend/src/views/editor/extensions/blockImage/clipboard.ts +++ b/frontend/src/views/editor/extensions/blockImage/clipboard.ts @@ -10,13 +10,14 @@ export function canvasToPngBlob(canvas: HTMLCanvasElement): Promise { }); } -export async function writeImageToClipboard(blob: Blob): Promise { +export async function writeImageToClipboard(blob: Blob, mimeType = blob.type || 'image/png'): Promise { const ClipboardItemCtor = globalThis.ClipboardItem; if (!ClipboardItemCtor || !navigator.clipboard?.write) { throw new Error('Clipboard image write is not supported in this environment'); } + const resolvedType = mimeType.startsWith('image/') ? mimeType : 'image/png'; await navigator.clipboard.write([ - new ClipboardItemCtor({'image/png': blob}), + new ClipboardItemCtor({[resolvedType]: blob}), ]); } diff --git a/frontend/src/views/editor/extensions/blockImage/contextMenu.ts b/frontend/src/views/editor/extensions/blockImage/contextMenu.ts index 2e662b50..9bd8d2fc 100644 --- a/frontend/src/views/editor/extensions/blockImage/contextMenu.ts +++ b/frontend/src/views/editor/extensions/blockImage/contextMenu.ts @@ -1,5 +1,5 @@ import type {MenuSchemaNode} from '../contextMenu/menuSchema'; -import {getActiveNoteBlock} from '../codeblock/state'; +import {getMenuBlock, runCommandInMenuBlock} from '../contextMenu/blockContext'; import {blockImageEnabledFacet, copyBlockImageCommand} from './index'; @@ -7,13 +7,13 @@ export const blockImageMenuNodes: MenuSchemaNode[] = [ { id: 'copy-block-image', labelKey: 'extensions.blockImage.copyMenu', - command: copyBlockImageCommand, + command: runCommandInMenuBlock(copyBlockImageCommand), visible: context => context.view.state.facet(blockImageEnabledFacet) && - Boolean(getActiveNoteBlock(context.view.state)), + Boolean(getMenuBlock(context)), enabled: context => context.view.state.facet(blockImageEnabledFacet) && - Boolean(getActiveNoteBlock(context.view.state)), + Boolean(getMenuBlock(context)), }, ]; diff --git a/frontend/src/views/editor/extensions/blockImage/exportPreset.ts b/frontend/src/views/editor/extensions/blockImage/exportPreset.ts index b8dc3c9b..348b31ac 100644 --- a/frontend/src/views/editor/extensions/blockImage/exportPreset.ts +++ b/frontend/src/views/editor/extensions/blockImage/exportPreset.ts @@ -18,6 +18,7 @@ import {colorTheme, colorView} from '../colorSelector'; import {getCodeBlockLanguageExtension} from '../codeblock/lang-parser'; import {getMathBlockExtensions} from '../codeblock/mathBlock'; import type {Block, BlockAccess} from '../codeblock/types'; +import {getImageBlockExtensions} from '../imageblock'; import {hyperLink} from '../hyperlink'; import {headingSlugField} from '../markdown/state/heading-slug'; import {render} from '../markdown/plugins/render'; @@ -240,6 +241,7 @@ export async function createBlockImageExportPreset(block: Block, sourceIndex: nu blockState, ...getCodeBlockLanguageExtension(), delimiterPresentationField, + ...getImageBlockExtensions(), ...getMathBlockExtensions(), createExportChromeTheme(), ...optionalContributions.map(({extension}) => extension), diff --git a/frontend/src/views/editor/extensions/blockReadonly/contextMenu.ts b/frontend/src/views/editor/extensions/blockReadonly/contextMenu.ts index fad50aed..69804596 100644 --- a/frontend/src/views/editor/extensions/blockReadonly/contextMenu.ts +++ b/frontend/src/views/editor/extensions/blockReadonly/contextMenu.ts @@ -1,5 +1,5 @@ import type { MenuContext, MenuSchemaNode } from '../contextMenu/menuSchema'; -import { getActiveNoteBlock } from '../codeblock/state'; +import {getMenuBlock as getTargetMenuBlock, runCommandInMenuBlock} from '../contextMenu/blockContext'; import { blockReadonlyEnabledFacet, setActiveBlockReadOnlyCommand, @@ -7,7 +7,7 @@ import { } from './index'; function getMenuBlock(context: MenuContext) { - return getActiveNoteBlock(context.view.state); + return getTargetMenuBlock(context); } function isReadonlyExtensionEnabled(context: MenuContext) { @@ -18,7 +18,7 @@ export const blockReadonlyMenuNodes: MenuSchemaNode[] = [ { id: 'set-block-readonly', labelKey: 'extensions.blockReadonly.markReadonly', - command: setActiveBlockReadOnlyCommand, + command: runCommandInMenuBlock(setActiveBlockReadOnlyCommand), visible: context => isReadonlyExtensionEnabled(context) && Boolean(getMenuBlock(context)?.delimiter.to), @@ -30,7 +30,7 @@ export const blockReadonlyMenuNodes: MenuSchemaNode[] = [ { id: 'set-block-writable', labelKey: 'extensions.blockReadonly.markWritable', - command: setActiveBlockWritableCommand, + command: runCommandInMenuBlock(setActiveBlockWritableCommand), visible: context => isReadonlyExtensionEnabled(context) && Boolean(getMenuBlock(context)?.delimiter.to), diff --git a/frontend/src/views/editor/extensions/codeblock/copyPaste.ts b/frontend/src/views/editor/extensions/codeblock/copyPaste.ts index b6099fb0..22e70c6a 100644 --- a/frontend/src/views/editor/extensions/codeblock/copyPaste.ts +++ b/frontend/src/views/editor/extensions/codeblock/copyPaste.ts @@ -3,11 +3,12 @@ * 防止复制分隔符标记,自动替换为换行符 */ -import { EditorState, EditorSelection } from "@codemirror/state"; -import { EditorView, Command } from "@codemirror/view"; -import { LANGUAGES } from "./lang-parser/languages"; -import { USER_EVENTS, codeBlockEvent, CONTENT_EDIT } from "./annotation"; +import {EditorSelection, EditorState} from "@codemirror/state"; +import {Command, EditorView} from "@codemirror/view"; +import {LANGUAGES} from "./lang-parser/languages"; +import {codeBlockEvent, CONTENT_EDIT, USER_EVENTS} from "./annotation"; import * as runtime from "@wailsio/runtime"; +import {handleImagePasteEvent, pasteImagesFromClipboard} from "../imageblock/clipboard"; /** * 构建块分隔符正则表达式 @@ -19,173 +20,198 @@ const blockSeparatorRegex = new RegExp(`\\n∞∞∞(?:${languageTokensMatcher}) * 获取被复制的范围和内容 */ function copiedRange(state: EditorState, forCut = false) { - const content: string[] = []; - const ranges: any[] = []; + const content: string[] = []; + const ranges: any[] = []; - for (const range of state.selection.ranges) { - if (!range.empty) { - content.push(state.sliceDoc(range.from, range.to)); - ranges.push(range); + for (const range of state.selection.ranges) { + if (!range.empty) { + content.push(state.sliceDoc(range.from, range.to)); + ranges.push(range); + } } - } - if (ranges.length === 0) { - const copiedLines: number[] = []; - for (const range of state.selection.ranges) { - if (!range.empty) { - continue; - } - - const line = state.doc.lineAt(range.head); - const lineContent = state.sliceDoc(line.from, line.to); - if (copiedLines.includes(line.from)) { - continue; - } - - content.push(lineContent); - if (forCut) { - const lineEnd = line.to < state.doc.length ? line.to + 1 : line.to; - ranges.push({ from: line.from, to: lineEnd }); - } else { - ranges.push(range); - } - copiedLines.push(line.from); + if (ranges.length === 0) { + const copiedLines: number[] = []; + for (const range of state.selection.ranges) { + if (!range.empty) { + continue; + } + + const line = state.doc.lineAt(range.head); + const lineContent = state.sliceDoc(line.from, line.to); + if (copiedLines.includes(line.from)) { + continue; + } + + content.push(lineContent); + if (forCut) { + const lineEnd = line.to < state.doc.length ? line.to + 1 : line.to; + ranges.push({from: line.from, to: lineEnd}); + } else { + ranges.push(range); + } + copiedLines.push(line.from); + } } - } - return { - text: content.join(state.lineBreak), - ranges, - }; + return { + text: content.join(state.lineBreak), + ranges, + }; } /** * 设置浏览器复制和剪切事件处理器,将块分隔符替换为换行符 */ export const codeBlockCopyCut = EditorView.domEventHandlers({ - copy(event, view) { - event.preventDefault(); - - let { text } = copiedRange(view.state); - text = text.replaceAll(blockSeparatorRegex, "\n\n"); - - runtime.Clipboard.SetText(text).catch(() => { - const data = event.clipboardData; - if (data) { - data.clearData(); - data.setData("text/plain", text); - } - }); - }, - - cut(event, view) { - event.preventDefault(); + copy(event, view) { + event.preventDefault(); - let { text, ranges } = copiedRange(view.state, true); - text = text.replaceAll(blockSeparatorRegex, "\n\n"); + let {text} = copiedRange(view.state); + text = text.replaceAll(blockSeparatorRegex, "\n\n"); - runtime.Clipboard.SetText(text).catch(() => { - const data = event.clipboardData; - if (data) { - data.clearData(); - data.setData("text/plain", text); - } - }); + runtime.Clipboard.SetText(text).catch(() => { + const data = event.clipboardData; + if (data) { + data.clearData(); + data.setData("text/plain", text); + } + }); + }, - if (!view.state.readOnly) { - view.dispatch({ - changes: ranges, - scrollIntoView: true, - userEvent: USER_EVENTS.DELETE_CUT, - annotations: [codeBlockEvent.of(CONTENT_EDIT)], - }); - } - }, + cut(event, view) { + event.preventDefault(); - paste(event, view) { - if (view.state.readOnly) { - return false; - } + let {text} = copiedRange(view.state, true); + const {ranges} = copiedRange(view.state, true); + text = text.replaceAll(blockSeparatorRegex, "\n\n"); - event.preventDefault(); + runtime.Clipboard.SetText(text).catch(() => { + const data = event.clipboardData; + if (data) { + data.clearData(); + data.setData("text/plain", text); + } + }); + + if (!view.state.readOnly) { + view.dispatch({ + changes: ranges, + scrollIntoView: true, + userEvent: USER_EVENTS.DELETE_CUT, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], + }); + } + }, - runtime.Clipboard.Text() - .then(text => { - if (text) { - doPaste(view, text); + paste(event, view) { + if (view.state.readOnly) { + return false; } - }) - .catch(error => { - console.error('[Clipboard] Failed to read from system clipboard:', error); - const data = event.clipboardData; - if (data) { - const text = data.getData("text/plain"); - if (text) { - doPaste(view, text); - } + if (handleImagePasteEvent(view, event as ClipboardEvent)) { + return true; } - }); - return true; - } + event.preventDefault(); + void pasteText(view, event as ClipboardEvent); + + return true; + } }); /** * 复制和剪切的通用函数 */ const copyCut = (view: EditorView, cut: boolean): boolean => { - let { text, ranges } = copiedRange(view.state, cut); - text = text.replaceAll(blockSeparatorRegex, "\n\n"); + let {text} = copiedRange(view.state, cut); + const {ranges} = copiedRange(view.state, cut); + text = text.replaceAll(blockSeparatorRegex, "\n\n"); - runtime.Clipboard.SetText(text).catch(err => { - console.error('[Clipboard] Failed to write to system clipboard:', err); + runtime.Clipboard.SetText(text).catch(err => { + console.error('[Clipboard] Failed to write to system clipboard:', err); - if (navigator.clipboard?.writeText) { - navigator.clipboard.writeText(text); - } - }); - - if (cut && !view.state.readOnly) { - view.dispatch({ - changes: ranges, - scrollIntoView: true, - userEvent: USER_EVENTS.DELETE_CUT, - annotations: [codeBlockEvent.of(CONTENT_EDIT)], + if (navigator.clipboard?.writeText) { + navigator.clipboard.writeText(text); + } }); - } - return true; + if (cut && !view.state.readOnly) { + view.dispatch({ + changes: ranges, + scrollIntoView: true, + userEvent: USER_EVENTS.DELETE_CUT, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], + }); + } + + return true; }; /** * 粘贴函数 */ function doPaste(view: EditorView, input: string) { - const { state } = view; - const text = state.toText(input); - const byLine = text.lines === state.selection.ranges.length; - - let changes: any; - - if (byLine) { - let i = 1; - changes = state.changeByRange(range => { - const line = text.line(i++); - return { - changes: { from: range.from, to: range.to, insert: line.text }, - range: EditorSelection.cursor(range.from + line.length) - }; + const {state} = view; + const text = state.toText(input); + const byLine = text.lines === state.selection.ranges.length; + + let changes: any; + + if (byLine) { + let i = 1; + changes = state.changeByRange(range => { + const line = text.line(i++); + return { + changes: {from: range.from, to: range.to, insert: line.text}, + range: EditorSelection.cursor(range.from + line.length) + }; + }); + } else { + changes = state.replaceSelection(text); + } + + view.dispatch(changes, { + userEvent: USER_EVENTS.INPUT_PASTE, + scrollIntoView: true, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], }); - } else { - changes = state.replaceSelection(text); - } - - view.dispatch(changes, { - userEvent: USER_EVENTS.INPUT_PASTE, - scrollIntoView: true, - annotations: [codeBlockEvent.of(CONTENT_EDIT)], - }); +} + +async function readClipboardText(event?: ClipboardEvent): Promise { + try { + const text = await runtime.Clipboard.Text(); + if (text) { + return text; + } + } catch (error) { + console.error('[Clipboard] Failed to read from system clipboard:', error); + } + + const eventText = event?.clipboardData?.getData("text/plain"); + if (eventText) { + return eventText; + } + + if (navigator.clipboard?.readText) { + try { + return await navigator.clipboard.readText(); + } catch (fallbackErr) { + console.error('[Clipboard] Fallback also failed:', fallbackErr); + } + } + + return ""; +} + +async function pasteText(view: EditorView, event?: ClipboardEvent): Promise { + const text = await readClipboardText(event); + if (!text) { + return false; + } + + doPaste(view, text); + return true; } /** @@ -202,36 +228,22 @@ export const cutCommand: Command = view => copyCut(view, true); * 粘贴命令 */ export const pasteCommand: Command = (view) => { - runtime.Clipboard.Text() - .then(text => { - if (text) { - doPaste(view, text); - } - }) - .catch(err => { - console.error('[Clipboard] Failed to read from system clipboard:', err); - - if (navigator.clipboard?.readText) { - navigator.clipboard.readText() - .then(text => { - if (text) { - doPaste(view, text); - } - }) - .catch(fallbackErr => { - console.error('[Clipboard] Fallback also failed:', fallbackErr); - }); - } - }); + void (async () => { + if (await pasteImagesFromClipboard(view)) { + return; + } + + await pasteText(view); + })(); - return true; + return true; }; /** * 获取复制粘贴扩展 */ export function getCopyPasteExtensions() { - return [ - codeBlockCopyCut, - ]; + return [ + codeBlockCopyCut, + ]; } diff --git a/frontend/src/views/editor/extensions/codeblock/index.ts b/frontend/src/views/editor/extensions/codeblock/index.ts index c626bd97..5230c0c9 100644 --- a/frontend/src/views/editor/extensions/codeblock/index.ts +++ b/frontend/src/views/editor/extensions/codeblock/index.ts @@ -26,6 +26,7 @@ import {createLanguageDetection} from './lang-detect'; import {SupportedLanguage} from './types'; import {getMathBlockExtensions} from './mathBlock'; import {createCursorProtection} from './cursorProtection'; +import {getImageBlockExtensions} from '../imageblock'; /** * 代码块扩展配置选项 @@ -119,6 +120,9 @@ export function createCodeBlockExtension(options: CodeBlockOptions = {}): Extens // 复制粘贴功能 ...getCopyPasteExtensions(), + // image 块可视化渲染 + ...getImageBlockExtensions(), + // 数学块功能 ...getMathBlockExtensions(), diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/codeblock.grammar b/frontend/src/views/editor/extensions/codeblock/lang-parser/codeblock.grammar index 388d52eb..878ce969 100644 --- a/frontend/src/views/editor/extensions/codeblock/lang-parser/codeblock.grammar +++ b/frontend/src/views/editor/extensions/codeblock/lang-parser/codeblock.grammar @@ -18,7 +18,7 @@ BlockLanguage { "go" | "clj" | "ex" | "erl" | "js" | "ts" | "swift" | "kt" | "groovy" | "ps1" | "dart" | "scala" | "math" | "dockerfile" | "lua" | "vue" | "lezer" | "liquid" | "wast" | "sass" | "less" | "angular" | "svelte" | - "http" | "mermaid" + "http" | "mermaid" | "image" } BlockFlag { diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts b/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts index b98bbad0..4b15d129 100644 --- a/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts +++ b/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts @@ -30,6 +30,7 @@ import {lessLanguage} from "@codemirror/lang-less"; import {angularLanguage} from "@codemirror/lang-angular"; import {svelteLanguage} from "@replit/codemirror-lang-svelte"; import {httpLanguage} from "@/views/editor/extensions/httpclient/language/http-language"; +import {imageLanguage} from "@/views/editor/extensions/imageblock/language/image-language"; import {mermaidLanguage} from '@/views/editor/language/mermaid'; import {StreamLanguage} from "@codemirror/language"; import {ruby} from "@codemirror/legacy-modes/mode/ruby"; @@ -248,6 +249,7 @@ export const LANGUAGES: LanguageInfo[] = [ } }), new LanguageInfo("http", "Http", httpLanguage.parser, ["http"]), + new LanguageInfo("image", "Image", imageLanguage.parser, ["image"]), new LanguageInfo("mermaid", "Mermaid", mermaidLanguage.parser, ["mermaid"]), ]; diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/parser.ts b/frontend/src/views/editor/extensions/codeblock/lang-parser/parser.ts index 491c905f..6c5d0802 100644 --- a/frontend/src/views/editor/extensions/codeblock/lang-parser/parser.ts +++ b/frontend/src/views/editor/extensions/codeblock/lang-parser/parser.ts @@ -3,14 +3,14 @@ import {LRParser} from "@lezer/lr" import {blockContent} from "./external-tokens.js" export const parser = LRParser.deserialize({ version: 14, - states: "!|QQOQOOOVOQO'#C`O#{OPO'#C_OOOO'#Cf'#CfQQOQOOOOOO'#Ca'#CaO$QOSO,58zOOOO,58y,58yOOOO-E6d-E6dOOOO'#Cb'#CbOOOO'#Cg'#CgO$`OSO1G.fOOOP1G.f1G.fOOOO-E6e-E6eOOOP7+$Q7+$Q", - stateData: "$n~O]PO~O^TO_TO`TOaTObTOcTOdTOeTOfTOgTOhTOiTOjTOkTOlTOmTOnTOoTOpTOqTOrTOsTOtTOuTOvTOwTOxTOyTOzTO{TO|TO}TO!OTO!PTO!QTO!RTO!STO!TTO!UTO!VTO!WTO!XTO~OPVO~OVXOWXOXXO!Y[O~OVXOWXOXXO!Y^O~O", + states: "!|QQOQOOOVOQO'#C`O$OOPO'#C_OOOO'#Cf'#CfQQOQOOOOOO'#Ca'#CaO$TOSO,58zOOOO,58y,58yOOOO-E6d-E6dOOOO'#Cb'#CbOOOO'#Cg'#CgO$cOSO1G.fOOOP1G.f1G.fOOOO-E6e-E6eOOOP7+$Q7+$Q", + stateData: "$q~O]PO~O^TO_TO`TOaTObTOcTOdTOeTOfTOgTOhTOiTOjTOkTOlTOmTOnTOoTOpTOqTOrTOsTOtTOuTOvTOwTOxTOyTOzTO{TO|TO}TO!OTO!PTO!QTO!RTO!STO!TTO!UTO!VTO!WTO!XTO!YTO~OPVO~OVXOWXOXXO!Z[O~OVXOWXOXXO!Z^O~O", goto: "x[PPP]aehPPPlrTROSTQOSRUPTYUZQSORWSQZUR]Z", nodeNames: "⚠ BlockContent Document Block BlockDelimiter BlockLanguage BlockFlag Auto ReadOnly Writable", - maxTerm: 56, + maxTerm: 57, skippedNodes: [0], repeatNodeCount: 2, - tokenData: "4}~RdYZ!a}!O!z#T#U#g#V#W$b#W#X%c#X#Y'U#Z#['o#[#](l#^#_)c#_#`*b#`#a*m#a#b,j#d#e.R#f#g.z#g#h/_#h#i2U#j#k3Z#k#l3l#l#m4T#m#n4fR!fP!YQ%&x%&y!iP!lP%&x%&y!oP!rP%&x%&y!uP!zO]P~!}R#T#U#W#f#g#]#k#l#b~#]OV~~#bOW~~#gOX~~#jP#b#c#m~#pP#Z#[#s~#vP#i#j#y~#|P#`#a$P~$SP#T#U$V~$YP#f#g$]~$bO!U~~$eR#`#a$n#d#e$y#g#h%U~$qP#^#_$t~$yOp~~$|P#d#e%P~%UOh~~%ZPj~#g#h%^~%cOf~~%fQ#T#U%l#c#d%}~%oP#f#g%r~%uP#h#i%x~%}Oy~~&QP#V#W&T~&WP#_#`&Z~&^P#X#Y&a~&dP#f#g&g~&jP#Y#Z&m~&pP#]#^&s~&vP#`#a&y~&|P#X#Y'P~'UO|~~'XQ#f#g'_#l#m'j~'bP#`#a'e~'jOr~~'oOq~~'rQ#c#d'x#f#g'}~'}Oo~~(QP#c#d(T~(WP#c#d(Z~(^P#j#k(a~(dP#m#n(g~(lOw~~(oP#h#i(r~(uQ#a#b({#h#i)W~)OP#`#a)R~)WOa~~)ZP#d#e)^~)cO!W~~)fQ#T#U)l#g#h)}~)oP#j#k)r~)uP#T#U)x~)}Od~~*SPs~#c#d*V~*YP#b#c*]~*bO_~~*eP#h#i*h~*mOv~~*pR#X#Y*y#]#^+p#i#j,_~*|Q#g#h+S#n#o+_~+VP#g#h+Y~+_O!T~~+bP#X#Y+e~+hP#f#g+k~+pO!P~~+sP#e#f+v~+yP#i#j+|~,PP#]#^,S~,VP#W#X,Y~,_O!Q~~,bP#T#U,e~,jO}~~,mR#T#U,v#W#X-X#X#Y-^~,yP#h#i,|~-PP#[#]-S~-XO{~~-^Oc~~-aP#f#g-d~-gP#a#b-j~-mP#T#U-p~-sP#]#^-v~-yP#W#X-|~.RO!X~~.UR#[#]._#g#h.j#m#n.u~.bP#d#e.e~.jOe~~.mP!R!S.p~.uOx~~.zO`~~.}Q#U#V/T#g#h/Y~/YOk~~/_Oi~~/bU#T#U/t#V#W0V#[#]0n#e#f0s#j#k1O#k#l1m~/wP#g#h/z~/}P#g#h0Q~0VO!S~~0YP#T#U0]~0`P#`#a0c~0fP#T#U0i~0nOz~~0sOl~~0vP#`#a0y~1OOb~~1RP#X#Y1U~1XP#`#a1[~1_P#h#i1b~1eP#X#Y1h~1mO!V~~1pP#]#^1s~1vP#Y#Z1y~1|P#h#i2P~2UOu~~2XR#X#Y2b#c#d2s#g#h3U~2eP#l#m2h~2kP#h#i2n~2sO^~~2vP#a#b2y~2|P#`#a3P~3UOn~~3ZOt~~3^P#i#j3a~3dP#X#Y3g~3lO!O~~3oP#T#U3r~3uP#g#h3x~3{P#h#i4O~4TO!R~~4WP#a#b4Z~4^P#`#a4a~4fOg~~4iP#T#U4l~4oP#a#b4r~4uP#`#a4x~4}Om~", + tokenData: "5o~ReYZ!d}!O!}#T#U#j#V#W$e#W#X%f#X#Y'X#Z#['r#[#](o#]#^)f#^#_*T#_#`+S#`#a+_#a#b-[#d#e.s#f#g/l#g#h0P#h#i2v#j#k3{#k#l4^#l#m4u#m#n5WR!iP!ZQ%&x%&y!lP!oP%&x%&y!rP!uP%&x%&y!xP!}O]P~#QR#T#U#Z#f#g#`#k#l#e~#`OV~~#eOW~~#jOX~~#mP#b#c#p~#sP#Z#[#v~#yP#i#j#|~$PP#`#a$S~$VP#T#U$Y~$]P#f#g$`~$eO!U~~$hR#`#a$q#d#e$|#g#h%X~$tP#^#_$w~$|Op~~%PP#d#e%S~%XOh~~%^Pj~#g#h%a~%fOf~~%iQ#T#U%o#c#d&Q~%rP#f#g%u~%xP#h#i%{~&QOy~~&TP#V#W&W~&ZP#_#`&^~&aP#X#Y&d~&gP#f#g&j~&mP#Y#Z&p~&sP#]#^&v~&yP#`#a&|~'PP#X#Y'S~'XO|~~'[Q#f#g'b#l#m'm~'eP#`#a'h~'mOr~~'rOq~~'uQ#c#d'{#f#g(Q~(QOo~~(TP#c#d(W~(ZP#c#d(^~(aP#j#k(d~(gP#m#n(j~(oOw~~(rP#h#i(u~(xQ#a#b)O#h#i)Z~)RP#`#a)U~)ZOa~~)^P#d#e)a~)fO!W~~)iP#a#b)l~)oP#T#U)r~)uP#Z#[)x~){P#X#Y*O~*TO!Y~~*WQ#T#U*^#g#h*o~*aP#j#k*d~*gP#T#U*j~*oOd~~*tPs~#c#d*w~*zP#b#c*}~+SO_~~+VP#h#i+Y~+_Ov~~+bR#X#Y+k#]#^,b#i#j-P~+nQ#g#h+t#n#o,P~+wP#g#h+z~,PO!T~~,SP#X#Y,V~,YP#f#g,]~,bO!P~~,eP#e#f,h~,kP#i#j,n~,qP#]#^,t~,wP#W#X,z~-PO!Q~~-SP#T#U-V~-[O}~~-_R#T#U-h#W#X-y#X#Y.O~-kP#h#i-n~-qP#[#]-t~-yO{~~.OOc~~.RP#f#g.U~.XP#a#b.[~._P#T#U.b~.eP#]#^.h~.kP#W#X.n~.sO!X~~.vR#[#]/P#g#h/[#m#n/g~/SP#d#e/V~/[Oe~~/_P!R!S/b~/gOx~~/lO`~~/oQ#U#V/u#g#h/z~/zOk~~0POi~~0SU#T#U0f#V#W0w#[#]1`#e#f1e#j#k1p#k#l2_~0iP#g#h0l~0oP#g#h0r~0wO!S~~0zP#T#U0}~1QP#`#a1T~1WP#T#U1Z~1`Oz~~1eOl~~1hP#`#a1k~1pOb~~1sP#X#Y1v~1yP#`#a1|~2PP#h#i2S~2VP#X#Y2Y~2_O!V~~2bP#]#^2e~2hP#Y#Z2k~2nP#h#i2q~2vOu~~2yR#X#Y3S#c#d3e#g#h3v~3VP#l#m3Y~3]P#h#i3`~3eO^~~3hP#a#b3k~3nP#`#a3q~3vOn~~3{Ot~~4OP#i#j4R~4UP#X#Y4X~4^O!O~~4aP#T#U4d~4gP#g#h4j~4mP#h#i4p~4uO!R~~4xP#a#b4{~5OP#`#a5R~5WOg~~5ZP#T#U5^~5aP#a#b5d~5gP#`#a5j~5oOm~", tokenizers: [blockContent, 0, 1], topRules: {"Document":[0,2]}, tokenPrec: 0 diff --git a/frontend/src/views/editor/extensions/codeblock/parser.test.ts b/frontend/src/views/editor/extensions/codeblock/parser.test.ts index 9dd00b6c..329fa979 100644 --- a/frontend/src/views/editor/extensions/codeblock/parser.test.ts +++ b/frontend/src/views/editor/extensions/codeblock/parser.test.ts @@ -36,4 +36,12 @@ describe('codeblock delimiter access', () => { expect(blocks[1]?.access).toBe('write'); expect(blocks[1]?.language).toEqual({ name: 'json', auto: true }); }); + + it('accepts image as a valid block language', () => { + expect(parseDelimiter(createDelimiter('image', false, 'write'))).toEqual({ + language: 'image', + auto: false, + access: 'write', + }); + }); }); diff --git a/frontend/src/views/editor/extensions/codeblock/types.ts b/frontend/src/views/editor/extensions/codeblock/types.ts index 740a4a38..948bed54 100644 --- a/frontend/src/views/editor/extensions/codeblock/types.ts +++ b/frontend/src/views/editor/extensions/codeblock/types.ts @@ -81,7 +81,8 @@ export type SupportedLanguage = | 'angular' | 'svelte' | 'http' - | 'mermaid'; + | 'mermaid' + | 'image'; /** * 创建块的选项 diff --git a/frontend/src/views/editor/extensions/contextMenu/blockContext.test.ts b/frontend/src/views/editor/extensions/contextMenu/blockContext.test.ts new file mode 100644 index 00000000..136bb65c --- /dev/null +++ b/frontend/src/views/editor/extensions/contextMenu/blockContext.test.ts @@ -0,0 +1,58 @@ +import {EditorState} from '@codemirror/state'; +import {describe, expect, it, vi} from 'vitest'; +vi.mock('../codeblock/lang-parser/languages', () => ({ + LANGUAGES: [ + {token: 'text'}, + {token: 'image'}, + ], +})); + +import {getActiveNoteBlock} from '../codeblock/state'; +import {createDelimiter} from '../codeblock/parser'; +import {blockState} from '../codeblock/state'; +import {runCommandInMenuBlock} from './blockContext'; +import type {MenuContext} from './menuSchema'; + +describe('runCommandInMenuBlock', () => { + it('executes the command against the block targeted by the context menu', () => { + let state = EditorState.create({ + doc: [ + createDelimiter('text', false, 'write'), + 'first block\n', + createDelimiter('image', false, 'write'), + 'img(ref="sha256-1", src="/media/a.png")', + ].join(''), + selection: {anchor: 1}, + extensions: [blockState], + }); + + const blocks = state.field(blockState); + const targetBlock = blocks[1]; + const command = vi.fn(view => getActiveNoteBlock(view.state)?.language.name === 'image'); + const wrapped = runCommandInMenuBlock(command); + + const view = { + get state() { + return state; + }, + dispatch(spec: Parameters[0]) { + state = state.update(spec).state; + }, + }; + + const context: MenuContext = { + view: view as never, + event: {} as MouseEvent, + targetPos: targetBlock.content.from, + targetBlock, + hasSelection: false, + selectionText: '', + isEditable: true, + }; + + expect(wrapped(view as never, context)).toBe(true); + expect(command).toHaveBeenCalledOnce(); + expect(getActiveNoteBlock(state)?.language.name).toBe('image'); + expect(state.selection.main.head).toBe(targetBlock.content.from); + }); +}); diff --git a/frontend/src/views/editor/extensions/contextMenu/blockContext.ts b/frontend/src/views/editor/extensions/contextMenu/blockContext.ts new file mode 100644 index 00000000..77c779f7 --- /dev/null +++ b/frontend/src/views/editor/extensions/contextMenu/blockContext.ts @@ -0,0 +1,40 @@ +import {EditorSelection, Transaction} from '@codemirror/state'; +import type {Command, EditorView} from '@codemirror/view'; +import {getNoteBlockFromPos} from '../codeblock/state'; +import type {Block} from '../codeblock/types'; +import type {MenuContext} from './menuSchema'; + +function containsPos(block: Block, pos: number): boolean { + return block.range.from <= pos && pos <= block.range.to; +} + +function getCurrentMenuBlock(view: EditorView, context: MenuContext): Block | null { + if (context.targetPos === null) { + return context.targetBlock; + } + + return getNoteBlockFromPos(view.state, Math.min(context.targetPos, view.state.doc.length)) ?? null; +} + +export function getMenuBlock(context: MenuContext): Block | null { + return context.targetBlock; +} + +export function runCommandInMenuBlock(command: Command) { + return (view: EditorView, context: MenuContext): boolean => { + const block = getCurrentMenuBlock(view, context); + if (!block) { + return false; + } + + const currentHead = view.state.selection.main.head; + if (!containsPos(block, currentHead)) { + view.dispatch({ + selection: EditorSelection.cursor(block.content.from), + annotations: [Transaction.addToHistory.of(false)], + }); + } + + return command(view); + }; +} diff --git a/frontend/src/views/editor/extensions/contextMenu/constants.ts b/frontend/src/views/editor/extensions/contextMenu/constants.ts new file mode 100644 index 00000000..b945c6be --- /dev/null +++ b/frontend/src/views/editor/extensions/contextMenu/constants.ts @@ -0,0 +1,2 @@ +export const CONTEXT_MENU_BLOCK_ANCHOR_DATASET = 'blockAnchor'; +export const CONTEXT_MENU_BLOCK_ANCHOR_SELECTOR = '[data-block-anchor]'; diff --git a/frontend/src/views/editor/extensions/contextMenu/menuSchema.ts b/frontend/src/views/editor/extensions/contextMenu/menuSchema.ts index 88b1adfa..29cf9bed 100644 --- a/frontend/src/views/editor/extensions/contextMenu/menuSchema.ts +++ b/frontend/src/views/editor/extensions/contextMenu/menuSchema.ts @@ -1,21 +1,28 @@ -import type { EditorView } from '@codemirror/view'; +import type {Command, EditorView} from '@codemirror/view'; import { EditorState } from '@codemirror/state'; +import {getNoteBlockFromPos} from '../codeblock/state'; +import type {Block} from '../codeblock/types'; +import {CONTEXT_MENU_BLOCK_ANCHOR_DATASET, CONTEXT_MENU_BLOCK_ANCHOR_SELECTOR} from './constants'; import { KeyBindingName } from '@/../bindings/voidraft/internal/models/models'; export interface MenuContext { view: EditorView; event: MouseEvent; + targetPos: number | null; + targetBlock: Block | null; hasSelection: boolean; selectionText: string; isEditable: boolean; } +export type MenuCommand = (view: EditorView, context: MenuContext) => boolean; + export type MenuSchemaNode = | { id: string; type?: "action"; labelKey: string; - command?: (view: EditorView) => boolean; + command?: Command | MenuCommand; keyBindingName?: KeyBindingName; visible?: (context: MenuContext) => boolean; enabled?: (context: MenuContext) => boolean; @@ -42,8 +49,48 @@ interface MenuBuildOptions { const menuRegistry: MenuSchemaNode[] = []; +function getAnchorPosFromTarget(target: EventTarget | null): number | null { + if (!(target instanceof HTMLElement)) { + return null; + } + + const anchor = target.closest(CONTEXT_MENU_BLOCK_ANCHOR_SELECTOR)?.dataset[CONTEXT_MENU_BLOCK_ANCHOR_DATASET]; + if (!anchor) { + return null; + } + + const pos = Number(anchor); + return Number.isInteger(pos) && pos >= 0 ? pos : null; +} + +function getTargetPos(view: EditorView, event: MouseEvent): number | null { + const anchorPos = getAnchorPosFromTarget(event.target); + if (anchorPos !== null) { + return anchorPos; + } + + const posAtCoords = view.posAtCoords({ + x: event.clientX, + y: event.clientY, + }); + if (posAtCoords !== null) { + return posAtCoords; + } + + if (!(event.target instanceof Node) || !view.dom.contains(event.target)) { + return null; + } + + try { + return view.posAtDOM(event.target, 0); + } catch { + return null; + } +} + export function createMenuContext(view: EditorView, event: MouseEvent): MenuContext { const { state } = view; + const targetPos = getTargetPos(view, event); const hasSelection = state.selection.ranges.some((range) => !range.empty); const selectionText = hasSelection ? state.sliceDoc(state.selection.main.from, state.selection.main.to) @@ -53,6 +100,8 @@ export function createMenuContext(view: EditorView, event: MouseEvent): MenuCont return { view, event, + targetPos, + targetBlock: targetPos !== null ? getNoteBlockFromPos(state, targetPos) ?? null : null, hasSelection, selectionText, isEditable @@ -90,6 +139,9 @@ function convertNode( const disabled = node.enabled ? !node.enabled(context) : false; const shortcut = options.formatShortcut(node.keyBindingName); + const command = node.command + ? (view: EditorView) => (node.command as (view: EditorView, context: MenuContext) => boolean)(view, context) + : undefined; return { id: node.id, @@ -97,6 +149,6 @@ function convertNode( label: options.translate(node.labelKey), shortcut: shortcut || undefined, disabled, - command: node.command + command }; } diff --git a/frontend/src/views/editor/extensions/imageblock/actions.ts b/frontend/src/views/editor/extensions/imageblock/actions.ts new file mode 100644 index 00000000..d5a5b1eb --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/actions.ts @@ -0,0 +1,46 @@ +import * as MediaHTTPService from '@/../bindings/voidraft/internal/services/mediahttpservice'; +import {writeImageToClipboard} from '@/views/editor/extensions/blockImage/clipboard'; +import {getNoteBlockFromPos} from '@/views/editor/extensions/codeblock/state'; +import {IMAGE_BLOCK_LANGUAGE} from './constants'; +import {getImageBlockItems} from './syntax'; +import type {ImageBlockItem} from './types'; +import {removeImageItemFromBlock} from './document'; +import type {EditorView} from '@codemirror/view'; + +function getClipboardMimeType(blob: Blob): string { + return blob.type.startsWith('image/') ? blob.type : 'image/png'; +} + +async function fetchImageBlob(item: ImageBlockItem): Promise { + const response = await fetch(item.src); + if (!response.ok) { + throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`); + } + + return response.blob(); +} + +export async function copyImageItemToClipboard(item: ImageBlockItem): Promise { + const blob = await fetchImageBlob(item); + await writeImageToClipboard(blob, getClipboardMimeType(blob)); +} + +export async function deleteImageItem(view: EditorView, anchor: number, itemIndex: number): Promise { + const block = getNoteBlockFromPos(view.state, anchor); + if (!block || block.language.name !== IMAGE_BLOCK_LANGUAGE) { + return false; + } + + const items = getImageBlockItems(view.state, block); + const item = items[itemIndex]; + if (!item) { + return false; + } + + const result = await MediaHTTPService.DeleteImage(item.ref); + if (!result?.deleted) { + return false; + } + + return removeImageItemFromBlock(view, block, itemIndex); +} diff --git a/frontend/src/views/editor/extensions/imageblock/clipboard.ts b/frontend/src/views/editor/extensions/imageblock/clipboard.ts new file mode 100644 index 00000000..f84b82a1 --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/clipboard.ts @@ -0,0 +1,159 @@ +import * as MediaHTTPService from '@/../bindings/voidraft/internal/services/mediahttpservice'; +import type {ImageAsset} from '@/../bindings/voidraft/internal/services/models'; +import {EditorView} from '@codemirror/view'; +import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state'; +import {createImageBlockItemFromAsset, insertImageBlockFromClipboard, insertImageItemsIntoBlock} from './document'; +import {getSelectedImageBlock} from './selection'; +import {IMAGE_BLOCK_LANGUAGE} from './constants'; + +type ClipboardImageInput = { + blob: Blob; + filename?: string; + mimeType?: string; +}; + +function getClipboardImageFiles(event: ClipboardEvent): ClipboardImageInput[] { + const files: ClipboardImageInput[] = []; + const items = event.clipboardData?.items; + if (!items) { + return files; + } + + for (let index = 0; index < items.length; index += 1) { + const item = items[index]; + if (item.kind !== 'file' || !item.type.startsWith('image/')) { + continue; + } + + const file = item.getAsFile(); + if (file) { + files.push({ + blob: file, + filename: file.name || undefined, + mimeType: file.type || undefined, + }); + } + } + + return files; +} + +async function readSystemClipboardImageFiles(): Promise { + if (!navigator.clipboard?.read) { + return []; + } + + const clipboardItems = await navigator.clipboard.read(); + const files: ClipboardImageInput[] = []; + + for (const item of clipboardItems) { + for (const type of item.types) { + if (!type.startsWith('image/')) { + continue; + } + + const blob = await item.getType(type); + files.push({ + blob, + filename: blob instanceof File && blob.name ? blob.name : undefined, + mimeType: type || blob.type || undefined, + }); + break; + } + } + + return files; +} + +function readBlobAsDataURL(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = () => reject(reader.error || new Error('Failed to read clipboard image')); + reader.onload = () => resolve(String(reader.result || '')); + reader.readAsDataURL(blob); + }); +} + +function normalizeClipboardFilename(filename?: string): string | undefined { + const normalized = filename?.trim(); + return normalized ? normalized : undefined; +} + +async function importClipboardImages(files: readonly ClipboardImageInput[]): Promise { + const assets: ImageAsset[] = []; + + for (const file of files) { + const dataURL = await readBlobAsDataURL(file.blob); + const asset = await MediaHTTPService.ImportImage({ + filename: normalizeClipboardFilename(file.filename), + mime_type: file.mimeType || file.blob.type || undefined, + data_base64: dataURL, + }); + + if (asset) { + assets.push(asset); + } + } + + return assets; +} + +async function pasteClipboardImages(view: EditorView, files: readonly ClipboardImageInput[]): Promise { + try { + const assets = await importClipboardImages(files); + if (assets.length === 0) { + return false; + } + + const items = assets.map(createImageBlockItemFromAsset); + const selected = getSelectedImageBlock(view.state); + if (selected) { + insertImageItemsIntoBlock(view, selected.block, items, selected.itemIndex); + return true; + } + + const activeBlock = getActiveNoteBlock(view.state); + if (activeBlock?.language.name === IMAGE_BLOCK_LANGUAGE) { + insertImageItemsIntoBlock(view, activeBlock, items, null); + return true; + } + + insertImageBlockFromClipboard(view, items); + return true; + } catch (error) { + console.error('[imageblock] Failed to import clipboard image:', error); + return false; + } +} + +export function handleImagePasteEvent(view: EditorView, event: ClipboardEvent): boolean { + if (view.state.readOnly) { + return false; + } + + const files = getClipboardImageFiles(event); + if (files.length === 0) { + return false; + } + + event.preventDefault(); + void pasteClipboardImages(view, files); + return true; +} + +export async function pasteImagesFromClipboard(view: EditorView): Promise { + if (view.state.readOnly) { + return false; + } + + try { + const files = await readSystemClipboardImageFiles(); + if (files.length === 0) { + return false; + } + + return pasteClipboardImages(view, files); + } catch { + return false; + } +} diff --git a/frontend/src/views/editor/extensions/imageblock/constants.ts b/frontend/src/views/editor/extensions/imageblock/constants.ts new file mode 100644 index 00000000..c8d85d5c --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/constants.ts @@ -0,0 +1,3 @@ +export const IMAGE_BLOCK_LANGUAGE = 'image'; +export const TARGET_PREVIEW_HEIGHT = 160; +export const MAX_PREVIEW_WIDTH = 320; diff --git a/frontend/src/views/editor/extensions/imageblock/document.test.ts b/frontend/src/views/editor/extensions/imageblock/document.test.ts new file mode 100644 index 00000000..1510e764 --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/document.test.ts @@ -0,0 +1,113 @@ +import {EditorState} from '@codemirror/state'; +import {describe, expect, it, vi} from 'vitest'; + +vi.mock('@/views/editor/extensions/codeblock/lang-parser/languages', async () => { + const {imageLanguage} = await import('@/views/editor/extensions/imageblock/language/image-language'); + + return { + LANGUAGES: [ + {token: 'text', parser: null}, + {token: 'image', parser: imageLanguage.parser}, + ], + languageMapping: { + image: imageLanguage.parser, + }, + }; +}); + +import {getCodeBlockLanguageExtension} from '@/views/editor/extensions/codeblock/lang-parser'; +import {createDelimiter} from '@/views/editor/extensions/codeblock/parser'; +import {blockState} from '@/views/editor/extensions/codeblock/state'; +import {moveImageItemInBlock, removeImageItemFromBlock} from './document'; +import {imageBlockSelectionField} from './selection'; + +function createView(state: EditorState) { + return { + get state() { + return state; + }, + dispatch(spec: Parameters[0]) { + state = state.update(spec).state; + }, + }; +} + +describe('removeImageItemFromBlock', () => { + it('removes one image record and keeps the block', () => { + let state = EditorState.create({ + doc: [ + createDelimiter('image', false, 'write'), + 'img(ref="sha256-1", src="/media/a.png")\n', + 'img(ref="sha256-2", src="/media/b.png")', + ].join(''), + extensions: [ + blockState, + imageBlockSelectionField, + ...getCodeBlockLanguageExtension(), + ], + }); + + const block = state.field(blockState)[0]; + const view = createView(state); + + expect(removeImageItemFromBlock(view as never, block, 0)).toBe(true); + expect(view.state.doc.toString()).toContain('sha256-2'); + expect(view.state.doc.toString()).not.toContain('sha256-1'); + expect(view.state.field(blockState)).toHaveLength(1); + expect(view.state.field(imageBlockSelectionField)?.itemIndex).toBe(0); + }); + + it('removes the whole image block when the last image is deleted', () => { + let state = EditorState.create({ + doc: [ + createDelimiter('text', false, 'write'), + 'before\n', + createDelimiter('image', false, 'write'), + 'img(ref="sha256-1", src="/media/a.png")', + ].join(''), + extensions: [ + blockState, + imageBlockSelectionField, + ...getCodeBlockLanguageExtension(), + ], + }); + + const block = state.field(blockState)[1]; + const view = createView(state); + + expect(removeImageItemFromBlock(view as never, block, 0)).toBe(true); + expect(view.state.doc.toString()).not.toContain('sha256-1'); + expect(view.state.field(blockState)).toHaveLength(1); + expect(view.state.field(blockState)[0].language.name).toBe('text'); + expect(view.state.field(imageBlockSelectionField, false)).toBeNull(); + }); +}); + +describe('moveImageItemInBlock', () => { + it('moves an image record within the current block', () => { + let state = EditorState.create({ + doc: [ + createDelimiter('image', false, 'write'), + 'img(ref="sha256-1", src="/media/a.png")\n', + 'img(ref="sha256-2", src="/media/b.png")\n', + 'img(ref="sha256-3", src="/media/c.png")', + ].join(''), + extensions: [ + blockState, + imageBlockSelectionField, + ...getCodeBlockLanguageExtension(), + ], + }); + + const block = state.field(blockState)[0]; + const view = createView(state); + + expect(moveImageItemInBlock(view as never, block, 0, 3)).toBe(true); + expect(view.state.doc.toString()).toContain([ + 'img(ref="sha256-2", src="/media/b.png")', + 'img(ref="sha256-3", src="/media/c.png")', + 'img(ref="sha256-1", src="/media/a.png")', + ].join('\n')); + expect(view.state.field(imageBlockSelectionField)?.itemIndex).toBe(2); + }); +}); diff --git a/frontend/src/views/editor/extensions/imageblock/document.ts b/frontend/src/views/editor/extensions/imageblock/document.ts new file mode 100644 index 00000000..6b79aa7f --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/document.ts @@ -0,0 +1,236 @@ +import type {ImageAsset} from '@/../bindings/voidraft/internal/services/models'; +import type {EditorView} from '@codemirror/view'; +import {CONTENT_EDIT, USER_EVENTS, codeBlockEvent} from '@/views/editor/extensions/codeblock/annotation'; +import {createDelimiter} from '@/views/editor/extensions/codeblock/parser'; +import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state'; +import type {Block} from '@/views/editor/extensions/codeblock/types'; +import {clearImageBlockSelection, setImageBlockSelection} from './selection'; +import {getImageBlockItems} from './syntax'; +import type {ImageBlockItem} from './types'; + +function quoteAttribute(value: string): string { + return JSON.stringify(value); +} + +function pushOptionalString(attrs: string[], key: string, value?: string) { + if (!value) { + return; + } + attrs.push(`${key}=${quoteAttribute(value)}`); +} + +function pushOptionalNumber(attrs: string[], key: string, value?: number) { + if (!value || value <= 0) { + return; + } + attrs.push(`${key}=${value}`); +} + +export function createImageBlockItemFromAsset(asset: ImageAsset): ImageBlockItem { + return { + ref: asset.id, + src: asset.url, + width: asset.width > 0 ? asset.width : undefined, + height: asset.height > 0 ? asset.height : undefined, + }; +} + +export function serializeImageBlockItem(item: ImageBlockItem): string { + const attrs = [ + `ref=${quoteAttribute(item.ref)}`, + `src=${quoteAttribute(item.src)}`, + ]; + + pushOptionalNumber(attrs, 'width', item.width); + pushOptionalNumber(attrs, 'height', item.height); + pushOptionalString(attrs, 'alt', item.alt); + pushOptionalString(attrs, 'title', item.title); + + return `img(${attrs.join(', ')})`; +} + +export function serializeImageBlockContent(items: readonly ImageBlockItem[]): string { + return items.map(serializeImageBlockItem).join('\n'); +} + +export function insertImageItemsIntoBlock( + view: EditorView, + block: Block, + itemsToInsert: readonly ImageBlockItem[], + afterItemIndex: number | null, +): boolean { + if (itemsToInsert.length === 0 || view.state.readOnly) { + return false; + } + + const currentItems = getImageBlockItems(view.state, block); + const insertAt = afterItemIndex === null + ? currentItems.length + : Math.min(currentItems.length, afterItemIndex + 1); + const nextItems = [ + ...currentItems.slice(0, insertAt), + ...itemsToInsert, + ...currentItems.slice(insertAt), + ]; + + view.dispatch({ + changes: { + from: block.content.from, + to: block.content.to, + insert: serializeImageBlockContent(nextItems), + }, + effects: [ + setImageBlockSelection.of({ + anchor: block.content.from, + itemIndex: insertAt + itemsToInsert.length - 1, + }), + ], + scrollIntoView: true, + userEvent: USER_EVENTS.INPUT_PASTE, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], + }); + + return true; +} + +export function removeImageItemFromBlock( + view: EditorView, + block: Block, + itemIndex: number, +): boolean { + if (view.state.readOnly) { + return false; + } + + const currentItems = getImageBlockItems(view.state, block); + if (itemIndex < 0 || itemIndex >= currentItems.length) { + return false; + } + + const nextItems = currentItems.filter((_, index) => index !== itemIndex); + if (nextItems.length === 0) { + view.dispatch({ + changes: { + from: block.range.from, + to: block.range.to, + insert: '', + }, + effects: [clearImageBlockSelection.of()], + scrollIntoView: true, + userEvent: USER_EVENTS.DELETE, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], + }); + return true; + } + + const nextSelectedIndex = Math.min(itemIndex, nextItems.length - 1); + view.dispatch({ + changes: { + from: block.content.from, + to: block.content.to, + insert: serializeImageBlockContent(nextItems), + }, + effects: [ + setImageBlockSelection.of({ + anchor: block.content.from, + itemIndex: nextSelectedIndex, + }), + ], + scrollIntoView: true, + userEvent: USER_EVENTS.DELETE, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], + }); + + return true; +} + +export function moveImageItemInBlock( + view: EditorView, + block: Block, + fromIndex: number, + toIndex: number, +): boolean { + if (view.state.readOnly) { + return false; + } + + const currentItems = getImageBlockItems(view.state, block); + if ( + fromIndex < 0 + || fromIndex >= currentItems.length + || toIndex < 0 + || toIndex > currentItems.length + ) { + return false; + } + + const nextIndex = toIndex > fromIndex ? toIndex - 1 : toIndex; + if (nextIndex === fromIndex) { + return false; + } + + const nextItems = [...currentItems]; + const [movedItem] = nextItems.splice(fromIndex, 1); + if (!movedItem) { + return false; + } + nextItems.splice(nextIndex, 0, movedItem); + + view.dispatch({ + changes: { + from: block.content.from, + to: block.content.to, + insert: serializeImageBlockContent(nextItems), + }, + effects: [ + setImageBlockSelection.of({ + anchor: block.content.from, + itemIndex: nextIndex, + }), + ], + scrollIntoView: true, + userEvent: USER_EVENTS.MOVE, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], + }); + + return true; +} + +export function insertImageBlockAfterBlock( + view: EditorView, + block: Block, + items: readonly ImageBlockItem[], +): boolean { + if (items.length === 0 || view.state.readOnly) { + return false; + } + + const delimiter = createDelimiter('image', false, 'write'); + const content = serializeImageBlockContent(items); + + view.dispatch({ + changes: { + from: block.content.to, + insert: `${delimiter}${content}`, + }, + effects: [clearImageBlockSelection.of()], + scrollIntoView: true, + userEvent: USER_EVENTS.INPUT_PASTE, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], + }); + + return true; +} + +export function insertImageBlockFromClipboard(view: EditorView, items: readonly ImageBlockItem[]): boolean { + if (items.length === 0 || view.state.readOnly) { + return false; + } + + const activeBlock = getActiveNoteBlock(view.state); + if (!activeBlock) { + return false; + } + + return insertImageBlockAfterBlock(view, activeBlock, items); +} diff --git a/frontend/src/views/editor/extensions/imageblock/drag.ts b/frontend/src/views/editor/extensions/imageblock/drag.ts new file mode 100644 index 00000000..703bd414 --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/drag.ts @@ -0,0 +1,211 @@ +import type {EditorView} from '@codemirror/view'; +import {getNoteBlockFromPos} from '@/views/editor/extensions/codeblock/state'; +import {IMAGE_BLOCK_LANGUAGE} from './constants'; +import {moveImageItemInBlock} from './document'; + +const IMAGE_BLOCK_ITEM_SELECTOR = '.cm-image-block-item'; +const IMAGE_BLOCK_DRAG_HANDLE_SELECTOR = '.cm-image-block-drag-handle'; +const DROP_BEFORE_CLASS = 'is-drop-before'; +const DROP_AFTER_CLASS = 'is-drop-after'; +const DRAGGING_CLASS = 'is-dragging'; +const ROW_GROUP_TOLERANCE = 8; + +interface FrameLayout { + frame: HTMLElement; + index: number; + rect: DOMRect; +} + +interface RowLayout { + entries: FrameLayout[]; + centerY: number; +} + +interface DropPlacement { + index: number; + markerFrame: HTMLElement; + markerSide: typeof DROP_BEFORE_CLASS | typeof DROP_AFTER_CLASS; +} + +function getFrames(flow: HTMLElement): HTMLElement[] { + return Array.from(flow.querySelectorAll(IMAGE_BLOCK_ITEM_SELECTOR)); +} + +function getFrameIndex(frame: HTMLElement): number | null { + const index = Number(frame.dataset.itemIndex); + return Number.isInteger(index) ? index : null; +} + +function buildRows(frames: readonly HTMLElement[]): RowLayout[] { + const rows: FrameLayout[][] = []; + + frames.forEach((frame, index) => { + const rect = frame.getBoundingClientRect(); + const entry = {frame, index, rect}; + const currentRow = rows.at(-1); + + if (!currentRow || Math.abs(currentRow[0].rect.top - rect.top) > ROW_GROUP_TOLERANCE) { + rows.push([entry]); + return; + } + + currentRow.push(entry); + }); + + return rows.map(entries => ({ + entries, + centerY: entries.reduce((sum, entry) => sum + entry.rect.top + entry.rect.height / 2, 0) / entries.length, + })); +} + +function resolveDropPlacement( + frames: readonly HTMLElement[], + clientX: number, + clientY: number, +): DropPlacement | null { + if (frames.length === 0) { + return null; + } + + const rows = buildRows(frames); + const targetRow = rows.reduce((closest, row) => { + if (!closest) { + return row; + } + + return Math.abs(row.centerY - clientY) < Math.abs(closest.centerY - clientY) ? row : closest; + }, rows[0] as RowLayout); + + const targetEntry = targetRow.entries.find(entry => clientX < entry.rect.left + entry.rect.width / 2); + if (targetEntry) { + return { + index: targetEntry.index, + markerFrame: targetEntry.frame, + markerSide: DROP_BEFORE_CLASS, + }; + } + + const lastEntry = targetRow.entries[targetRow.entries.length - 1]; + return { + index: lastEntry.index + 1, + markerFrame: lastEntry.frame, + markerSide: DROP_AFTER_CLASS, + }; +} + +function clearMarker(frame: HTMLElement | null, side: DropPlacement['markerSide'] | null) { + if (!frame || !side) { + return; + } + + frame.classList.remove(side); +} + +export function attachImageBlockDrag( + flow: HTMLElement, + view: EditorView, + anchor: number, +): void { + const frames = getFrames(flow); + frames.forEach((frame, index) => { + frame.dataset.itemIndex = String(index); + frame.draggable = false; + const handle = frame.querySelector(IMAGE_BLOCK_DRAG_HANDLE_SELECTOR); + if (handle) { + handle.draggable = true; + } + }); + + let sourceIndex: number | null = null; + let sourceFrame: HTMLElement | null = null; + let dropPlacement: DropPlacement | null = null; + + const updatePlacement = (nextPlacement: DropPlacement | null) => { + if ( + dropPlacement?.index === nextPlacement?.index + && dropPlacement?.markerFrame === nextPlacement?.markerFrame + && dropPlacement?.markerSide === nextPlacement?.markerSide + ) { + return; + } + + clearMarker(dropPlacement?.markerFrame ?? null, dropPlacement?.markerSide ?? null); + dropPlacement = nextPlacement; + if (dropPlacement) { + dropPlacement.markerFrame.classList.add(dropPlacement.markerSide); + } + }; + + const resetDragState = () => { + updatePlacement(null); + sourceFrame?.classList.remove(DRAGGING_CLASS); + sourceFrame = null; + sourceIndex = null; + }; + + flow.addEventListener('dragstart', event => { + const target = event.target as HTMLElement | null; + const handle = target?.closest(IMAGE_BLOCK_DRAG_HANDLE_SELECTOR); + const frame = handle?.closest(IMAGE_BLOCK_ITEM_SELECTOR); + if (!frame || !flow.contains(frame)) { + return; + } + if (!frame.classList.contains('is-selected') || frame.classList.contains('is-busy')) { + event.preventDefault(); + return; + } + + const index = getFrameIndex(frame); + if (index === null) { + return; + } + + sourceIndex = index; + sourceFrame = frame; + frame.classList.add(DRAGGING_CLASS); + + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = 'move'; + event.dataTransfer.setData('text/plain', String(index)); + } + }); + + flow.addEventListener('dragover', event => { + if (sourceIndex === null) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'move'; + } + + updatePlacement(resolveDropPlacement(getFrames(flow), event.clientX, event.clientY)); + }); + + flow.addEventListener('drop', event => { + if (sourceIndex === null) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + const currentSourceIndex = sourceIndex; + const block = getNoteBlockFromPos(view.state, anchor); + const placement = dropPlacement ?? resolveDropPlacement(getFrames(flow), event.clientX, event.clientY); + resetDragState(); + + if (!block || block.language.name !== IMAGE_BLOCK_LANGUAGE || !placement) { + return; + } + + moveImageItemInBlock(view, block, currentSourceIndex, placement.index); + }); + + flow.addEventListener('dragend', () => { + resetDragState(); + }); +} diff --git a/frontend/src/views/editor/extensions/imageblock/extension.ts b/frontend/src/views/editor/extensions/imageblock/extension.ts new file mode 100644 index 00000000..5a6f859f --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/extension.ts @@ -0,0 +1,10 @@ +import type {Extension} from '@codemirror/state'; +import {getImageBlockRenderExtensions} from './render'; +import {imageBlockSelectionField} from './selection'; + +export function getImageBlockExtensions(): Extension[] { + return [ + imageBlockSelectionField, + ...getImageBlockRenderExtensions(), + ]; +} diff --git a/frontend/src/views/editor/extensions/imageblock/index.ts b/frontend/src/views/editor/extensions/imageblock/index.ts new file mode 100644 index 00000000..a28c61b3 --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/index.ts @@ -0,0 +1,3 @@ +export {getImageBlockExtensions} from './extension'; +export * from './types'; +export * from './language'; diff --git a/frontend/src/views/editor/extensions/imageblock/language/build-parser.js b/frontend/src/views/editor/extensions/imageblock/language/build-parser.js new file mode 100644 index 00000000..6cf251d2 --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/language/build-parser.js @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +import { execSync } from 'child_process'; +import path from 'path'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +console.log('Building image grammar parser...'); + +try { + const grammarFile = path.join(__dirname, 'image.grammar'); + if (!fs.existsSync(grammarFile)) { + throw new Error('Grammar file image.grammar not found'); + } + + execSync('npx lezer-generator image.grammar -o image.parser.ts --typeScript', { + cwd: __dirname, + stdio: 'inherit', + }); + + const parserFile = path.join(__dirname, 'image.parser.ts'); + const termsFile = path.join(__dirname, 'image.parser.terms.ts'); + + if (!fs.existsSync(parserFile) || !fs.existsSync(termsFile)) { + throw new Error('Failed to generate image parser artifacts'); + } + + console.log('Generated image.parser.ts and image.parser.terms.ts'); +} catch (error) { + console.error('Failed to build image grammar parser:', error instanceof Error ? error.message : error); + process.exit(1); +} diff --git a/frontend/src/views/editor/extensions/imageblock/language/image-language.ts b/frontend/src/views/editor/extensions/imageblock/language/image-language.ts new file mode 100644 index 00000000..509d5c25 --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/language/image-language.ts @@ -0,0 +1,17 @@ +import { LRLanguage, LanguageSupport } from '@codemirror/language'; +import { parser } from './image.parser'; +import { imageHighlighting } from './image.highlight'; + +export const imageLanguage = LRLanguage.define({ + parser: parser.configure({ + props: [imageHighlighting], + }), + languageData: { + closeBrackets: { brackets: ['(', '"', "'"] }, + wordChars: '-_', + }, +}); + +export function image() { + return new LanguageSupport(imageLanguage); +} diff --git a/frontend/src/views/editor/extensions/imageblock/language/image.grammar b/frontend/src/views/editor/extensions/imageblock/language/image.grammar new file mode 100644 index 00000000..e3c876b3 --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/language/image.grammar @@ -0,0 +1,112 @@ +// Image block grammar +// +// Example: +// img(ref="media-id-1", src="/media/2026/04/a.png", alt="封面", width=320, height=180, title="封面图") +// img(ref="media-id-2", src="/media/2026/04/b.png", alt="流程图", width=240, height=240) +// img(ref="media-id-3", src="https://example.com/demo.png", alt="外链图") + +@skip { whitespace | LineComment } + +@top Document { ImageElement* } + +ImgKeyword { + @specialize[@name=ImgKeyword] +} + +ImageElement { + ImgKeyword ImageConfig +} + +ImageConfig { + "(" RequiredAttributePair ")" | + "(" RequiredAttributePair "," OptionalAttributeList ")" +} + +RequiredAttributePair { + RefAttribute "," SrcAttribute | + SrcAttribute "," RefAttribute +} + +OptionalAttribute { + WidthAttribute | + HeightAttribute | + AltAttribute | + TitleAttribute +} + +OptionalAttributeList { + OptionalAttribute ("," OptionalAttribute)* +} + +RefAttribute { + RefAttributeName "=" StringLiteral +} + +SrcAttribute { + SrcAttributeName "=" StringLiteral +} + +WidthAttribute { + WidthAttributeName "=" NumberLiteral +} + +HeightAttribute { + HeightAttributeName "=" NumberLiteral +} + +AltAttribute { + AltAttributeName "=" StringLiteral +} + +TitleAttribute { + TitleAttributeName "=" StringLiteral +} + +RefAttributeName { + @specialize[@name=RefAttributeName] +} + +SrcAttributeName { + @specialize[@name=SrcAttributeName] +} + +WidthAttributeName { + @specialize[@name=WidthAttributeName] +} + +HeightAttributeName { + @specialize[@name=HeightAttributeName] +} + +AltAttributeName { + @specialize[@name=AltAttributeName] +} + +TitleAttributeName { + @specialize[@name=TitleAttributeName] +} + +@tokens { + LineComment { "#" ![\n]* } + + identifier { + (@asciiLetter | $[_$]) + (@asciiLetter | @digit | $[_$])* + } + + whitespace { @whitespace+ } + + NumberLiteral { + ("+" | "-")? (@digit+ ("." @digit*)? | "." @digit+) + (("e" | "E") ("+" | "-")? @digit+)? + } + + StringLiteral[isolate] { + "\"" (!["\n\\] | "\\" _)* "\"" | + "'" (!['\n\\] | "\\" _)* "'" + } + + "=" "," "(" ")" +} + +@detectDelim diff --git a/frontend/src/views/editor/extensions/imageblock/language/image.grammar.test.ts b/frontend/src/views/editor/extensions/imageblock/language/image.grammar.test.ts new file mode 100644 index 00000000..ece73d68 --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/language/image.grammar.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from 'vitest'; +import { parser } from './image.parser'; + +function parseCode(code: string) { + return parser.parse(code); +} + +function getErrorNodes(tree: any, code: string) { + const errors: Array<{ from: number; to: number; text: string }> = []; + + tree.iterate({ + enter: (node: any) => { + if (node.type?.isError || node.name === '⚠') { + errors.push({ + from: node.from, + to: node.to, + text: code.slice(node.from, node.to), + }); + } + }, + }); + + return errors; +} + +function countNodes(tree: any, name: string) { + let count = 0; + + tree.iterate({ + enter: (node: any) => { + if (node.name === name) { + count += 1; + } + }, + }); + + return count; +} + +describe('image grammar', () => { + it('parses flat img records with required ref and src', () => { + const code = `img(ref="media-id-1", src="/media/2026/04/a.png", alt="封面", width=320, height=180, title="封面图") +img(ref="media-id-2", src="/media/2026/04/b.png", alt="流程图", width=240, height=240) +img(ref="media-id-3", src="https://example.com/demo.png", alt="外链图")`; + + const tree = parseCode(code); + const errors = getErrorNodes(tree, code); + + expect(errors).toHaveLength(0); + expect(countNodes(tree, 'ImageElement')).toBe(3); + expect(countNodes(tree, 'RefAttribute')).toBe(3); + expect(countNodes(tree, 'SrcAttribute')).toBe(3); + expect(countNodes(tree, 'WidthAttribute')).toBe(2); + expect(countNodes(tree, 'HeightAttribute')).toBe(2); + expect(countNodes(tree, 'AltAttribute')).toBe(3); + expect(countNodes(tree, 'TitleAttribute')).toBe(1); + }); + + it('supports required attributes in either order', () => { + const code = `img(src="/media/2026/04/a.png", ref="media-id-1", alt="封面") +img(ref="media-id-2", src="/media/2026/04/b.png")`; + + const tree = parseCode(code); + const errors = getErrorNodes(tree, code); + + expect(errors).toHaveLength(0); + expect(countNodes(tree, 'ImageElement')).toBe(2); + expect(countNodes(tree, 'RefAttribute')).toBe(2); + expect(countNodes(tree, 'SrcAttribute')).toBe(2); + }); + + it('rejects plain text lines', () => { + const code = `hello world +img(ref="media-id-1", src="/media/2026/04/a.png")`; + + const tree = parseCode(code); + const errors = getErrorNodes(tree, code); + + expect(errors.length).toBeGreaterThan(0); + }); + + it('rejects records missing ref or src', () => { + const code = `img(ref="media-id-1", alt="only ref") +img(src="/media/2026/04/a.png", alt="only src")`; + + const tree = parseCode(code); + const errors = getErrorNodes(tree, code); + + expect(errors.length).toBeGreaterThan(0); + }); +}); diff --git a/frontend/src/views/editor/extensions/imageblock/language/image.highlight.ts b/frontend/src/views/editor/extensions/imageblock/language/image.highlight.ts new file mode 100644 index 00000000..b17a1103 --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/language/image.highlight.ts @@ -0,0 +1,19 @@ +import { styleTags, tags as t } from '@lezer/highlight'; + +export const imageHighlighting = styleTags({ + ImgKeyword: t.keyword, + + 'RefAttributeName SrcAttributeName WidthAttributeName HeightAttributeName AltAttributeName TitleAttributeName': + t.attributeName, + + StringLiteral: t.string, + NumberLiteral: t.number, + 'True False': t.bool, + Null: t.null, + + LineComment: t.lineComment, + + '=': t.definitionOperator, + ',': t.separator, + '( )': t.paren, +}); diff --git a/frontend/src/views/editor/extensions/imageblock/language/image.parser.terms.ts b/frontend/src/views/editor/extensions/imageblock/language/image.parser.terms.ts new file mode 100644 index 00000000..80972a23 --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/language/image.parser.terms.ts @@ -0,0 +1,24 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +export const + LineComment = 1, + Document = 2, + ImageElement = 3, + ImgKeyword = 5, + ImageConfig = 6, + RequiredAttributePair = 8, + RefAttribute = 9, + RefAttributeName = 11, + StringLiteral = 13, + SrcAttribute = 15, + SrcAttributeName = 17, + OptionalAttributeList = 19, + OptionalAttribute = 20, + WidthAttribute = 21, + WidthAttributeName = 23, + NumberLiteral = 24, + HeightAttribute = 25, + HeightAttributeName = 27, + AltAttribute = 28, + AltAttributeName = 30, + TitleAttribute = 31, + TitleAttributeName = 33 diff --git a/frontend/src/views/editor/extensions/imageblock/language/image.parser.ts b/frontend/src/views/editor/extensions/imageblock/language/image.parser.ts new file mode 100644 index 00000000..93f6e80e --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/language/image.parser.ts @@ -0,0 +1,21 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +import {LRParser} from "@lezer/lr" +const spec_identifier = {__proto__:null,img:10, ref:22, src:34, width:46, height:54, alt:60, title:66} +export const parser = LRParser.deserialize({ + version: 14, + states: "'[QYQPOOOOQO'#C`'#C`O_QPO'#C_OOQO'#DO'#DOQYQPOOOdQPO'#CbOOQO,58y,58yOOQO-E6|-E6|OOQO'#Cf'#CfOlQPO'#CeOOQO'#Cl'#ClOqQPO'#CkOvQPO'#CdO{QPO'#CdO!QQPO,58|O!YQPO,59PO!_QPO,59VOgQPO,59OO!dQPO,59OOOQO1G.h1G.hO!iQPO1G.hOOQO1G.k1G.kOOQO1G.q1G.qOOQO1G.j1G.jOOQO'#Cr'#CrO!wQPO'#CqOOQO'#Cv'#CvO!|QPO'#CuOOQO'#Cy'#CyO#RQPO'#CxOOQO'#C|'#C|O#WQPO'#C{OOQO'#Cp'#CpO#]QPO'#CoO#eQPO7+$SO#jQPO,59]O#oQPO,59aO#tQPO,59dO#yQPO,59gO!iQPO'#DPO$OQPO,59ZOOQO< spec_identifier[value] || -1}], + tokenPrec: 0 +}) diff --git a/frontend/src/views/editor/extensions/imageblock/language/index.ts b/frontend/src/views/editor/extensions/imageblock/language/index.ts new file mode 100644 index 00000000..1169161b --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/language/index.ts @@ -0,0 +1,3 @@ +export { image, imageLanguage } from './image-language'; +export { parser } from './image.parser'; +export type { LRLanguage } from '@codemirror/language'; diff --git a/frontend/src/views/editor/extensions/imageblock/render.ts b/frontend/src/views/editor/extensions/imageblock/render.ts new file mode 100644 index 00000000..27a06db6 --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/render.ts @@ -0,0 +1,268 @@ +import {Extension, RangeSetBuilder, StateField} from '@codemirror/state'; +import { + Decoration, + type DecorationSet, + EditorView, +} from '@codemirror/view'; +import {blockState} from '@/views/editor/extensions/codeblock/state'; +import {IMAGE_BLOCK_LANGUAGE, TARGET_PREVIEW_HEIGHT} from './constants'; +import {getSelectedImageBlock, imageBlockSelectionField} from './selection'; +import {getImageBlockItemMap} from './syntax'; +import type {ImageBlockItem, ImageBlockSelection} from './types'; +import {ImageBlockWidget} from './widget'; + +const EMPTY_IMAGE_ITEM_MAP = new Map(); + +function isSameImageSelection( + previous: ImageBlockSelection | null | undefined, + next: ImageBlockSelection | null | undefined, +): boolean { + if (!previous || !next) { + return previous === next; + } + + return previous.anchor === next.anchor && previous.itemIndex === next.itemIndex; +} + +function isSelectionInImageBlock(state: EditorView['state'], from: number, to: number): boolean { + return state.selection.ranges.some(range => range.from <= to && from <= range.to); +} + +function buildImageBlockDecorations(state: EditorView['state']): DecorationSet { + const blocks = state.field(blockState, false) ?? []; + const itemsByBlock = state.field(imageBlockItemsField, false) ?? EMPTY_IMAGE_ITEM_MAP; + const selected = getSelectedImageBlock(state); + const builder = new RangeSetBuilder(); + + for (const block of blocks) { + if (block.language.name !== IMAGE_BLOCK_LANGUAGE) { + continue; + } + if (block.content.from >= block.content.to) { + continue; + } + if (isSelectionInImageBlock(state, block.content.from, block.content.to)) { + continue; + } + + const items = itemsByBlock.get(block.content.from) ?? []; + if (items.length === 0) { + continue; + } + + builder.add( + block.content.from, + block.content.to, + Decoration.replace({ + block: true, + inclusive: true, + widget: new ImageBlockWidget( + block.content.from, + items, + selected?.block.content.from === block.content.from ? selected.itemIndex : null, + ), + }), + ); + } + + return builder.finish(); +} + +const imageBlockItemsField = StateField.define>({ + create: getImageBlockItemMap, + update(items, transaction) { + return transaction.docChanged ? getImageBlockItemMap(transaction.state) : items; + }, +}); + +const imageBlockDecorationField = StateField.define({ + create: buildImageBlockDecorations, + update(decorations, transaction) { + const previousSelection = transaction.startState.field(imageBlockSelectionField, false); + const nextSelection = transaction.state.field(imageBlockSelectionField, false); + + if ( + transaction.docChanged + || Boolean(transaction.selection) + || !isSameImageSelection(previousSelection, nextSelection) + ) { + return buildImageBlockDecorations(transaction.state); + } + + return decorations; + }, + provide: field => EditorView.decorations.from(field), +}); + +const imageBlockTheme = EditorView.baseTheme({ + '.cm-image-block-widget': { + padding: '8px 0 6px', + }, + '.cm-image-block-flow': { + display: 'flex', + flexWrap: 'wrap', + alignItems: 'flex-start', + justifyContent: 'flex-start', + gap: '12px', + }, + '.cm-image-block-item': { + position: 'relative', + display: 'flex', + alignItems: 'stretch', + justifyContent: 'center', + flex: '0 0 auto', + maxWidth: '100%', + minWidth: '48px', + minHeight: '48px', + overflow: 'visible', + border: '1px solid rgba(127, 127, 127, 0.18)', + background: ` + linear-gradient(45deg, rgba(127, 127, 127, 0.08) 25%, transparent 25%), + linear-gradient(-45deg, rgba(127, 127, 127, 0.08) 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, rgba(127, 127, 127, 0.08) 75%), + linear-gradient(-45deg, transparent 75%, rgba(127, 127, 127, 0.08) 75%) + `, + backgroundColor: 'rgba(127, 127, 127, 0.04)', + backgroundSize: '12px 12px', + backgroundPosition: '0 0, 0 6px, 6px -6px, -6px 0', + }, + '.cm-image-block-item.is-selected': { + outline: '1px solid var(--cm-selection-border, #4a9eff)', + outlineOffset: '1px', + zIndex: '2', + }, + '.cm-image-block-item.is-busy': { + cursor: 'progress', + }, + '.cm-image-block-item.is-error': { + borderStyle: 'dashed', + }, + '.cm-image-block-item.is-dragging': { + opacity: '0.48', + zIndex: '3', + }, + '.cm-image-block-item.is-dragging .cm-image-block-actions': { + opacity: '0', + pointerEvents: 'none', + }, + '.cm-image-block-item.is-drop-before::before': { + content: '""', + position: 'absolute', + top: '0', + bottom: '0', + left: '-7px', + width: '2px', + background: 'var(--cm-selection-border, #4a9eff)', + }, + '.cm-image-block-item.is-drop-after::after': { + content: '""', + position: 'absolute', + top: '0', + bottom: '0', + right: '-7px', + width: '2px', + background: 'var(--cm-selection-border, #4a9eff)', + }, + '.cm-image-block-thumb': { + display: 'block', + width: 'auto', + height: 'auto', + maxHeight: `${TARGET_PREVIEW_HEIGHT}px`, + maxWidth: 'min(320px, calc(100vw - 96px))', + objectFit: 'contain', + opacity: '0', + transition: 'opacity 0.16s ease', + }, + '.cm-image-block-item.is-loaded .cm-image-block-thumb': { + opacity: '1', + }, + '.cm-image-block-actions': { + position: 'absolute', + top: '0', + right: '-36px', + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + gap: '6px', + opacity: '0', + pointerEvents: 'none', + transition: 'opacity 0.12s ease', + zIndex: '1', + }, + '.cm-image-block-item.is-selected .cm-image-block-actions': { + opacity: '1', + pointerEvents: 'auto', + }, + '.cm-image-block-drag-handle': { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + width: '28px', + height: '28px', + border: '1px solid var(--border-color, rgba(127, 127, 127, 0.24))', + background: 'var(--bg-primary, #ffffff)', + color: 'var(--text-secondary, #666666)', + boxShadow: '0 2px 8px rgba(0, 0, 0, 0.18)', + cursor: 'grab', + userSelect: 'none', + outline: 'none', + transition: 'background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease, opacity 0.12s ease', + }, + '.cm-image-block-drag-handle:hover': { + background: 'var(--bg-hover, rgba(127, 127, 127, 0.08))', + color: 'var(--text-primary, #111111)', + }, + '.cm-image-block-drag-handle:focus-visible': { + borderColor: 'var(--cm-selection-border, #4a9eff)', + }, + '.cm-image-block-item.is-dragging .cm-image-block-drag-handle': { + cursor: 'grabbing', + }, + '.cm-image-block-item.is-busy .cm-image-block-drag-handle': { + opacity: '0.55', + pointerEvents: 'none', + }, + '.cm-image-block-drag-handle svg': { + width: '14px', + height: '14px', + fill: 'currentColor', + }, + '.cm-image-block-action': { + appearance: 'none', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + width: '28px', + height: '28px', + padding: '0', + border: '1px solid var(--border-color, rgba(127, 127, 127, 0.24))', + background: 'var(--bg-primary, #ffffff)', + color: 'var(--text-primary, #111111)', + boxShadow: '0 2px 8px rgba(0, 0, 0, 0.18)', + cursor: 'pointer', + outline: 'none', + transition: 'background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease, opacity 0.12s ease', + }, + '.cm-image-block-action:hover:not(:disabled)': { + background: 'var(--bg-hover, rgba(127, 127, 127, 0.08))', + }, + '.cm-image-block-action:disabled': { + opacity: '0.55', + cursor: 'default', + }, + '.cm-image-block-action:focus-visible': { + borderColor: 'var(--cm-selection-border, #4a9eff)', + }, + '.cm-image-block-action svg': { + width: '14px', + height: '14px', + fill: 'currentColor', + }, + '.cm-image-block-action.is-delete': { + color: 'var(--text-danger, #d14b4b)', + }, +}); + +export function getImageBlockRenderExtensions(): Extension[] { + return [imageBlockItemsField, imageBlockDecorationField, imageBlockTheme]; +} diff --git a/frontend/src/views/editor/extensions/imageblock/selection.ts b/frontend/src/views/editor/extensions/imageblock/selection.ts new file mode 100644 index 00000000..ccd968bb --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/selection.ts @@ -0,0 +1,69 @@ +import {StateEffect, StateField} from '@codemirror/state'; +import {getBlockFromPos} from '@/views/editor/extensions/codeblock/parser'; +import type {Block} from '@/views/editor/extensions/codeblock/types'; +import {IMAGE_BLOCK_LANGUAGE} from './constants'; +import type {ImageBlockSelection} from './types'; + +export const setImageBlockSelection = StateEffect.define({ + map(value, changes) { + return { + ...value, + anchor: changes.mapPos(value.anchor, 1), + }; + }, +}); + +export const clearImageBlockSelection = StateEffect.define(); + +export const imageBlockSelectionField = StateField.define({ + create: () => null, + update(value, transaction) { + if (value) { + value = { + ...value, + anchor: transaction.changes.mapPos(value.anchor, 1), + }; + } + + for (const effect of transaction.effects) { + if (effect.is(clearImageBlockSelection)) { + return null; + } + if (effect.is(setImageBlockSelection)) { + return effect.value; + } + } + + if (transaction.selection) { + return null; + } + + if (!value) { + return null; + } + + const block = getBlockFromPos(transaction.state, value.anchor); + if (!block || block.language.name !== IMAGE_BLOCK_LANGUAGE) { + return null; + } + + return value; + }, +}); + +export function getSelectedImageBlock(state: Parameters[0]): { block: Block; itemIndex: number | null } | null { + const selection = state.field(imageBlockSelectionField, false); + if (!selection) { + return null; + } + + const block = getBlockFromPos(state, selection.anchor); + if (!block || block.language.name !== IMAGE_BLOCK_LANGUAGE) { + return null; + } + + return { + block, + itemIndex: selection.itemIndex, + }; +} diff --git a/frontend/src/views/editor/extensions/imageblock/syntax.test.ts b/frontend/src/views/editor/extensions/imageblock/syntax.test.ts new file mode 100644 index 00000000..fd972ab3 --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/syntax.test.ts @@ -0,0 +1,61 @@ +import {EditorState} from '@codemirror/state'; +import {describe, expect, it, vi} from 'vitest'; + +vi.mock('@/views/editor/extensions/codeblock/lang-parser/languages', async () => { + const {imageLanguage} = await import('@/views/editor/extensions/imageblock/language/image-language'); + + return { + LANGUAGES: [ + {token: 'text', parser: null}, + {token: 'image', parser: imageLanguage.parser}, + ], + languageMapping: { + image: imageLanguage.parser, + }, + }; +}); + +import {getCodeBlockLanguageExtension} from '@/views/editor/extensions/codeblock/lang-parser'; +import {createDelimiter} from '@/views/editor/extensions/codeblock/parser'; +import {blockState} from '@/views/editor/extensions/codeblock/state'; +import {getImageBlockItems} from './syntax'; + +describe('image block syntax integration', () => { + it('reads image items from the nested image parser inside a code block', () => { + const doc = [ + createDelimiter('image', false, 'write'), + 'img(ref="sha256-1", src="/media/a.png", alt="cover", width=320, height=180, title="cover image")\n', + 'img(src="https://example.com/demo.png", ref="sha256-2", width=240, height=240)', + ].join(''); + + const state = EditorState.create({ + doc, + extensions: [ + blockState, + ...getCodeBlockLanguageExtension(), + ], + }); + + const [block] = state.field(blockState); + expect(block?.language.name).toBe('image'); + + expect(getImageBlockItems(state, block!)).toEqual([ + { + ref: 'sha256-1', + src: '/media/a.png', + alt: 'cover', + title: 'cover image', + width: 320, + height: 180, + }, + { + ref: 'sha256-2', + src: 'https://example.com/demo.png', + alt: undefined, + title: undefined, + width: 240, + height: 240, + }, + ]); + }); +}); diff --git a/frontend/src/views/editor/extensions/imageblock/syntax.ts b/frontend/src/views/editor/extensions/imageblock/syntax.ts new file mode 100644 index 00000000..576dabc5 --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/syntax.ts @@ -0,0 +1,182 @@ +import {ensureSyntaxTree} from '@codemirror/language'; +import type {EditorState} from '@codemirror/state'; +import type {SyntaxNode, Tree} from '@lezer/common'; +import {blockState} from '@/views/editor/extensions/codeblock/state'; +import type {Block} from '@/views/editor/extensions/codeblock/types'; +import {IMAGE_BLOCK_LANGUAGE} from './constants'; +import type {ImageBlockItem} from './types'; + +const NODE_NAMES = { + imageElement: 'ImageElement', + imageConfig: 'ImageConfig', + requiredAttributePair: 'RequiredAttributePair', + optionalAttributeList: 'OptionalAttributeList', + optionalAttribute: 'OptionalAttribute', + refAttribute: 'RefAttribute', + srcAttribute: 'SrcAttribute', + widthAttribute: 'WidthAttribute', + heightAttribute: 'HeightAttribute', + altAttribute: 'AltAttribute', + titleAttribute: 'TitleAttribute', + stringLiteral: 'StringLiteral', + numberLiteral: 'NumberLiteral', +} as const; + +const EMPTY_OPTIONAL_ATTRIBUTE_MAP = new Map(); +const EMPTY_IMAGE_ITEM_MAP = new Map(); + +function parseQuotedLiteral(raw: string): string { + if (raw.length < 2) { + return raw; + } + + const quote = raw[0]; + if ((quote !== '"' && quote !== '\'') || raw[raw.length - 1] !== quote) { + return raw; + } + + if (quote === '"') { + try { + return JSON.parse(raw); + } catch { + // Keep the raw token if JSON parsing fails. + } + } + + let result = ''; + let escaping = false; + for (const char of raw.slice(1, -1)) { + if (escaping) { + if (char === 'n') result += '\n'; + else if (char === 'r') result += '\r'; + else if (char === 't') result += '\t'; + else result += char; + escaping = false; + continue; + } + + if (char === '\\') { + escaping = true; + continue; + } + + result += char; + } + + return escaping ? `${result}\\` : result; +} + +function readStringLiteral(state: EditorState, attribute: SyntaxNode | null | undefined): string | undefined { + const valueNode = attribute?.getChild(NODE_NAMES.stringLiteral); + if (!valueNode) { + return undefined; + } + + return parseQuotedLiteral(state.sliceDoc(valueNode.from, valueNode.to)); +} + +function readNumberLiteral(state: EditorState, attribute: SyntaxNode | null | undefined): number | undefined { + const valueNode = attribute?.getChild(NODE_NAMES.numberLiteral); + if (!valueNode) { + return undefined; + } + + const value = Number(state.sliceDoc(valueNode.from, valueNode.to)); + return Number.isFinite(value) && value > 0 ? value : undefined; +} + +function getOptionalAttributes(config: SyntaxNode): ReadonlyMap { + const optionalList = config.getChild(NODE_NAMES.optionalAttributeList); + if (!optionalList) { + return EMPTY_OPTIONAL_ATTRIBUTE_MAP; + } + + return new Map( + optionalList + .getChildren(NODE_NAMES.optionalAttribute) + .flatMap(optionalAttribute => { + const attribute = optionalAttribute.firstChild; + return attribute ? [[attribute.name, attribute] as const] : []; + }), + ); +} + +function readImageElement(state: EditorState, node: SyntaxNode): ImageBlockItem | null { + const config = node.getChild(NODE_NAMES.imageConfig); + const requiredAttributes = config?.getChild(NODE_NAMES.requiredAttributePair); + if (!config || !requiredAttributes) { + return null; + } + + const ref = readStringLiteral(state, requiredAttributes.getChild(NODE_NAMES.refAttribute)); + const src = readStringLiteral(state, requiredAttributes.getChild(NODE_NAMES.srcAttribute)); + if (!ref || !src) { + return null; + } + + const optionalAttributes = getOptionalAttributes(config); + + return { + ref, + src, + alt: readStringLiteral(state, optionalAttributes.get(NODE_NAMES.altAttribute)), + title: readStringLiteral(state, optionalAttributes.get(NODE_NAMES.titleAttribute)), + width: readNumberLiteral(state, optionalAttributes.get(NODE_NAMES.widthAttribute)), + height: readNumberLiteral(state, optionalAttributes.get(NODE_NAMES.heightAttribute)), + }; +} + +export function isImageBlock(block: Block | null | undefined): block is Block { + return Boolean(block && block.language.name === IMAGE_BLOCK_LANGUAGE); +} + +function collectImageBlockItems(state: EditorState, tree: Tree, block: Block): ImageBlockItem[] { + const items: ImageBlockItem[] = []; + tree.iterate({ + from: block.content.from, + to: block.content.to, + enter: nodeRef => { + if (nodeRef.name !== NODE_NAMES.imageElement) { + return; + } + + const item = readImageElement(state, nodeRef.node); + if (item) { + items.push(item); + } + }, + }); + + return items; +} + +export function getImageBlockItems(state: EditorState, block: Block): ImageBlockItem[] { + if (!isImageBlock(block)) { + return []; + } + + const tree = ensureSyntaxTree(state, state.doc.length, 1000); + if (!tree) { + return []; + } + + return collectImageBlockItems(state, tree, block); +} + +export function getImageBlockItemMap(state: EditorState): ReadonlyMap { + const blocks = (state.field(blockState, false) ?? []).filter(isImageBlock); + if (blocks.length === 0) { + return EMPTY_IMAGE_ITEM_MAP; + } + + const tree = ensureSyntaxTree(state, state.doc.length, 1000); + if (!tree) { + return EMPTY_IMAGE_ITEM_MAP; + } + + const entries = blocks + .map(block => [block.content.from, collectImageBlockItems(state, tree, block)] as const) + .filter(([, items]) => items.length > 0); + + return entries.length > 0 ? new Map(entries) : EMPTY_IMAGE_ITEM_MAP; +} diff --git a/frontend/src/views/editor/extensions/imageblock/types.ts b/frontend/src/views/editor/extensions/imageblock/types.ts new file mode 100644 index 00000000..87464728 --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/types.ts @@ -0,0 +1,13 @@ +export interface ImageBlockItem { + ref: string; + src: string; + alt?: string; + title?: string; + width?: number; + height?: number; +} + +export interface ImageBlockSelection { + anchor: number; + itemIndex: number | null; +} diff --git a/frontend/src/views/editor/extensions/imageblock/widget.ts b/frontend/src/views/editor/extensions/imageblock/widget.ts new file mode 100644 index 00000000..051daeaf --- /dev/null +++ b/frontend/src/views/editor/extensions/imageblock/widget.ts @@ -0,0 +1,303 @@ +import {EditorView, WidgetType} from '@codemirror/view'; +import {CONTEXT_MENU_BLOCK_ANCHOR_DATASET} from '@/views/editor/extensions/contextMenu/constants'; +import i18n from '@/i18n'; +import {copyImageItemToClipboard, deleteImageItem} from './actions'; +import {MAX_PREVIEW_WIDTH, TARGET_PREVIEW_HEIGHT} from './constants'; +import {attachImageBlockDrag} from './drag'; +import {setImageBlockSelection} from './selection'; +import type {ImageBlockItem} from './types'; + +const ITEM_SIGNATURE_SEPARATOR = '\u001f'; +const t = (key: string) => i18n.global.t(key); + +function getPreviewWidth(width: number, height: number): number { + return Math.min( + MAX_PREVIEW_WIDTH, + Math.max(48, Math.round((TARGET_PREVIEW_HEIGHT * width) / height)), + ); +} + +function applyPreviewSize(element: HTMLElement, width?: number, height?: number) { + if (!width || !height || width <= 0 || height <= 0) { + return; + } + + element.style.width = `${getPreviewWidth(width, height)}px`; + element.style.aspectRatio = `${width} / ${height}`; +} + +function getItemSignature(item: ImageBlockItem): string { + return [ + item.ref, + item.src, + item.alt ?? '', + item.title ?? '', + item.width ?? '', + item.height ?? '', + ].join(ITEM_SIGNATURE_SEPARATOR); +} + +function areItemsEqual(left: readonly ImageBlockItem[], right: readonly ImageBlockItem[]): boolean { + if (left.length !== right.length) { + return false; + } + + return left.every((item, index) => { + const other = right[index]; + return item.ref === other?.ref + && item.src === other?.src + && item.alt === other?.alt + && item.title === other?.title + && item.width === other?.width + && item.height === other?.height; + }); +} + +function setSelectionState(frame: HTMLElement, selected: boolean) { + frame.classList.toggle('is-selected', selected); +} + +function getImageTitle(item: ImageBlockItem): string | undefined { + return item.title ?? item.alt; +} + +function applyLoadedState(frame: HTMLElement, image: HTMLImageElement) { + if (image.naturalWidth > 0 && image.naturalHeight > 0) { + applyPreviewSize(frame, image.naturalWidth, image.naturalHeight); + } + + frame.classList.remove('is-loading'); + frame.classList.add('is-loaded'); +} + +function createImageFrame( + view: EditorView, + anchor: number, + item: ImageBlockItem, + index: number, + selectedIndex: number | null, +): HTMLElement { + const frame = document.createElement('div'); + frame.className = 'cm-image-block-item is-loading'; + frame.dataset.signature = getItemSignature(item); + setSelectionState(frame, selectedIndex === index); + applyPreviewSize(frame, item.width, item.height); + + frame.addEventListener('mousedown', event => { + event.preventDefault(); + event.stopPropagation(); + + view.focus(); + view.dispatch({ + effects: [setImageBlockSelection.of({ + anchor, + itemIndex: index, + })], + }); + }); + + const image = document.createElement('img'); + image.className = 'cm-image-block-thumb'; + image.alt = item.alt ?? item.title ?? ''; + image.draggable = false; + image.decoding = 'async'; + image.loading = 'lazy'; + + const title = getImageTitle(item); + if (title) { + image.title = title; + } + + image.addEventListener('load', () => { + applyLoadedState(frame, image); + }); + + image.addEventListener('error', () => { + frame.classList.remove('is-loading'); + frame.classList.add('is-error'); + }); + + image.src = item.src; + if (image.complete && image.naturalWidth > 0) { + applyLoadedState(frame, image); + } + + frame.appendChild(image); + + const actions = document.createElement('div'); + actions.className = 'cm-image-block-actions'; + frame.appendChild(actions); + + const stopPropagation = (event: Event) => { + event.stopPropagation(); + }; + + const stopPointerEvent = (event: MouseEvent) => { + event.preventDefault(); + stopPropagation(event); + }; + + const dragHandle = document.createElement('div'); + dragHandle.className = 'cm-image-block-drag-handle'; + dragHandle.title = t('extensions.imageBlock.dragSort'); + dragHandle.setAttribute('aria-label', t('extensions.imageBlock.dragSort')); + dragHandle.innerHTML = ''; + dragHandle.addEventListener('mousedown', stopPropagation); + dragHandle.addEventListener('click', event => { + event.preventDefault(); + stopPropagation(event); + }); + actions.appendChild(dragHandle); + + const setBusy = (busy: boolean) => { + frame.classList.toggle('is-busy', busy); + Array.from(actions.querySelectorAll('button')).forEach(button => { + button.disabled = busy; + }); + }; + + const createActionButton = ( + className: string, + title: string, + iconMarkup: string, + action: () => Promise, + options: { + busy?: boolean; + } = {}, + ) => { + const button = document.createElement('button'); + button.type = 'button'; + button.className = `cm-image-block-action ${className}`; + button.title = title; + button.setAttribute('aria-label', title); + button.innerHTML = iconMarkup; + button.addEventListener('mousedown', stopPointerEvent); + button.addEventListener('click', async event => { + stopPointerEvent(event); + if (options.busy) { + setBusy(true); + } + try { + await action(); + } catch (error) { + console.error(`[imageblock] Failed to ${className}:`, error); + } finally { + if (options.busy) { + setBusy(false); + } + } + }); + actions.appendChild(button); + }; + + createActionButton( + 'is-copy', + t('extensions.imageBlock.copyImage'), + '', + () => copyImageItemToClipboard(item), + ); + + createActionButton( + 'is-delete', + t('extensions.imageBlock.deleteImage'), + '', + async () => { + const deleted = await deleteImageItem(view, anchor, index); + if (!deleted) { + throw new Error('Delete image request was not completed'); + } + }, + {busy: true}, + ); + + return frame; +} + +export class ImageBlockWidget extends WidgetType { + constructor( + private readonly anchor: number, + private readonly items: readonly ImageBlockItem[], + private readonly selectedIndex: number | null, + ) { + super(); + } + + override eq(other: ImageBlockWidget): boolean { + return this.anchor === other.anchor + && this.selectedIndex === other.selectedIndex + && areItemsEqual(this.items, other.items); + } + + override toDOM(view: EditorView): HTMLElement { + const root = document.createElement('div'); + root.className = 'cm-image-block-widget'; + root.dataset[CONTEXT_MENU_BLOCK_ANCHOR_DATASET] = String(this.anchor); + + const flow = document.createElement('div'); + flow.className = 'cm-image-block-flow'; + root.appendChild(flow); + + root.addEventListener('mousedown', event => { + const target = event.target as HTMLElement | null; + if (target?.closest('.cm-image-block-item')) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + view.focus(); + view.dispatch({ + effects: [setImageBlockSelection.of({ + anchor: this.anchor, + itemIndex: null, + })], + }); + }); + + this.items.forEach((item, index) => { + flow.appendChild(createImageFrame(view, this.anchor, item, index, this.selectedIndex)); + }); + + attachImageBlockDrag(flow, view, this.anchor); + + return root; + } + + override updateDOM(dom: HTMLElement): boolean { + if (dom.dataset[CONTEXT_MENU_BLOCK_ANCHOR_DATASET] !== String(this.anchor)) { + return false; + } + + const flow = dom.querySelector('.cm-image-block-flow'); + if (!flow) { + return false; + } + + const frames = Array.from(flow.children); + if (frames.length !== this.items.length) { + return false; + } + + const canReuse = this.items.every((item, index) => { + const frame = frames[index]; + return frame instanceof HTMLElement && frame.dataset.signature === getItemSignature(item); + }); + + if (!canReuse) { + return false; + } + + frames.forEach((frame, index) => { + if (frame instanceof HTMLElement) { + setSelectionState(frame, this.selectedIndex === index); + } + }); + + return true; + } + + override ignoreEvent(): boolean { + return true; + } +} From 1722b98ac2a1c14100999743e356ff957dc976b7 Mon Sep 17 00:00:00 2001 From: landaiqing Date: Fri, 3 Apr 2026 11:13:01 +0800 Subject: [PATCH 21/26] :fire: remove image code block --- frontend/package.json | 1 - frontend/src/i18n/locales/en-US.ts | 5 - frontend/src/i18n/locales/zh-CN.ts | 5 - .../views/editor/basic/wheelZoomExtension.ts | 87 ++--- .../extensions/blockImage/exportPreset.ts | 2 - .../editor/extensions/codeblock/copyPaste.ts | 9 - .../editor/extensions/codeblock/index.ts | 4 - .../codeblock/lang-parser/codeblock.grammar | 2 +- .../codeblock/lang-parser/languages.ts | 2 - .../codeblock/lang-parser/parser.ts | 8 +- .../extensions/codeblock/parser.test.ts | 8 - .../editor/extensions/codeblock/types.ts | 3 +- .../contextMenu/blockContext.test.ts | 10 +- .../editor/extensions/imageblock/actions.ts | 46 --- .../editor/extensions/imageblock/clipboard.ts | 159 --------- .../editor/extensions/imageblock/constants.ts | 3 - .../extensions/imageblock/document.test.ts | 113 ------- .../editor/extensions/imageblock/document.ts | 236 -------------- .../editor/extensions/imageblock/drag.ts | 211 ------------ .../editor/extensions/imageblock/extension.ts | 10 - .../editor/extensions/imageblock/index.ts | 3 - .../imageblock/language/build-parser.js | 34 -- .../imageblock/language/image-language.ts | 17 - .../imageblock/language/image.grammar | 112 ------- .../imageblock/language/image.grammar.test.ts | 91 ------ .../imageblock/language/image.highlight.ts | 19 -- .../imageblock/language/image.parser.terms.ts | 24 -- .../imageblock/language/image.parser.ts | 21 -- .../extensions/imageblock/language/index.ts | 3 - .../editor/extensions/imageblock/render.ts | 268 ---------------- .../editor/extensions/imageblock/selection.ts | 69 ---- .../extensions/imageblock/syntax.test.ts | 61 ---- .../editor/extensions/imageblock/syntax.ts | 182 ----------- .../editor/extensions/imageblock/types.ts | 13 - .../editor/extensions/imageblock/widget.ts | 303 ------------------ 35 files changed, 62 insertions(+), 2082 deletions(-) delete mode 100644 frontend/src/views/editor/extensions/imageblock/actions.ts delete mode 100644 frontend/src/views/editor/extensions/imageblock/clipboard.ts delete mode 100644 frontend/src/views/editor/extensions/imageblock/constants.ts delete mode 100644 frontend/src/views/editor/extensions/imageblock/document.test.ts delete mode 100644 frontend/src/views/editor/extensions/imageblock/document.ts delete mode 100644 frontend/src/views/editor/extensions/imageblock/drag.ts delete mode 100644 frontend/src/views/editor/extensions/imageblock/extension.ts delete mode 100644 frontend/src/views/editor/extensions/imageblock/index.ts delete mode 100644 frontend/src/views/editor/extensions/imageblock/language/build-parser.js delete mode 100644 frontend/src/views/editor/extensions/imageblock/language/image-language.ts delete mode 100644 frontend/src/views/editor/extensions/imageblock/language/image.grammar delete mode 100644 frontend/src/views/editor/extensions/imageblock/language/image.grammar.test.ts delete mode 100644 frontend/src/views/editor/extensions/imageblock/language/image.highlight.ts delete mode 100644 frontend/src/views/editor/extensions/imageblock/language/image.parser.terms.ts delete mode 100644 frontend/src/views/editor/extensions/imageblock/language/image.parser.ts delete mode 100644 frontend/src/views/editor/extensions/imageblock/language/index.ts delete mode 100644 frontend/src/views/editor/extensions/imageblock/render.ts delete mode 100644 frontend/src/views/editor/extensions/imageblock/selection.ts delete mode 100644 frontend/src/views/editor/extensions/imageblock/syntax.test.ts delete mode 100644 frontend/src/views/editor/extensions/imageblock/syntax.ts delete mode 100644 frontend/src/views/editor/extensions/imageblock/types.ts delete mode 100644 frontend/src/views/editor/extensions/imageblock/widget.ts diff --git a/frontend/package.json b/frontend/package.json index 7beb7fcf..83c457ac 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,6 @@ "lint": "eslint", "lint:fix": "eslint --fix", "build:lang-parser": "node src/views/editor/extensions/codeblock/lang-parser/build-parser.js", - "build:image-parser": "node src/views/editor/extensions/imageblock/language/build-parser.js", "build:mermaid-parser": "node src/views/editor/language/mermaid/build-parsers.js", "test": "vitest", "docs:dev": "vitepress dev docs", diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index cd448dbf..f2c43646 100644 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -380,11 +380,6 @@ export default { description: 'Protect readonly code block ranges and allow toggling block access from the context menu', markReadonly: 'Set block readonly', markWritable: 'Set block writable' - }, - imageBlock: { - dragSort: 'Drag to reorder', - copyImage: 'Copy image to clipboard', - deleteImage: 'Delete image' } }, monitor: { diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 5eefec45..16fa22b1 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -383,11 +383,6 @@ export default { markReadonly: '设为只读块', markWritable: '设为可写块' }, - imageBlock: { - dragSort: '拖拽排序', - copyImage: '复制图片到剪切板', - deleteImage: '删除图片' - }, }, monitor: { memory: '内存', diff --git a/frontend/src/views/editor/basic/wheelZoomExtension.ts b/frontend/src/views/editor/basic/wheelZoomExtension.ts index 4edd708d..1c2f25cd 100644 --- a/frontend/src/views/editor/basic/wheelZoomExtension.ts +++ b/frontend/src/views/editor/basic/wheelZoomExtension.ts @@ -1,5 +1,5 @@ import type {Extension} from '@codemirror/state'; -import {EditorView} from '@codemirror/view'; +import {EditorView, ViewPlugin} from '@codemirror/view'; import {createDebounce} from '@/common/utils/debounce'; type FontAdjuster = () => void; @@ -32,57 +32,72 @@ export const createWheelZoomExtension = (options: WheelZoomOptions): Extension = }, {delay: saveDelay}) : {debouncedFn: null}; - let pendingDelta = 0; - let frameId: number | null = null; + return ViewPlugin.fromClass(class { + private pendingDelta = 0; + private frameId: number | null = null; + private readonly domWindow: Window; + private readonly onWheel = (event: WheelEvent) => { + if (!event.ctrlKey) { + return; + } - const flushPendingDelta = () => { - frameId = null; + event.preventDefault(); + event.stopPropagation(); - if (pendingDelta === 0) { - return; - } + if (event.deltaY < 0) { + this.pendingDelta += 1; + } else if (event.deltaY > 0) { + this.pendingDelta -= 1; + } - const delta = pendingDelta; - pendingDelta = 0; + if (this.pendingDelta !== 0 && this.frameId === null) { + this.frameId = this.domWindow.requestAnimationFrame(this.flushPendingDelta); + } - if (adjustFontSize) { - adjustFontSize(delta); - return; + if (debouncedSave) { + debouncedSave(); + } + }; + + constructor(private readonly view: EditorView) { + this.domWindow = this.view.dom.ownerDocument.defaultView ?? window; + this.view.dom.addEventListener('wheel', this.onWheel, { + capture: true, + passive: false, + }); } - const applyStep = delta > 0 ? increaseFontSize : decreaseFontSize; - if (!applyStep) { - return; - } + destroy() { + this.view.dom.removeEventListener('wheel', this.onWheel, true); - for (let index = 0; index < Math.abs(delta); index++) { - applyStep(); + if (this.frameId !== null) { + this.domWindow.cancelAnimationFrame(this.frameId); + } } - }; - return EditorView.domEventHandlers({ - wheel(event) { - if (!event.ctrlKey) { - return false; + private readonly flushPendingDelta = () => { + this.frameId = null; + + if (this.pendingDelta === 0) { + return; } - event.preventDefault(); + const delta = this.pendingDelta; + this.pendingDelta = 0; - if (event.deltaY < 0) { - pendingDelta += 1; - } else if (event.deltaY > 0) { - pendingDelta -= 1; + if (adjustFontSize) { + adjustFontSize(delta); + return; } - if (pendingDelta !== 0 && frameId === null) { - frameId = requestAnimationFrame(flushPendingDelta); + const applyStep = delta > 0 ? increaseFontSize : decreaseFontSize; + if (!applyStep) { + return; } - if (debouncedSave) { - debouncedSave(); + for (let index = 0; index < Math.abs(delta); index++) { + applyStep(); } - - return true; - } + }; }); }; diff --git a/frontend/src/views/editor/extensions/blockImage/exportPreset.ts b/frontend/src/views/editor/extensions/blockImage/exportPreset.ts index 348b31ac..b8dc3c9b 100644 --- a/frontend/src/views/editor/extensions/blockImage/exportPreset.ts +++ b/frontend/src/views/editor/extensions/blockImage/exportPreset.ts @@ -18,7 +18,6 @@ import {colorTheme, colorView} from '../colorSelector'; import {getCodeBlockLanguageExtension} from '../codeblock/lang-parser'; import {getMathBlockExtensions} from '../codeblock/mathBlock'; import type {Block, BlockAccess} from '../codeblock/types'; -import {getImageBlockExtensions} from '../imageblock'; import {hyperLink} from '../hyperlink'; import {headingSlugField} from '../markdown/state/heading-slug'; import {render} from '../markdown/plugins/render'; @@ -241,7 +240,6 @@ export async function createBlockImageExportPreset(block: Block, sourceIndex: nu blockState, ...getCodeBlockLanguageExtension(), delimiterPresentationField, - ...getImageBlockExtensions(), ...getMathBlockExtensions(), createExportChromeTheme(), ...optionalContributions.map(({extension}) => extension), diff --git a/frontend/src/views/editor/extensions/codeblock/copyPaste.ts b/frontend/src/views/editor/extensions/codeblock/copyPaste.ts index 22e70c6a..3891a447 100644 --- a/frontend/src/views/editor/extensions/codeblock/copyPaste.ts +++ b/frontend/src/views/editor/extensions/codeblock/copyPaste.ts @@ -8,7 +8,6 @@ import {Command, EditorView} from "@codemirror/view"; import {LANGUAGES} from "./lang-parser/languages"; import {codeBlockEvent, CONTENT_EDIT, USER_EVENTS} from "./annotation"; import * as runtime from "@wailsio/runtime"; -import {handleImagePasteEvent, pasteImagesFromClipboard} from "../imageblock/clipboard"; /** * 构建块分隔符正则表达式 @@ -109,10 +108,6 @@ export const codeBlockCopyCut = EditorView.domEventHandlers({ return false; } - if (handleImagePasteEvent(view, event as ClipboardEvent)) { - return true; - } - event.preventDefault(); void pasteText(view, event as ClipboardEvent); @@ -229,10 +224,6 @@ export const cutCommand: Command = view => copyCut(view, true); */ export const pasteCommand: Command = (view) => { void (async () => { - if (await pasteImagesFromClipboard(view)) { - return; - } - await pasteText(view); })(); diff --git a/frontend/src/views/editor/extensions/codeblock/index.ts b/frontend/src/views/editor/extensions/codeblock/index.ts index 5230c0c9..c626bd97 100644 --- a/frontend/src/views/editor/extensions/codeblock/index.ts +++ b/frontend/src/views/editor/extensions/codeblock/index.ts @@ -26,7 +26,6 @@ import {createLanguageDetection} from './lang-detect'; import {SupportedLanguage} from './types'; import {getMathBlockExtensions} from './mathBlock'; import {createCursorProtection} from './cursorProtection'; -import {getImageBlockExtensions} from '../imageblock'; /** * 代码块扩展配置选项 @@ -120,9 +119,6 @@ export function createCodeBlockExtension(options: CodeBlockOptions = {}): Extens // 复制粘贴功能 ...getCopyPasteExtensions(), - // image 块可视化渲染 - ...getImageBlockExtensions(), - // 数学块功能 ...getMathBlockExtensions(), diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/codeblock.grammar b/frontend/src/views/editor/extensions/codeblock/lang-parser/codeblock.grammar index 878ce969..388d52eb 100644 --- a/frontend/src/views/editor/extensions/codeblock/lang-parser/codeblock.grammar +++ b/frontend/src/views/editor/extensions/codeblock/lang-parser/codeblock.grammar @@ -18,7 +18,7 @@ BlockLanguage { "go" | "clj" | "ex" | "erl" | "js" | "ts" | "swift" | "kt" | "groovy" | "ps1" | "dart" | "scala" | "math" | "dockerfile" | "lua" | "vue" | "lezer" | "liquid" | "wast" | "sass" | "less" | "angular" | "svelte" | - "http" | "mermaid" | "image" + "http" | "mermaid" } BlockFlag { diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts b/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts index 4b15d129..b98bbad0 100644 --- a/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts +++ b/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts @@ -30,7 +30,6 @@ import {lessLanguage} from "@codemirror/lang-less"; import {angularLanguage} from "@codemirror/lang-angular"; import {svelteLanguage} from "@replit/codemirror-lang-svelte"; import {httpLanguage} from "@/views/editor/extensions/httpclient/language/http-language"; -import {imageLanguage} from "@/views/editor/extensions/imageblock/language/image-language"; import {mermaidLanguage} from '@/views/editor/language/mermaid'; import {StreamLanguage} from "@codemirror/language"; import {ruby} from "@codemirror/legacy-modes/mode/ruby"; @@ -249,7 +248,6 @@ export const LANGUAGES: LanguageInfo[] = [ } }), new LanguageInfo("http", "Http", httpLanguage.parser, ["http"]), - new LanguageInfo("image", "Image", imageLanguage.parser, ["image"]), new LanguageInfo("mermaid", "Mermaid", mermaidLanguage.parser, ["mermaid"]), ]; diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/parser.ts b/frontend/src/views/editor/extensions/codeblock/lang-parser/parser.ts index 6c5d0802..491c905f 100644 --- a/frontend/src/views/editor/extensions/codeblock/lang-parser/parser.ts +++ b/frontend/src/views/editor/extensions/codeblock/lang-parser/parser.ts @@ -3,14 +3,14 @@ import {LRParser} from "@lezer/lr" import {blockContent} from "./external-tokens.js" export const parser = LRParser.deserialize({ version: 14, - states: "!|QQOQOOOVOQO'#C`O$OOPO'#C_OOOO'#Cf'#CfQQOQOOOOOO'#Ca'#CaO$TOSO,58zOOOO,58y,58yOOOO-E6d-E6dOOOO'#Cb'#CbOOOO'#Cg'#CgO$cOSO1G.fOOOP1G.f1G.fOOOO-E6e-E6eOOOP7+$Q7+$Q", - stateData: "$q~O]PO~O^TO_TO`TOaTObTOcTOdTOeTOfTOgTOhTOiTOjTOkTOlTOmTOnTOoTOpTOqTOrTOsTOtTOuTOvTOwTOxTOyTOzTO{TO|TO}TO!OTO!PTO!QTO!RTO!STO!TTO!UTO!VTO!WTO!XTO!YTO~OPVO~OVXOWXOXXO!Z[O~OVXOWXOXXO!Z^O~O", + states: "!|QQOQOOOVOQO'#C`O#{OPO'#C_OOOO'#Cf'#CfQQOQOOOOOO'#Ca'#CaO$QOSO,58zOOOO,58y,58yOOOO-E6d-E6dOOOO'#Cb'#CbOOOO'#Cg'#CgO$`OSO1G.fOOOP1G.f1G.fOOOO-E6e-E6eOOOP7+$Q7+$Q", + stateData: "$n~O]PO~O^TO_TO`TOaTObTOcTOdTOeTOfTOgTOhTOiTOjTOkTOlTOmTOnTOoTOpTOqTOrTOsTOtTOuTOvTOwTOxTOyTOzTO{TO|TO}TO!OTO!PTO!QTO!RTO!STO!TTO!UTO!VTO!WTO!XTO~OPVO~OVXOWXOXXO!Y[O~OVXOWXOXXO!Y^O~O", goto: "x[PPP]aehPPPlrTROSTQOSRUPTYUZQSORWSQZUR]Z", nodeNames: "⚠ BlockContent Document Block BlockDelimiter BlockLanguage BlockFlag Auto ReadOnly Writable", - maxTerm: 57, + maxTerm: 56, skippedNodes: [0], repeatNodeCount: 2, - tokenData: "5o~ReYZ!d}!O!}#T#U#j#V#W$e#W#X%f#X#Y'X#Z#['r#[#](o#]#^)f#^#_*T#_#`+S#`#a+_#a#b-[#d#e.s#f#g/l#g#h0P#h#i2v#j#k3{#k#l4^#l#m4u#m#n5WR!iP!ZQ%&x%&y!lP!oP%&x%&y!rP!uP%&x%&y!xP!}O]P~#QR#T#U#Z#f#g#`#k#l#e~#`OV~~#eOW~~#jOX~~#mP#b#c#p~#sP#Z#[#v~#yP#i#j#|~$PP#`#a$S~$VP#T#U$Y~$]P#f#g$`~$eO!U~~$hR#`#a$q#d#e$|#g#h%X~$tP#^#_$w~$|Op~~%PP#d#e%S~%XOh~~%^Pj~#g#h%a~%fOf~~%iQ#T#U%o#c#d&Q~%rP#f#g%u~%xP#h#i%{~&QOy~~&TP#V#W&W~&ZP#_#`&^~&aP#X#Y&d~&gP#f#g&j~&mP#Y#Z&p~&sP#]#^&v~&yP#`#a&|~'PP#X#Y'S~'XO|~~'[Q#f#g'b#l#m'm~'eP#`#a'h~'mOr~~'rOq~~'uQ#c#d'{#f#g(Q~(QOo~~(TP#c#d(W~(ZP#c#d(^~(aP#j#k(d~(gP#m#n(j~(oOw~~(rP#h#i(u~(xQ#a#b)O#h#i)Z~)RP#`#a)U~)ZOa~~)^P#d#e)a~)fO!W~~)iP#a#b)l~)oP#T#U)r~)uP#Z#[)x~){P#X#Y*O~*TO!Y~~*WQ#T#U*^#g#h*o~*aP#j#k*d~*gP#T#U*j~*oOd~~*tPs~#c#d*w~*zP#b#c*}~+SO_~~+VP#h#i+Y~+_Ov~~+bR#X#Y+k#]#^,b#i#j-P~+nQ#g#h+t#n#o,P~+wP#g#h+z~,PO!T~~,SP#X#Y,V~,YP#f#g,]~,bO!P~~,eP#e#f,h~,kP#i#j,n~,qP#]#^,t~,wP#W#X,z~-PO!Q~~-SP#T#U-V~-[O}~~-_R#T#U-h#W#X-y#X#Y.O~-kP#h#i-n~-qP#[#]-t~-yO{~~.OOc~~.RP#f#g.U~.XP#a#b.[~._P#T#U.b~.eP#]#^.h~.kP#W#X.n~.sO!X~~.vR#[#]/P#g#h/[#m#n/g~/SP#d#e/V~/[Oe~~/_P!R!S/b~/gOx~~/lO`~~/oQ#U#V/u#g#h/z~/zOk~~0POi~~0SU#T#U0f#V#W0w#[#]1`#e#f1e#j#k1p#k#l2_~0iP#g#h0l~0oP#g#h0r~0wO!S~~0zP#T#U0}~1QP#`#a1T~1WP#T#U1Z~1`Oz~~1eOl~~1hP#`#a1k~1pOb~~1sP#X#Y1v~1yP#`#a1|~2PP#h#i2S~2VP#X#Y2Y~2_O!V~~2bP#]#^2e~2hP#Y#Z2k~2nP#h#i2q~2vOu~~2yR#X#Y3S#c#d3e#g#h3v~3VP#l#m3Y~3]P#h#i3`~3eO^~~3hP#a#b3k~3nP#`#a3q~3vOn~~3{Ot~~4OP#i#j4R~4UP#X#Y4X~4^O!O~~4aP#T#U4d~4gP#g#h4j~4mP#h#i4p~4uO!R~~4xP#a#b4{~5OP#`#a5R~5WOg~~5ZP#T#U5^~5aP#a#b5d~5gP#`#a5j~5oOm~", + tokenData: "4}~RdYZ!a}!O!z#T#U#g#V#W$b#W#X%c#X#Y'U#Z#['o#[#](l#^#_)c#_#`*b#`#a*m#a#b,j#d#e.R#f#g.z#g#h/_#h#i2U#j#k3Z#k#l3l#l#m4T#m#n4fR!fP!YQ%&x%&y!iP!lP%&x%&y!oP!rP%&x%&y!uP!zO]P~!}R#T#U#W#f#g#]#k#l#b~#]OV~~#bOW~~#gOX~~#jP#b#c#m~#pP#Z#[#s~#vP#i#j#y~#|P#`#a$P~$SP#T#U$V~$YP#f#g$]~$bO!U~~$eR#`#a$n#d#e$y#g#h%U~$qP#^#_$t~$yOp~~$|P#d#e%P~%UOh~~%ZPj~#g#h%^~%cOf~~%fQ#T#U%l#c#d%}~%oP#f#g%r~%uP#h#i%x~%}Oy~~&QP#V#W&T~&WP#_#`&Z~&^P#X#Y&a~&dP#f#g&g~&jP#Y#Z&m~&pP#]#^&s~&vP#`#a&y~&|P#X#Y'P~'UO|~~'XQ#f#g'_#l#m'j~'bP#`#a'e~'jOr~~'oOq~~'rQ#c#d'x#f#g'}~'}Oo~~(QP#c#d(T~(WP#c#d(Z~(^P#j#k(a~(dP#m#n(g~(lOw~~(oP#h#i(r~(uQ#a#b({#h#i)W~)OP#`#a)R~)WOa~~)ZP#d#e)^~)cO!W~~)fQ#T#U)l#g#h)}~)oP#j#k)r~)uP#T#U)x~)}Od~~*SPs~#c#d*V~*YP#b#c*]~*bO_~~*eP#h#i*h~*mOv~~*pR#X#Y*y#]#^+p#i#j,_~*|Q#g#h+S#n#o+_~+VP#g#h+Y~+_O!T~~+bP#X#Y+e~+hP#f#g+k~+pO!P~~+sP#e#f+v~+yP#i#j+|~,PP#]#^,S~,VP#W#X,Y~,_O!Q~~,bP#T#U,e~,jO}~~,mR#T#U,v#W#X-X#X#Y-^~,yP#h#i,|~-PP#[#]-S~-XO{~~-^Oc~~-aP#f#g-d~-gP#a#b-j~-mP#T#U-p~-sP#]#^-v~-yP#W#X-|~.RO!X~~.UR#[#]._#g#h.j#m#n.u~.bP#d#e.e~.jOe~~.mP!R!S.p~.uOx~~.zO`~~.}Q#U#V/T#g#h/Y~/YOk~~/_Oi~~/bU#T#U/t#V#W0V#[#]0n#e#f0s#j#k1O#k#l1m~/wP#g#h/z~/}P#g#h0Q~0VO!S~~0YP#T#U0]~0`P#`#a0c~0fP#T#U0i~0nOz~~0sOl~~0vP#`#a0y~1OOb~~1RP#X#Y1U~1XP#`#a1[~1_P#h#i1b~1eP#X#Y1h~1mO!V~~1pP#]#^1s~1vP#Y#Z1y~1|P#h#i2P~2UOu~~2XR#X#Y2b#c#d2s#g#h3U~2eP#l#m2h~2kP#h#i2n~2sO^~~2vP#a#b2y~2|P#`#a3P~3UOn~~3ZOt~~3^P#i#j3a~3dP#X#Y3g~3lO!O~~3oP#T#U3r~3uP#g#h3x~3{P#h#i4O~4TO!R~~4WP#a#b4Z~4^P#`#a4a~4fOg~~4iP#T#U4l~4oP#a#b4r~4uP#`#a4x~4}Om~", tokenizers: [blockContent, 0, 1], topRules: {"Document":[0,2]}, tokenPrec: 0 diff --git a/frontend/src/views/editor/extensions/codeblock/parser.test.ts b/frontend/src/views/editor/extensions/codeblock/parser.test.ts index 329fa979..9dd00b6c 100644 --- a/frontend/src/views/editor/extensions/codeblock/parser.test.ts +++ b/frontend/src/views/editor/extensions/codeblock/parser.test.ts @@ -36,12 +36,4 @@ describe('codeblock delimiter access', () => { expect(blocks[1]?.access).toBe('write'); expect(blocks[1]?.language).toEqual({ name: 'json', auto: true }); }); - - it('accepts image as a valid block language', () => { - expect(parseDelimiter(createDelimiter('image', false, 'write'))).toEqual({ - language: 'image', - auto: false, - access: 'write', - }); - }); }); diff --git a/frontend/src/views/editor/extensions/codeblock/types.ts b/frontend/src/views/editor/extensions/codeblock/types.ts index 948bed54..740a4a38 100644 --- a/frontend/src/views/editor/extensions/codeblock/types.ts +++ b/frontend/src/views/editor/extensions/codeblock/types.ts @@ -81,8 +81,7 @@ export type SupportedLanguage = | 'angular' | 'svelte' | 'http' - | 'mermaid' - | 'image'; + | 'mermaid'; /** * 创建块的选项 diff --git a/frontend/src/views/editor/extensions/contextMenu/blockContext.test.ts b/frontend/src/views/editor/extensions/contextMenu/blockContext.test.ts index 136bb65c..7761fb91 100644 --- a/frontend/src/views/editor/extensions/contextMenu/blockContext.test.ts +++ b/frontend/src/views/editor/extensions/contextMenu/blockContext.test.ts @@ -3,7 +3,7 @@ import {describe, expect, it, vi} from 'vitest'; vi.mock('../codeblock/lang-parser/languages', () => ({ LANGUAGES: [ {token: 'text'}, - {token: 'image'}, + {token: 'http'}, ], })); @@ -19,8 +19,8 @@ describe('runCommandInMenuBlock', () => { doc: [ createDelimiter('text', false, 'write'), 'first block\n', - createDelimiter('image', false, 'write'), - 'img(ref="sha256-1", src="/media/a.png")', + createDelimiter('http', false, 'write'), + 'GET https://example.com', ].join(''), selection: {anchor: 1}, extensions: [blockState], @@ -28,7 +28,7 @@ describe('runCommandInMenuBlock', () => { const blocks = state.field(blockState); const targetBlock = blocks[1]; - const command = vi.fn(view => getActiveNoteBlock(view.state)?.language.name === 'image'); + const command = vi.fn(view => getActiveNoteBlock(view.state)?.language.name === 'http'); const wrapped = runCommandInMenuBlock(command); const view = { @@ -52,7 +52,7 @@ describe('runCommandInMenuBlock', () => { expect(wrapped(view as never, context)).toBe(true); expect(command).toHaveBeenCalledOnce(); - expect(getActiveNoteBlock(state)?.language.name).toBe('image'); + expect(getActiveNoteBlock(state)?.language.name).toBe('http'); expect(state.selection.main.head).toBe(targetBlock.content.from); }); }); diff --git a/frontend/src/views/editor/extensions/imageblock/actions.ts b/frontend/src/views/editor/extensions/imageblock/actions.ts deleted file mode 100644 index d5a5b1eb..00000000 --- a/frontend/src/views/editor/extensions/imageblock/actions.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as MediaHTTPService from '@/../bindings/voidraft/internal/services/mediahttpservice'; -import {writeImageToClipboard} from '@/views/editor/extensions/blockImage/clipboard'; -import {getNoteBlockFromPos} from '@/views/editor/extensions/codeblock/state'; -import {IMAGE_BLOCK_LANGUAGE} from './constants'; -import {getImageBlockItems} from './syntax'; -import type {ImageBlockItem} from './types'; -import {removeImageItemFromBlock} from './document'; -import type {EditorView} from '@codemirror/view'; - -function getClipboardMimeType(blob: Blob): string { - return blob.type.startsWith('image/') ? blob.type : 'image/png'; -} - -async function fetchImageBlob(item: ImageBlockItem): Promise { - const response = await fetch(item.src); - if (!response.ok) { - throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`); - } - - return response.blob(); -} - -export async function copyImageItemToClipboard(item: ImageBlockItem): Promise { - const blob = await fetchImageBlob(item); - await writeImageToClipboard(blob, getClipboardMimeType(blob)); -} - -export async function deleteImageItem(view: EditorView, anchor: number, itemIndex: number): Promise { - const block = getNoteBlockFromPos(view.state, anchor); - if (!block || block.language.name !== IMAGE_BLOCK_LANGUAGE) { - return false; - } - - const items = getImageBlockItems(view.state, block); - const item = items[itemIndex]; - if (!item) { - return false; - } - - const result = await MediaHTTPService.DeleteImage(item.ref); - if (!result?.deleted) { - return false; - } - - return removeImageItemFromBlock(view, block, itemIndex); -} diff --git a/frontend/src/views/editor/extensions/imageblock/clipboard.ts b/frontend/src/views/editor/extensions/imageblock/clipboard.ts deleted file mode 100644 index f84b82a1..00000000 --- a/frontend/src/views/editor/extensions/imageblock/clipboard.ts +++ /dev/null @@ -1,159 +0,0 @@ -import * as MediaHTTPService from '@/../bindings/voidraft/internal/services/mediahttpservice'; -import type {ImageAsset} from '@/../bindings/voidraft/internal/services/models'; -import {EditorView} from '@codemirror/view'; -import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state'; -import {createImageBlockItemFromAsset, insertImageBlockFromClipboard, insertImageItemsIntoBlock} from './document'; -import {getSelectedImageBlock} from './selection'; -import {IMAGE_BLOCK_LANGUAGE} from './constants'; - -type ClipboardImageInput = { - blob: Blob; - filename?: string; - mimeType?: string; -}; - -function getClipboardImageFiles(event: ClipboardEvent): ClipboardImageInput[] { - const files: ClipboardImageInput[] = []; - const items = event.clipboardData?.items; - if (!items) { - return files; - } - - for (let index = 0; index < items.length; index += 1) { - const item = items[index]; - if (item.kind !== 'file' || !item.type.startsWith('image/')) { - continue; - } - - const file = item.getAsFile(); - if (file) { - files.push({ - blob: file, - filename: file.name || undefined, - mimeType: file.type || undefined, - }); - } - } - - return files; -} - -async function readSystemClipboardImageFiles(): Promise { - if (!navigator.clipboard?.read) { - return []; - } - - const clipboardItems = await navigator.clipboard.read(); - const files: ClipboardImageInput[] = []; - - for (const item of clipboardItems) { - for (const type of item.types) { - if (!type.startsWith('image/')) { - continue; - } - - const blob = await item.getType(type); - files.push({ - blob, - filename: blob instanceof File && blob.name ? blob.name : undefined, - mimeType: type || blob.type || undefined, - }); - break; - } - } - - return files; -} - -function readBlobAsDataURL(blob: Blob): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onerror = () => reject(reader.error || new Error('Failed to read clipboard image')); - reader.onload = () => resolve(String(reader.result || '')); - reader.readAsDataURL(blob); - }); -} - -function normalizeClipboardFilename(filename?: string): string | undefined { - const normalized = filename?.trim(); - return normalized ? normalized : undefined; -} - -async function importClipboardImages(files: readonly ClipboardImageInput[]): Promise { - const assets: ImageAsset[] = []; - - for (const file of files) { - const dataURL = await readBlobAsDataURL(file.blob); - const asset = await MediaHTTPService.ImportImage({ - filename: normalizeClipboardFilename(file.filename), - mime_type: file.mimeType || file.blob.type || undefined, - data_base64: dataURL, - }); - - if (asset) { - assets.push(asset); - } - } - - return assets; -} - -async function pasteClipboardImages(view: EditorView, files: readonly ClipboardImageInput[]): Promise { - try { - const assets = await importClipboardImages(files); - if (assets.length === 0) { - return false; - } - - const items = assets.map(createImageBlockItemFromAsset); - const selected = getSelectedImageBlock(view.state); - if (selected) { - insertImageItemsIntoBlock(view, selected.block, items, selected.itemIndex); - return true; - } - - const activeBlock = getActiveNoteBlock(view.state); - if (activeBlock?.language.name === IMAGE_BLOCK_LANGUAGE) { - insertImageItemsIntoBlock(view, activeBlock, items, null); - return true; - } - - insertImageBlockFromClipboard(view, items); - return true; - } catch (error) { - console.error('[imageblock] Failed to import clipboard image:', error); - return false; - } -} - -export function handleImagePasteEvent(view: EditorView, event: ClipboardEvent): boolean { - if (view.state.readOnly) { - return false; - } - - const files = getClipboardImageFiles(event); - if (files.length === 0) { - return false; - } - - event.preventDefault(); - void pasteClipboardImages(view, files); - return true; -} - -export async function pasteImagesFromClipboard(view: EditorView): Promise { - if (view.state.readOnly) { - return false; - } - - try { - const files = await readSystemClipboardImageFiles(); - if (files.length === 0) { - return false; - } - - return pasteClipboardImages(view, files); - } catch { - return false; - } -} diff --git a/frontend/src/views/editor/extensions/imageblock/constants.ts b/frontend/src/views/editor/extensions/imageblock/constants.ts deleted file mode 100644 index c8d85d5c..00000000 --- a/frontend/src/views/editor/extensions/imageblock/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const IMAGE_BLOCK_LANGUAGE = 'image'; -export const TARGET_PREVIEW_HEIGHT = 160; -export const MAX_PREVIEW_WIDTH = 320; diff --git a/frontend/src/views/editor/extensions/imageblock/document.test.ts b/frontend/src/views/editor/extensions/imageblock/document.test.ts deleted file mode 100644 index 1510e764..00000000 --- a/frontend/src/views/editor/extensions/imageblock/document.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import {EditorState} from '@codemirror/state'; -import {describe, expect, it, vi} from 'vitest'; - -vi.mock('@/views/editor/extensions/codeblock/lang-parser/languages', async () => { - const {imageLanguage} = await import('@/views/editor/extensions/imageblock/language/image-language'); - - return { - LANGUAGES: [ - {token: 'text', parser: null}, - {token: 'image', parser: imageLanguage.parser}, - ], - languageMapping: { - image: imageLanguage.parser, - }, - }; -}); - -import {getCodeBlockLanguageExtension} from '@/views/editor/extensions/codeblock/lang-parser'; -import {createDelimiter} from '@/views/editor/extensions/codeblock/parser'; -import {blockState} from '@/views/editor/extensions/codeblock/state'; -import {moveImageItemInBlock, removeImageItemFromBlock} from './document'; -import {imageBlockSelectionField} from './selection'; - -function createView(state: EditorState) { - return { - get state() { - return state; - }, - dispatch(spec: Parameters[0]) { - state = state.update(spec).state; - }, - }; -} - -describe('removeImageItemFromBlock', () => { - it('removes one image record and keeps the block', () => { - let state = EditorState.create({ - doc: [ - createDelimiter('image', false, 'write'), - 'img(ref="sha256-1", src="/media/a.png")\n', - 'img(ref="sha256-2", src="/media/b.png")', - ].join(''), - extensions: [ - blockState, - imageBlockSelectionField, - ...getCodeBlockLanguageExtension(), - ], - }); - - const block = state.field(blockState)[0]; - const view = createView(state); - - expect(removeImageItemFromBlock(view as never, block, 0)).toBe(true); - expect(view.state.doc.toString()).toContain('sha256-2'); - expect(view.state.doc.toString()).not.toContain('sha256-1'); - expect(view.state.field(blockState)).toHaveLength(1); - expect(view.state.field(imageBlockSelectionField)?.itemIndex).toBe(0); - }); - - it('removes the whole image block when the last image is deleted', () => { - let state = EditorState.create({ - doc: [ - createDelimiter('text', false, 'write'), - 'before\n', - createDelimiter('image', false, 'write'), - 'img(ref="sha256-1", src="/media/a.png")', - ].join(''), - extensions: [ - blockState, - imageBlockSelectionField, - ...getCodeBlockLanguageExtension(), - ], - }); - - const block = state.field(blockState)[1]; - const view = createView(state); - - expect(removeImageItemFromBlock(view as never, block, 0)).toBe(true); - expect(view.state.doc.toString()).not.toContain('sha256-1'); - expect(view.state.field(blockState)).toHaveLength(1); - expect(view.state.field(blockState)[0].language.name).toBe('text'); - expect(view.state.field(imageBlockSelectionField, false)).toBeNull(); - }); -}); - -describe('moveImageItemInBlock', () => { - it('moves an image record within the current block', () => { - let state = EditorState.create({ - doc: [ - createDelimiter('image', false, 'write'), - 'img(ref="sha256-1", src="/media/a.png")\n', - 'img(ref="sha256-2", src="/media/b.png")\n', - 'img(ref="sha256-3", src="/media/c.png")', - ].join(''), - extensions: [ - blockState, - imageBlockSelectionField, - ...getCodeBlockLanguageExtension(), - ], - }); - - const block = state.field(blockState)[0]; - const view = createView(state); - - expect(moveImageItemInBlock(view as never, block, 0, 3)).toBe(true); - expect(view.state.doc.toString()).toContain([ - 'img(ref="sha256-2", src="/media/b.png")', - 'img(ref="sha256-3", src="/media/c.png")', - 'img(ref="sha256-1", src="/media/a.png")', - ].join('\n')); - expect(view.state.field(imageBlockSelectionField)?.itemIndex).toBe(2); - }); -}); diff --git a/frontend/src/views/editor/extensions/imageblock/document.ts b/frontend/src/views/editor/extensions/imageblock/document.ts deleted file mode 100644 index 6b79aa7f..00000000 --- a/frontend/src/views/editor/extensions/imageblock/document.ts +++ /dev/null @@ -1,236 +0,0 @@ -import type {ImageAsset} from '@/../bindings/voidraft/internal/services/models'; -import type {EditorView} from '@codemirror/view'; -import {CONTENT_EDIT, USER_EVENTS, codeBlockEvent} from '@/views/editor/extensions/codeblock/annotation'; -import {createDelimiter} from '@/views/editor/extensions/codeblock/parser'; -import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state'; -import type {Block} from '@/views/editor/extensions/codeblock/types'; -import {clearImageBlockSelection, setImageBlockSelection} from './selection'; -import {getImageBlockItems} from './syntax'; -import type {ImageBlockItem} from './types'; - -function quoteAttribute(value: string): string { - return JSON.stringify(value); -} - -function pushOptionalString(attrs: string[], key: string, value?: string) { - if (!value) { - return; - } - attrs.push(`${key}=${quoteAttribute(value)}`); -} - -function pushOptionalNumber(attrs: string[], key: string, value?: number) { - if (!value || value <= 0) { - return; - } - attrs.push(`${key}=${value}`); -} - -export function createImageBlockItemFromAsset(asset: ImageAsset): ImageBlockItem { - return { - ref: asset.id, - src: asset.url, - width: asset.width > 0 ? asset.width : undefined, - height: asset.height > 0 ? asset.height : undefined, - }; -} - -export function serializeImageBlockItem(item: ImageBlockItem): string { - const attrs = [ - `ref=${quoteAttribute(item.ref)}`, - `src=${quoteAttribute(item.src)}`, - ]; - - pushOptionalNumber(attrs, 'width', item.width); - pushOptionalNumber(attrs, 'height', item.height); - pushOptionalString(attrs, 'alt', item.alt); - pushOptionalString(attrs, 'title', item.title); - - return `img(${attrs.join(', ')})`; -} - -export function serializeImageBlockContent(items: readonly ImageBlockItem[]): string { - return items.map(serializeImageBlockItem).join('\n'); -} - -export function insertImageItemsIntoBlock( - view: EditorView, - block: Block, - itemsToInsert: readonly ImageBlockItem[], - afterItemIndex: number | null, -): boolean { - if (itemsToInsert.length === 0 || view.state.readOnly) { - return false; - } - - const currentItems = getImageBlockItems(view.state, block); - const insertAt = afterItemIndex === null - ? currentItems.length - : Math.min(currentItems.length, afterItemIndex + 1); - const nextItems = [ - ...currentItems.slice(0, insertAt), - ...itemsToInsert, - ...currentItems.slice(insertAt), - ]; - - view.dispatch({ - changes: { - from: block.content.from, - to: block.content.to, - insert: serializeImageBlockContent(nextItems), - }, - effects: [ - setImageBlockSelection.of({ - anchor: block.content.from, - itemIndex: insertAt + itemsToInsert.length - 1, - }), - ], - scrollIntoView: true, - userEvent: USER_EVENTS.INPUT_PASTE, - annotations: [codeBlockEvent.of(CONTENT_EDIT)], - }); - - return true; -} - -export function removeImageItemFromBlock( - view: EditorView, - block: Block, - itemIndex: number, -): boolean { - if (view.state.readOnly) { - return false; - } - - const currentItems = getImageBlockItems(view.state, block); - if (itemIndex < 0 || itemIndex >= currentItems.length) { - return false; - } - - const nextItems = currentItems.filter((_, index) => index !== itemIndex); - if (nextItems.length === 0) { - view.dispatch({ - changes: { - from: block.range.from, - to: block.range.to, - insert: '', - }, - effects: [clearImageBlockSelection.of()], - scrollIntoView: true, - userEvent: USER_EVENTS.DELETE, - annotations: [codeBlockEvent.of(CONTENT_EDIT)], - }); - return true; - } - - const nextSelectedIndex = Math.min(itemIndex, nextItems.length - 1); - view.dispatch({ - changes: { - from: block.content.from, - to: block.content.to, - insert: serializeImageBlockContent(nextItems), - }, - effects: [ - setImageBlockSelection.of({ - anchor: block.content.from, - itemIndex: nextSelectedIndex, - }), - ], - scrollIntoView: true, - userEvent: USER_EVENTS.DELETE, - annotations: [codeBlockEvent.of(CONTENT_EDIT)], - }); - - return true; -} - -export function moveImageItemInBlock( - view: EditorView, - block: Block, - fromIndex: number, - toIndex: number, -): boolean { - if (view.state.readOnly) { - return false; - } - - const currentItems = getImageBlockItems(view.state, block); - if ( - fromIndex < 0 - || fromIndex >= currentItems.length - || toIndex < 0 - || toIndex > currentItems.length - ) { - return false; - } - - const nextIndex = toIndex > fromIndex ? toIndex - 1 : toIndex; - if (nextIndex === fromIndex) { - return false; - } - - const nextItems = [...currentItems]; - const [movedItem] = nextItems.splice(fromIndex, 1); - if (!movedItem) { - return false; - } - nextItems.splice(nextIndex, 0, movedItem); - - view.dispatch({ - changes: { - from: block.content.from, - to: block.content.to, - insert: serializeImageBlockContent(nextItems), - }, - effects: [ - setImageBlockSelection.of({ - anchor: block.content.from, - itemIndex: nextIndex, - }), - ], - scrollIntoView: true, - userEvent: USER_EVENTS.MOVE, - annotations: [codeBlockEvent.of(CONTENT_EDIT)], - }); - - return true; -} - -export function insertImageBlockAfterBlock( - view: EditorView, - block: Block, - items: readonly ImageBlockItem[], -): boolean { - if (items.length === 0 || view.state.readOnly) { - return false; - } - - const delimiter = createDelimiter('image', false, 'write'); - const content = serializeImageBlockContent(items); - - view.dispatch({ - changes: { - from: block.content.to, - insert: `${delimiter}${content}`, - }, - effects: [clearImageBlockSelection.of()], - scrollIntoView: true, - userEvent: USER_EVENTS.INPUT_PASTE, - annotations: [codeBlockEvent.of(CONTENT_EDIT)], - }); - - return true; -} - -export function insertImageBlockFromClipboard(view: EditorView, items: readonly ImageBlockItem[]): boolean { - if (items.length === 0 || view.state.readOnly) { - return false; - } - - const activeBlock = getActiveNoteBlock(view.state); - if (!activeBlock) { - return false; - } - - return insertImageBlockAfterBlock(view, activeBlock, items); -} diff --git a/frontend/src/views/editor/extensions/imageblock/drag.ts b/frontend/src/views/editor/extensions/imageblock/drag.ts deleted file mode 100644 index 703bd414..00000000 --- a/frontend/src/views/editor/extensions/imageblock/drag.ts +++ /dev/null @@ -1,211 +0,0 @@ -import type {EditorView} from '@codemirror/view'; -import {getNoteBlockFromPos} from '@/views/editor/extensions/codeblock/state'; -import {IMAGE_BLOCK_LANGUAGE} from './constants'; -import {moveImageItemInBlock} from './document'; - -const IMAGE_BLOCK_ITEM_SELECTOR = '.cm-image-block-item'; -const IMAGE_BLOCK_DRAG_HANDLE_SELECTOR = '.cm-image-block-drag-handle'; -const DROP_BEFORE_CLASS = 'is-drop-before'; -const DROP_AFTER_CLASS = 'is-drop-after'; -const DRAGGING_CLASS = 'is-dragging'; -const ROW_GROUP_TOLERANCE = 8; - -interface FrameLayout { - frame: HTMLElement; - index: number; - rect: DOMRect; -} - -interface RowLayout { - entries: FrameLayout[]; - centerY: number; -} - -interface DropPlacement { - index: number; - markerFrame: HTMLElement; - markerSide: typeof DROP_BEFORE_CLASS | typeof DROP_AFTER_CLASS; -} - -function getFrames(flow: HTMLElement): HTMLElement[] { - return Array.from(flow.querySelectorAll(IMAGE_BLOCK_ITEM_SELECTOR)); -} - -function getFrameIndex(frame: HTMLElement): number | null { - const index = Number(frame.dataset.itemIndex); - return Number.isInteger(index) ? index : null; -} - -function buildRows(frames: readonly HTMLElement[]): RowLayout[] { - const rows: FrameLayout[][] = []; - - frames.forEach((frame, index) => { - const rect = frame.getBoundingClientRect(); - const entry = {frame, index, rect}; - const currentRow = rows.at(-1); - - if (!currentRow || Math.abs(currentRow[0].rect.top - rect.top) > ROW_GROUP_TOLERANCE) { - rows.push([entry]); - return; - } - - currentRow.push(entry); - }); - - return rows.map(entries => ({ - entries, - centerY: entries.reduce((sum, entry) => sum + entry.rect.top + entry.rect.height / 2, 0) / entries.length, - })); -} - -function resolveDropPlacement( - frames: readonly HTMLElement[], - clientX: number, - clientY: number, -): DropPlacement | null { - if (frames.length === 0) { - return null; - } - - const rows = buildRows(frames); - const targetRow = rows.reduce((closest, row) => { - if (!closest) { - return row; - } - - return Math.abs(row.centerY - clientY) < Math.abs(closest.centerY - clientY) ? row : closest; - }, rows[0] as RowLayout); - - const targetEntry = targetRow.entries.find(entry => clientX < entry.rect.left + entry.rect.width / 2); - if (targetEntry) { - return { - index: targetEntry.index, - markerFrame: targetEntry.frame, - markerSide: DROP_BEFORE_CLASS, - }; - } - - const lastEntry = targetRow.entries[targetRow.entries.length - 1]; - return { - index: lastEntry.index + 1, - markerFrame: lastEntry.frame, - markerSide: DROP_AFTER_CLASS, - }; -} - -function clearMarker(frame: HTMLElement | null, side: DropPlacement['markerSide'] | null) { - if (!frame || !side) { - return; - } - - frame.classList.remove(side); -} - -export function attachImageBlockDrag( - flow: HTMLElement, - view: EditorView, - anchor: number, -): void { - const frames = getFrames(flow); - frames.forEach((frame, index) => { - frame.dataset.itemIndex = String(index); - frame.draggable = false; - const handle = frame.querySelector(IMAGE_BLOCK_DRAG_HANDLE_SELECTOR); - if (handle) { - handle.draggable = true; - } - }); - - let sourceIndex: number | null = null; - let sourceFrame: HTMLElement | null = null; - let dropPlacement: DropPlacement | null = null; - - const updatePlacement = (nextPlacement: DropPlacement | null) => { - if ( - dropPlacement?.index === nextPlacement?.index - && dropPlacement?.markerFrame === nextPlacement?.markerFrame - && dropPlacement?.markerSide === nextPlacement?.markerSide - ) { - return; - } - - clearMarker(dropPlacement?.markerFrame ?? null, dropPlacement?.markerSide ?? null); - dropPlacement = nextPlacement; - if (dropPlacement) { - dropPlacement.markerFrame.classList.add(dropPlacement.markerSide); - } - }; - - const resetDragState = () => { - updatePlacement(null); - sourceFrame?.classList.remove(DRAGGING_CLASS); - sourceFrame = null; - sourceIndex = null; - }; - - flow.addEventListener('dragstart', event => { - const target = event.target as HTMLElement | null; - const handle = target?.closest(IMAGE_BLOCK_DRAG_HANDLE_SELECTOR); - const frame = handle?.closest(IMAGE_BLOCK_ITEM_SELECTOR); - if (!frame || !flow.contains(frame)) { - return; - } - if (!frame.classList.contains('is-selected') || frame.classList.contains('is-busy')) { - event.preventDefault(); - return; - } - - const index = getFrameIndex(frame); - if (index === null) { - return; - } - - sourceIndex = index; - sourceFrame = frame; - frame.classList.add(DRAGGING_CLASS); - - if (event.dataTransfer) { - event.dataTransfer.effectAllowed = 'move'; - event.dataTransfer.setData('text/plain', String(index)); - } - }); - - flow.addEventListener('dragover', event => { - if (sourceIndex === null) { - return; - } - - event.preventDefault(); - event.stopPropagation(); - - if (event.dataTransfer) { - event.dataTransfer.dropEffect = 'move'; - } - - updatePlacement(resolveDropPlacement(getFrames(flow), event.clientX, event.clientY)); - }); - - flow.addEventListener('drop', event => { - if (sourceIndex === null) { - return; - } - - event.preventDefault(); - event.stopPropagation(); - - const currentSourceIndex = sourceIndex; - const block = getNoteBlockFromPos(view.state, anchor); - const placement = dropPlacement ?? resolveDropPlacement(getFrames(flow), event.clientX, event.clientY); - resetDragState(); - - if (!block || block.language.name !== IMAGE_BLOCK_LANGUAGE || !placement) { - return; - } - - moveImageItemInBlock(view, block, currentSourceIndex, placement.index); - }); - - flow.addEventListener('dragend', () => { - resetDragState(); - }); -} diff --git a/frontend/src/views/editor/extensions/imageblock/extension.ts b/frontend/src/views/editor/extensions/imageblock/extension.ts deleted file mode 100644 index 5a6f859f..00000000 --- a/frontend/src/views/editor/extensions/imageblock/extension.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type {Extension} from '@codemirror/state'; -import {getImageBlockRenderExtensions} from './render'; -import {imageBlockSelectionField} from './selection'; - -export function getImageBlockExtensions(): Extension[] { - return [ - imageBlockSelectionField, - ...getImageBlockRenderExtensions(), - ]; -} diff --git a/frontend/src/views/editor/extensions/imageblock/index.ts b/frontend/src/views/editor/extensions/imageblock/index.ts deleted file mode 100644 index a28c61b3..00000000 --- a/frontend/src/views/editor/extensions/imageblock/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export {getImageBlockExtensions} from './extension'; -export * from './types'; -export * from './language'; diff --git a/frontend/src/views/editor/extensions/imageblock/language/build-parser.js b/frontend/src/views/editor/extensions/imageblock/language/build-parser.js deleted file mode 100644 index 6cf251d2..00000000 --- a/frontend/src/views/editor/extensions/imageblock/language/build-parser.js +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env node - -import { execSync } from 'child_process'; -import path from 'path'; -import fs from 'fs'; -import { fileURLToPath } from 'url'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -console.log('Building image grammar parser...'); - -try { - const grammarFile = path.join(__dirname, 'image.grammar'); - if (!fs.existsSync(grammarFile)) { - throw new Error('Grammar file image.grammar not found'); - } - - execSync('npx lezer-generator image.grammar -o image.parser.ts --typeScript', { - cwd: __dirname, - stdio: 'inherit', - }); - - const parserFile = path.join(__dirname, 'image.parser.ts'); - const termsFile = path.join(__dirname, 'image.parser.terms.ts'); - - if (!fs.existsSync(parserFile) || !fs.existsSync(termsFile)) { - throw new Error('Failed to generate image parser artifacts'); - } - - console.log('Generated image.parser.ts and image.parser.terms.ts'); -} catch (error) { - console.error('Failed to build image grammar parser:', error instanceof Error ? error.message : error); - process.exit(1); -} diff --git a/frontend/src/views/editor/extensions/imageblock/language/image-language.ts b/frontend/src/views/editor/extensions/imageblock/language/image-language.ts deleted file mode 100644 index 509d5c25..00000000 --- a/frontend/src/views/editor/extensions/imageblock/language/image-language.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { LRLanguage, LanguageSupport } from '@codemirror/language'; -import { parser } from './image.parser'; -import { imageHighlighting } from './image.highlight'; - -export const imageLanguage = LRLanguage.define({ - parser: parser.configure({ - props: [imageHighlighting], - }), - languageData: { - closeBrackets: { brackets: ['(', '"', "'"] }, - wordChars: '-_', - }, -}); - -export function image() { - return new LanguageSupport(imageLanguage); -} diff --git a/frontend/src/views/editor/extensions/imageblock/language/image.grammar b/frontend/src/views/editor/extensions/imageblock/language/image.grammar deleted file mode 100644 index e3c876b3..00000000 --- a/frontend/src/views/editor/extensions/imageblock/language/image.grammar +++ /dev/null @@ -1,112 +0,0 @@ -// Image block grammar -// -// Example: -// img(ref="media-id-1", src="/media/2026/04/a.png", alt="封面", width=320, height=180, title="封面图") -// img(ref="media-id-2", src="/media/2026/04/b.png", alt="流程图", width=240, height=240) -// img(ref="media-id-3", src="https://example.com/demo.png", alt="外链图") - -@skip { whitespace | LineComment } - -@top Document { ImageElement* } - -ImgKeyword { - @specialize[@name=ImgKeyword] -} - -ImageElement { - ImgKeyword ImageConfig -} - -ImageConfig { - "(" RequiredAttributePair ")" | - "(" RequiredAttributePair "," OptionalAttributeList ")" -} - -RequiredAttributePair { - RefAttribute "," SrcAttribute | - SrcAttribute "," RefAttribute -} - -OptionalAttribute { - WidthAttribute | - HeightAttribute | - AltAttribute | - TitleAttribute -} - -OptionalAttributeList { - OptionalAttribute ("," OptionalAttribute)* -} - -RefAttribute { - RefAttributeName "=" StringLiteral -} - -SrcAttribute { - SrcAttributeName "=" StringLiteral -} - -WidthAttribute { - WidthAttributeName "=" NumberLiteral -} - -HeightAttribute { - HeightAttributeName "=" NumberLiteral -} - -AltAttribute { - AltAttributeName "=" StringLiteral -} - -TitleAttribute { - TitleAttributeName "=" StringLiteral -} - -RefAttributeName { - @specialize[@name=RefAttributeName] -} - -SrcAttributeName { - @specialize[@name=SrcAttributeName] -} - -WidthAttributeName { - @specialize[@name=WidthAttributeName] -} - -HeightAttributeName { - @specialize[@name=HeightAttributeName] -} - -AltAttributeName { - @specialize[@name=AltAttributeName] -} - -TitleAttributeName { - @specialize[@name=TitleAttributeName] -} - -@tokens { - LineComment { "#" ![\n]* } - - identifier { - (@asciiLetter | $[_$]) - (@asciiLetter | @digit | $[_$])* - } - - whitespace { @whitespace+ } - - NumberLiteral { - ("+" | "-")? (@digit+ ("." @digit*)? | "." @digit+) - (("e" | "E") ("+" | "-")? @digit+)? - } - - StringLiteral[isolate] { - "\"" (!["\n\\] | "\\" _)* "\"" | - "'" (!['\n\\] | "\\" _)* "'" - } - - "=" "," "(" ")" -} - -@detectDelim diff --git a/frontend/src/views/editor/extensions/imageblock/language/image.grammar.test.ts b/frontend/src/views/editor/extensions/imageblock/language/image.grammar.test.ts deleted file mode 100644 index ece73d68..00000000 --- a/frontend/src/views/editor/extensions/imageblock/language/image.grammar.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { parser } from './image.parser'; - -function parseCode(code: string) { - return parser.parse(code); -} - -function getErrorNodes(tree: any, code: string) { - const errors: Array<{ from: number; to: number; text: string }> = []; - - tree.iterate({ - enter: (node: any) => { - if (node.type?.isError || node.name === '⚠') { - errors.push({ - from: node.from, - to: node.to, - text: code.slice(node.from, node.to), - }); - } - }, - }); - - return errors; -} - -function countNodes(tree: any, name: string) { - let count = 0; - - tree.iterate({ - enter: (node: any) => { - if (node.name === name) { - count += 1; - } - }, - }); - - return count; -} - -describe('image grammar', () => { - it('parses flat img records with required ref and src', () => { - const code = `img(ref="media-id-1", src="/media/2026/04/a.png", alt="封面", width=320, height=180, title="封面图") -img(ref="media-id-2", src="/media/2026/04/b.png", alt="流程图", width=240, height=240) -img(ref="media-id-3", src="https://example.com/demo.png", alt="外链图")`; - - const tree = parseCode(code); - const errors = getErrorNodes(tree, code); - - expect(errors).toHaveLength(0); - expect(countNodes(tree, 'ImageElement')).toBe(3); - expect(countNodes(tree, 'RefAttribute')).toBe(3); - expect(countNodes(tree, 'SrcAttribute')).toBe(3); - expect(countNodes(tree, 'WidthAttribute')).toBe(2); - expect(countNodes(tree, 'HeightAttribute')).toBe(2); - expect(countNodes(tree, 'AltAttribute')).toBe(3); - expect(countNodes(tree, 'TitleAttribute')).toBe(1); - }); - - it('supports required attributes in either order', () => { - const code = `img(src="/media/2026/04/a.png", ref="media-id-1", alt="封面") -img(ref="media-id-2", src="/media/2026/04/b.png")`; - - const tree = parseCode(code); - const errors = getErrorNodes(tree, code); - - expect(errors).toHaveLength(0); - expect(countNodes(tree, 'ImageElement')).toBe(2); - expect(countNodes(tree, 'RefAttribute')).toBe(2); - expect(countNodes(tree, 'SrcAttribute')).toBe(2); - }); - - it('rejects plain text lines', () => { - const code = `hello world -img(ref="media-id-1", src="/media/2026/04/a.png")`; - - const tree = parseCode(code); - const errors = getErrorNodes(tree, code); - - expect(errors.length).toBeGreaterThan(0); - }); - - it('rejects records missing ref or src', () => { - const code = `img(ref="media-id-1", alt="only ref") -img(src="/media/2026/04/a.png", alt="only src")`; - - const tree = parseCode(code); - const errors = getErrorNodes(tree, code); - - expect(errors.length).toBeGreaterThan(0); - }); -}); diff --git a/frontend/src/views/editor/extensions/imageblock/language/image.highlight.ts b/frontend/src/views/editor/extensions/imageblock/language/image.highlight.ts deleted file mode 100644 index b17a1103..00000000 --- a/frontend/src/views/editor/extensions/imageblock/language/image.highlight.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { styleTags, tags as t } from '@lezer/highlight'; - -export const imageHighlighting = styleTags({ - ImgKeyword: t.keyword, - - 'RefAttributeName SrcAttributeName WidthAttributeName HeightAttributeName AltAttributeName TitleAttributeName': - t.attributeName, - - StringLiteral: t.string, - NumberLiteral: t.number, - 'True False': t.bool, - Null: t.null, - - LineComment: t.lineComment, - - '=': t.definitionOperator, - ',': t.separator, - '( )': t.paren, -}); diff --git a/frontend/src/views/editor/extensions/imageblock/language/image.parser.terms.ts b/frontend/src/views/editor/extensions/imageblock/language/image.parser.terms.ts deleted file mode 100644 index 80972a23..00000000 --- a/frontend/src/views/editor/extensions/imageblock/language/image.parser.terms.ts +++ /dev/null @@ -1,24 +0,0 @@ -// This file was generated by lezer-generator. You probably shouldn't edit it. -export const - LineComment = 1, - Document = 2, - ImageElement = 3, - ImgKeyword = 5, - ImageConfig = 6, - RequiredAttributePair = 8, - RefAttribute = 9, - RefAttributeName = 11, - StringLiteral = 13, - SrcAttribute = 15, - SrcAttributeName = 17, - OptionalAttributeList = 19, - OptionalAttribute = 20, - WidthAttribute = 21, - WidthAttributeName = 23, - NumberLiteral = 24, - HeightAttribute = 25, - HeightAttributeName = 27, - AltAttribute = 28, - AltAttributeName = 30, - TitleAttribute = 31, - TitleAttributeName = 33 diff --git a/frontend/src/views/editor/extensions/imageblock/language/image.parser.ts b/frontend/src/views/editor/extensions/imageblock/language/image.parser.ts deleted file mode 100644 index 93f6e80e..00000000 --- a/frontend/src/views/editor/extensions/imageblock/language/image.parser.ts +++ /dev/null @@ -1,21 +0,0 @@ -// This file was generated by lezer-generator. You probably shouldn't edit it. -import {LRParser} from "@lezer/lr" -const spec_identifier = {__proto__:null,img:10, ref:22, src:34, width:46, height:54, alt:60, title:66} -export const parser = LRParser.deserialize({ - version: 14, - states: "'[QYQPOOOOQO'#C`'#C`O_QPO'#C_OOQO'#DO'#DOQYQPOOOdQPO'#CbOOQO,58y,58yOOQO-E6|-E6|OOQO'#Cf'#CfOlQPO'#CeOOQO'#Cl'#ClOqQPO'#CkOvQPO'#CdO{QPO'#CdO!QQPO,58|O!YQPO,59PO!_QPO,59VOgQPO,59OO!dQPO,59OOOQO1G.h1G.hO!iQPO1G.hOOQO1G.k1G.kOOQO1G.q1G.qOOQO1G.j1G.jOOQO'#Cr'#CrO!wQPO'#CqOOQO'#Cv'#CvO!|QPO'#CuOOQO'#Cy'#CyO#RQPO'#CxOOQO'#C|'#C|O#WQPO'#C{OOQO'#Cp'#CpO#]QPO'#CoO#eQPO7+$SO#jQPO,59]O#oQPO,59aO#tQPO,59dO#yQPO,59gO!iQPO'#DPO$OQPO,59ZOOQO< spec_identifier[value] || -1}], - tokenPrec: 0 -}) diff --git a/frontend/src/views/editor/extensions/imageblock/language/index.ts b/frontend/src/views/editor/extensions/imageblock/language/index.ts deleted file mode 100644 index 1169161b..00000000 --- a/frontend/src/views/editor/extensions/imageblock/language/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { image, imageLanguage } from './image-language'; -export { parser } from './image.parser'; -export type { LRLanguage } from '@codemirror/language'; diff --git a/frontend/src/views/editor/extensions/imageblock/render.ts b/frontend/src/views/editor/extensions/imageblock/render.ts deleted file mode 100644 index 27a06db6..00000000 --- a/frontend/src/views/editor/extensions/imageblock/render.ts +++ /dev/null @@ -1,268 +0,0 @@ -import {Extension, RangeSetBuilder, StateField} from '@codemirror/state'; -import { - Decoration, - type DecorationSet, - EditorView, -} from '@codemirror/view'; -import {blockState} from '@/views/editor/extensions/codeblock/state'; -import {IMAGE_BLOCK_LANGUAGE, TARGET_PREVIEW_HEIGHT} from './constants'; -import {getSelectedImageBlock, imageBlockSelectionField} from './selection'; -import {getImageBlockItemMap} from './syntax'; -import type {ImageBlockItem, ImageBlockSelection} from './types'; -import {ImageBlockWidget} from './widget'; - -const EMPTY_IMAGE_ITEM_MAP = new Map(); - -function isSameImageSelection( - previous: ImageBlockSelection | null | undefined, - next: ImageBlockSelection | null | undefined, -): boolean { - if (!previous || !next) { - return previous === next; - } - - return previous.anchor === next.anchor && previous.itemIndex === next.itemIndex; -} - -function isSelectionInImageBlock(state: EditorView['state'], from: number, to: number): boolean { - return state.selection.ranges.some(range => range.from <= to && from <= range.to); -} - -function buildImageBlockDecorations(state: EditorView['state']): DecorationSet { - const blocks = state.field(blockState, false) ?? []; - const itemsByBlock = state.field(imageBlockItemsField, false) ?? EMPTY_IMAGE_ITEM_MAP; - const selected = getSelectedImageBlock(state); - const builder = new RangeSetBuilder(); - - for (const block of blocks) { - if (block.language.name !== IMAGE_BLOCK_LANGUAGE) { - continue; - } - if (block.content.from >= block.content.to) { - continue; - } - if (isSelectionInImageBlock(state, block.content.from, block.content.to)) { - continue; - } - - const items = itemsByBlock.get(block.content.from) ?? []; - if (items.length === 0) { - continue; - } - - builder.add( - block.content.from, - block.content.to, - Decoration.replace({ - block: true, - inclusive: true, - widget: new ImageBlockWidget( - block.content.from, - items, - selected?.block.content.from === block.content.from ? selected.itemIndex : null, - ), - }), - ); - } - - return builder.finish(); -} - -const imageBlockItemsField = StateField.define>({ - create: getImageBlockItemMap, - update(items, transaction) { - return transaction.docChanged ? getImageBlockItemMap(transaction.state) : items; - }, -}); - -const imageBlockDecorationField = StateField.define({ - create: buildImageBlockDecorations, - update(decorations, transaction) { - const previousSelection = transaction.startState.field(imageBlockSelectionField, false); - const nextSelection = transaction.state.field(imageBlockSelectionField, false); - - if ( - transaction.docChanged - || Boolean(transaction.selection) - || !isSameImageSelection(previousSelection, nextSelection) - ) { - return buildImageBlockDecorations(transaction.state); - } - - return decorations; - }, - provide: field => EditorView.decorations.from(field), -}); - -const imageBlockTheme = EditorView.baseTheme({ - '.cm-image-block-widget': { - padding: '8px 0 6px', - }, - '.cm-image-block-flow': { - display: 'flex', - flexWrap: 'wrap', - alignItems: 'flex-start', - justifyContent: 'flex-start', - gap: '12px', - }, - '.cm-image-block-item': { - position: 'relative', - display: 'flex', - alignItems: 'stretch', - justifyContent: 'center', - flex: '0 0 auto', - maxWidth: '100%', - minWidth: '48px', - minHeight: '48px', - overflow: 'visible', - border: '1px solid rgba(127, 127, 127, 0.18)', - background: ` - linear-gradient(45deg, rgba(127, 127, 127, 0.08) 25%, transparent 25%), - linear-gradient(-45deg, rgba(127, 127, 127, 0.08) 25%, transparent 25%), - linear-gradient(45deg, transparent 75%, rgba(127, 127, 127, 0.08) 75%), - linear-gradient(-45deg, transparent 75%, rgba(127, 127, 127, 0.08) 75%) - `, - backgroundColor: 'rgba(127, 127, 127, 0.04)', - backgroundSize: '12px 12px', - backgroundPosition: '0 0, 0 6px, 6px -6px, -6px 0', - }, - '.cm-image-block-item.is-selected': { - outline: '1px solid var(--cm-selection-border, #4a9eff)', - outlineOffset: '1px', - zIndex: '2', - }, - '.cm-image-block-item.is-busy': { - cursor: 'progress', - }, - '.cm-image-block-item.is-error': { - borderStyle: 'dashed', - }, - '.cm-image-block-item.is-dragging': { - opacity: '0.48', - zIndex: '3', - }, - '.cm-image-block-item.is-dragging .cm-image-block-actions': { - opacity: '0', - pointerEvents: 'none', - }, - '.cm-image-block-item.is-drop-before::before': { - content: '""', - position: 'absolute', - top: '0', - bottom: '0', - left: '-7px', - width: '2px', - background: 'var(--cm-selection-border, #4a9eff)', - }, - '.cm-image-block-item.is-drop-after::after': { - content: '""', - position: 'absolute', - top: '0', - bottom: '0', - right: '-7px', - width: '2px', - background: 'var(--cm-selection-border, #4a9eff)', - }, - '.cm-image-block-thumb': { - display: 'block', - width: 'auto', - height: 'auto', - maxHeight: `${TARGET_PREVIEW_HEIGHT}px`, - maxWidth: 'min(320px, calc(100vw - 96px))', - objectFit: 'contain', - opacity: '0', - transition: 'opacity 0.16s ease', - }, - '.cm-image-block-item.is-loaded .cm-image-block-thumb': { - opacity: '1', - }, - '.cm-image-block-actions': { - position: 'absolute', - top: '0', - right: '-36px', - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-start', - gap: '6px', - opacity: '0', - pointerEvents: 'none', - transition: 'opacity 0.12s ease', - zIndex: '1', - }, - '.cm-image-block-item.is-selected .cm-image-block-actions': { - opacity: '1', - pointerEvents: 'auto', - }, - '.cm-image-block-drag-handle': { - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - width: '28px', - height: '28px', - border: '1px solid var(--border-color, rgba(127, 127, 127, 0.24))', - background: 'var(--bg-primary, #ffffff)', - color: 'var(--text-secondary, #666666)', - boxShadow: '0 2px 8px rgba(0, 0, 0, 0.18)', - cursor: 'grab', - userSelect: 'none', - outline: 'none', - transition: 'background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease, opacity 0.12s ease', - }, - '.cm-image-block-drag-handle:hover': { - background: 'var(--bg-hover, rgba(127, 127, 127, 0.08))', - color: 'var(--text-primary, #111111)', - }, - '.cm-image-block-drag-handle:focus-visible': { - borderColor: 'var(--cm-selection-border, #4a9eff)', - }, - '.cm-image-block-item.is-dragging .cm-image-block-drag-handle': { - cursor: 'grabbing', - }, - '.cm-image-block-item.is-busy .cm-image-block-drag-handle': { - opacity: '0.55', - pointerEvents: 'none', - }, - '.cm-image-block-drag-handle svg': { - width: '14px', - height: '14px', - fill: 'currentColor', - }, - '.cm-image-block-action': { - appearance: 'none', - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - width: '28px', - height: '28px', - padding: '0', - border: '1px solid var(--border-color, rgba(127, 127, 127, 0.24))', - background: 'var(--bg-primary, #ffffff)', - color: 'var(--text-primary, #111111)', - boxShadow: '0 2px 8px rgba(0, 0, 0, 0.18)', - cursor: 'pointer', - outline: 'none', - transition: 'background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease, opacity 0.12s ease', - }, - '.cm-image-block-action:hover:not(:disabled)': { - background: 'var(--bg-hover, rgba(127, 127, 127, 0.08))', - }, - '.cm-image-block-action:disabled': { - opacity: '0.55', - cursor: 'default', - }, - '.cm-image-block-action:focus-visible': { - borderColor: 'var(--cm-selection-border, #4a9eff)', - }, - '.cm-image-block-action svg': { - width: '14px', - height: '14px', - fill: 'currentColor', - }, - '.cm-image-block-action.is-delete': { - color: 'var(--text-danger, #d14b4b)', - }, -}); - -export function getImageBlockRenderExtensions(): Extension[] { - return [imageBlockItemsField, imageBlockDecorationField, imageBlockTheme]; -} diff --git a/frontend/src/views/editor/extensions/imageblock/selection.ts b/frontend/src/views/editor/extensions/imageblock/selection.ts deleted file mode 100644 index ccd968bb..00000000 --- a/frontend/src/views/editor/extensions/imageblock/selection.ts +++ /dev/null @@ -1,69 +0,0 @@ -import {StateEffect, StateField} from '@codemirror/state'; -import {getBlockFromPos} from '@/views/editor/extensions/codeblock/parser'; -import type {Block} from '@/views/editor/extensions/codeblock/types'; -import {IMAGE_BLOCK_LANGUAGE} from './constants'; -import type {ImageBlockSelection} from './types'; - -export const setImageBlockSelection = StateEffect.define({ - map(value, changes) { - return { - ...value, - anchor: changes.mapPos(value.anchor, 1), - }; - }, -}); - -export const clearImageBlockSelection = StateEffect.define(); - -export const imageBlockSelectionField = StateField.define({ - create: () => null, - update(value, transaction) { - if (value) { - value = { - ...value, - anchor: transaction.changes.mapPos(value.anchor, 1), - }; - } - - for (const effect of transaction.effects) { - if (effect.is(clearImageBlockSelection)) { - return null; - } - if (effect.is(setImageBlockSelection)) { - return effect.value; - } - } - - if (transaction.selection) { - return null; - } - - if (!value) { - return null; - } - - const block = getBlockFromPos(transaction.state, value.anchor); - if (!block || block.language.name !== IMAGE_BLOCK_LANGUAGE) { - return null; - } - - return value; - }, -}); - -export function getSelectedImageBlock(state: Parameters[0]): { block: Block; itemIndex: number | null } | null { - const selection = state.field(imageBlockSelectionField, false); - if (!selection) { - return null; - } - - const block = getBlockFromPos(state, selection.anchor); - if (!block || block.language.name !== IMAGE_BLOCK_LANGUAGE) { - return null; - } - - return { - block, - itemIndex: selection.itemIndex, - }; -} diff --git a/frontend/src/views/editor/extensions/imageblock/syntax.test.ts b/frontend/src/views/editor/extensions/imageblock/syntax.test.ts deleted file mode 100644 index fd972ab3..00000000 --- a/frontend/src/views/editor/extensions/imageblock/syntax.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import {EditorState} from '@codemirror/state'; -import {describe, expect, it, vi} from 'vitest'; - -vi.mock('@/views/editor/extensions/codeblock/lang-parser/languages', async () => { - const {imageLanguage} = await import('@/views/editor/extensions/imageblock/language/image-language'); - - return { - LANGUAGES: [ - {token: 'text', parser: null}, - {token: 'image', parser: imageLanguage.parser}, - ], - languageMapping: { - image: imageLanguage.parser, - }, - }; -}); - -import {getCodeBlockLanguageExtension} from '@/views/editor/extensions/codeblock/lang-parser'; -import {createDelimiter} from '@/views/editor/extensions/codeblock/parser'; -import {blockState} from '@/views/editor/extensions/codeblock/state'; -import {getImageBlockItems} from './syntax'; - -describe('image block syntax integration', () => { - it('reads image items from the nested image parser inside a code block', () => { - const doc = [ - createDelimiter('image', false, 'write'), - 'img(ref="sha256-1", src="/media/a.png", alt="cover", width=320, height=180, title="cover image")\n', - 'img(src="https://example.com/demo.png", ref="sha256-2", width=240, height=240)', - ].join(''); - - const state = EditorState.create({ - doc, - extensions: [ - blockState, - ...getCodeBlockLanguageExtension(), - ], - }); - - const [block] = state.field(blockState); - expect(block?.language.name).toBe('image'); - - expect(getImageBlockItems(state, block!)).toEqual([ - { - ref: 'sha256-1', - src: '/media/a.png', - alt: 'cover', - title: 'cover image', - width: 320, - height: 180, - }, - { - ref: 'sha256-2', - src: 'https://example.com/demo.png', - alt: undefined, - title: undefined, - width: 240, - height: 240, - }, - ]); - }); -}); diff --git a/frontend/src/views/editor/extensions/imageblock/syntax.ts b/frontend/src/views/editor/extensions/imageblock/syntax.ts deleted file mode 100644 index 576dabc5..00000000 --- a/frontend/src/views/editor/extensions/imageblock/syntax.ts +++ /dev/null @@ -1,182 +0,0 @@ -import {ensureSyntaxTree} from '@codemirror/language'; -import type {EditorState} from '@codemirror/state'; -import type {SyntaxNode, Tree} from '@lezer/common'; -import {blockState} from '@/views/editor/extensions/codeblock/state'; -import type {Block} from '@/views/editor/extensions/codeblock/types'; -import {IMAGE_BLOCK_LANGUAGE} from './constants'; -import type {ImageBlockItem} from './types'; - -const NODE_NAMES = { - imageElement: 'ImageElement', - imageConfig: 'ImageConfig', - requiredAttributePair: 'RequiredAttributePair', - optionalAttributeList: 'OptionalAttributeList', - optionalAttribute: 'OptionalAttribute', - refAttribute: 'RefAttribute', - srcAttribute: 'SrcAttribute', - widthAttribute: 'WidthAttribute', - heightAttribute: 'HeightAttribute', - altAttribute: 'AltAttribute', - titleAttribute: 'TitleAttribute', - stringLiteral: 'StringLiteral', - numberLiteral: 'NumberLiteral', -} as const; - -const EMPTY_OPTIONAL_ATTRIBUTE_MAP = new Map(); -const EMPTY_IMAGE_ITEM_MAP = new Map(); - -function parseQuotedLiteral(raw: string): string { - if (raw.length < 2) { - return raw; - } - - const quote = raw[0]; - if ((quote !== '"' && quote !== '\'') || raw[raw.length - 1] !== quote) { - return raw; - } - - if (quote === '"') { - try { - return JSON.parse(raw); - } catch { - // Keep the raw token if JSON parsing fails. - } - } - - let result = ''; - let escaping = false; - for (const char of raw.slice(1, -1)) { - if (escaping) { - if (char === 'n') result += '\n'; - else if (char === 'r') result += '\r'; - else if (char === 't') result += '\t'; - else result += char; - escaping = false; - continue; - } - - if (char === '\\') { - escaping = true; - continue; - } - - result += char; - } - - return escaping ? `${result}\\` : result; -} - -function readStringLiteral(state: EditorState, attribute: SyntaxNode | null | undefined): string | undefined { - const valueNode = attribute?.getChild(NODE_NAMES.stringLiteral); - if (!valueNode) { - return undefined; - } - - return parseQuotedLiteral(state.sliceDoc(valueNode.from, valueNode.to)); -} - -function readNumberLiteral(state: EditorState, attribute: SyntaxNode | null | undefined): number | undefined { - const valueNode = attribute?.getChild(NODE_NAMES.numberLiteral); - if (!valueNode) { - return undefined; - } - - const value = Number(state.sliceDoc(valueNode.from, valueNode.to)); - return Number.isFinite(value) && value > 0 ? value : undefined; -} - -function getOptionalAttributes(config: SyntaxNode): ReadonlyMap { - const optionalList = config.getChild(NODE_NAMES.optionalAttributeList); - if (!optionalList) { - return EMPTY_OPTIONAL_ATTRIBUTE_MAP; - } - - return new Map( - optionalList - .getChildren(NODE_NAMES.optionalAttribute) - .flatMap(optionalAttribute => { - const attribute = optionalAttribute.firstChild; - return attribute ? [[attribute.name, attribute] as const] : []; - }), - ); -} - -function readImageElement(state: EditorState, node: SyntaxNode): ImageBlockItem | null { - const config = node.getChild(NODE_NAMES.imageConfig); - const requiredAttributes = config?.getChild(NODE_NAMES.requiredAttributePair); - if (!config || !requiredAttributes) { - return null; - } - - const ref = readStringLiteral(state, requiredAttributes.getChild(NODE_NAMES.refAttribute)); - const src = readStringLiteral(state, requiredAttributes.getChild(NODE_NAMES.srcAttribute)); - if (!ref || !src) { - return null; - } - - const optionalAttributes = getOptionalAttributes(config); - - return { - ref, - src, - alt: readStringLiteral(state, optionalAttributes.get(NODE_NAMES.altAttribute)), - title: readStringLiteral(state, optionalAttributes.get(NODE_NAMES.titleAttribute)), - width: readNumberLiteral(state, optionalAttributes.get(NODE_NAMES.widthAttribute)), - height: readNumberLiteral(state, optionalAttributes.get(NODE_NAMES.heightAttribute)), - }; -} - -export function isImageBlock(block: Block | null | undefined): block is Block { - return Boolean(block && block.language.name === IMAGE_BLOCK_LANGUAGE); -} - -function collectImageBlockItems(state: EditorState, tree: Tree, block: Block): ImageBlockItem[] { - const items: ImageBlockItem[] = []; - tree.iterate({ - from: block.content.from, - to: block.content.to, - enter: nodeRef => { - if (nodeRef.name !== NODE_NAMES.imageElement) { - return; - } - - const item = readImageElement(state, nodeRef.node); - if (item) { - items.push(item); - } - }, - }); - - return items; -} - -export function getImageBlockItems(state: EditorState, block: Block): ImageBlockItem[] { - if (!isImageBlock(block)) { - return []; - } - - const tree = ensureSyntaxTree(state, state.doc.length, 1000); - if (!tree) { - return []; - } - - return collectImageBlockItems(state, tree, block); -} - -export function getImageBlockItemMap(state: EditorState): ReadonlyMap { - const blocks = (state.field(blockState, false) ?? []).filter(isImageBlock); - if (blocks.length === 0) { - return EMPTY_IMAGE_ITEM_MAP; - } - - const tree = ensureSyntaxTree(state, state.doc.length, 1000); - if (!tree) { - return EMPTY_IMAGE_ITEM_MAP; - } - - const entries = blocks - .map(block => [block.content.from, collectImageBlockItems(state, tree, block)] as const) - .filter(([, items]) => items.length > 0); - - return entries.length > 0 ? new Map(entries) : EMPTY_IMAGE_ITEM_MAP; -} diff --git a/frontend/src/views/editor/extensions/imageblock/types.ts b/frontend/src/views/editor/extensions/imageblock/types.ts deleted file mode 100644 index 87464728..00000000 --- a/frontend/src/views/editor/extensions/imageblock/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface ImageBlockItem { - ref: string; - src: string; - alt?: string; - title?: string; - width?: number; - height?: number; -} - -export interface ImageBlockSelection { - anchor: number; - itemIndex: number | null; -} diff --git a/frontend/src/views/editor/extensions/imageblock/widget.ts b/frontend/src/views/editor/extensions/imageblock/widget.ts deleted file mode 100644 index 051daeaf..00000000 --- a/frontend/src/views/editor/extensions/imageblock/widget.ts +++ /dev/null @@ -1,303 +0,0 @@ -import {EditorView, WidgetType} from '@codemirror/view'; -import {CONTEXT_MENU_BLOCK_ANCHOR_DATASET} from '@/views/editor/extensions/contextMenu/constants'; -import i18n from '@/i18n'; -import {copyImageItemToClipboard, deleteImageItem} from './actions'; -import {MAX_PREVIEW_WIDTH, TARGET_PREVIEW_HEIGHT} from './constants'; -import {attachImageBlockDrag} from './drag'; -import {setImageBlockSelection} from './selection'; -import type {ImageBlockItem} from './types'; - -const ITEM_SIGNATURE_SEPARATOR = '\u001f'; -const t = (key: string) => i18n.global.t(key); - -function getPreviewWidth(width: number, height: number): number { - return Math.min( - MAX_PREVIEW_WIDTH, - Math.max(48, Math.round((TARGET_PREVIEW_HEIGHT * width) / height)), - ); -} - -function applyPreviewSize(element: HTMLElement, width?: number, height?: number) { - if (!width || !height || width <= 0 || height <= 0) { - return; - } - - element.style.width = `${getPreviewWidth(width, height)}px`; - element.style.aspectRatio = `${width} / ${height}`; -} - -function getItemSignature(item: ImageBlockItem): string { - return [ - item.ref, - item.src, - item.alt ?? '', - item.title ?? '', - item.width ?? '', - item.height ?? '', - ].join(ITEM_SIGNATURE_SEPARATOR); -} - -function areItemsEqual(left: readonly ImageBlockItem[], right: readonly ImageBlockItem[]): boolean { - if (left.length !== right.length) { - return false; - } - - return left.every((item, index) => { - const other = right[index]; - return item.ref === other?.ref - && item.src === other?.src - && item.alt === other?.alt - && item.title === other?.title - && item.width === other?.width - && item.height === other?.height; - }); -} - -function setSelectionState(frame: HTMLElement, selected: boolean) { - frame.classList.toggle('is-selected', selected); -} - -function getImageTitle(item: ImageBlockItem): string | undefined { - return item.title ?? item.alt; -} - -function applyLoadedState(frame: HTMLElement, image: HTMLImageElement) { - if (image.naturalWidth > 0 && image.naturalHeight > 0) { - applyPreviewSize(frame, image.naturalWidth, image.naturalHeight); - } - - frame.classList.remove('is-loading'); - frame.classList.add('is-loaded'); -} - -function createImageFrame( - view: EditorView, - anchor: number, - item: ImageBlockItem, - index: number, - selectedIndex: number | null, -): HTMLElement { - const frame = document.createElement('div'); - frame.className = 'cm-image-block-item is-loading'; - frame.dataset.signature = getItemSignature(item); - setSelectionState(frame, selectedIndex === index); - applyPreviewSize(frame, item.width, item.height); - - frame.addEventListener('mousedown', event => { - event.preventDefault(); - event.stopPropagation(); - - view.focus(); - view.dispatch({ - effects: [setImageBlockSelection.of({ - anchor, - itemIndex: index, - })], - }); - }); - - const image = document.createElement('img'); - image.className = 'cm-image-block-thumb'; - image.alt = item.alt ?? item.title ?? ''; - image.draggable = false; - image.decoding = 'async'; - image.loading = 'lazy'; - - const title = getImageTitle(item); - if (title) { - image.title = title; - } - - image.addEventListener('load', () => { - applyLoadedState(frame, image); - }); - - image.addEventListener('error', () => { - frame.classList.remove('is-loading'); - frame.classList.add('is-error'); - }); - - image.src = item.src; - if (image.complete && image.naturalWidth > 0) { - applyLoadedState(frame, image); - } - - frame.appendChild(image); - - const actions = document.createElement('div'); - actions.className = 'cm-image-block-actions'; - frame.appendChild(actions); - - const stopPropagation = (event: Event) => { - event.stopPropagation(); - }; - - const stopPointerEvent = (event: MouseEvent) => { - event.preventDefault(); - stopPropagation(event); - }; - - const dragHandle = document.createElement('div'); - dragHandle.className = 'cm-image-block-drag-handle'; - dragHandle.title = t('extensions.imageBlock.dragSort'); - dragHandle.setAttribute('aria-label', t('extensions.imageBlock.dragSort')); - dragHandle.innerHTML = ''; - dragHandle.addEventListener('mousedown', stopPropagation); - dragHandle.addEventListener('click', event => { - event.preventDefault(); - stopPropagation(event); - }); - actions.appendChild(dragHandle); - - const setBusy = (busy: boolean) => { - frame.classList.toggle('is-busy', busy); - Array.from(actions.querySelectorAll('button')).forEach(button => { - button.disabled = busy; - }); - }; - - const createActionButton = ( - className: string, - title: string, - iconMarkup: string, - action: () => Promise, - options: { - busy?: boolean; - } = {}, - ) => { - const button = document.createElement('button'); - button.type = 'button'; - button.className = `cm-image-block-action ${className}`; - button.title = title; - button.setAttribute('aria-label', title); - button.innerHTML = iconMarkup; - button.addEventListener('mousedown', stopPointerEvent); - button.addEventListener('click', async event => { - stopPointerEvent(event); - if (options.busy) { - setBusy(true); - } - try { - await action(); - } catch (error) { - console.error(`[imageblock] Failed to ${className}:`, error); - } finally { - if (options.busy) { - setBusy(false); - } - } - }); - actions.appendChild(button); - }; - - createActionButton( - 'is-copy', - t('extensions.imageBlock.copyImage'), - '', - () => copyImageItemToClipboard(item), - ); - - createActionButton( - 'is-delete', - t('extensions.imageBlock.deleteImage'), - '', - async () => { - const deleted = await deleteImageItem(view, anchor, index); - if (!deleted) { - throw new Error('Delete image request was not completed'); - } - }, - {busy: true}, - ); - - return frame; -} - -export class ImageBlockWidget extends WidgetType { - constructor( - private readonly anchor: number, - private readonly items: readonly ImageBlockItem[], - private readonly selectedIndex: number | null, - ) { - super(); - } - - override eq(other: ImageBlockWidget): boolean { - return this.anchor === other.anchor - && this.selectedIndex === other.selectedIndex - && areItemsEqual(this.items, other.items); - } - - override toDOM(view: EditorView): HTMLElement { - const root = document.createElement('div'); - root.className = 'cm-image-block-widget'; - root.dataset[CONTEXT_MENU_BLOCK_ANCHOR_DATASET] = String(this.anchor); - - const flow = document.createElement('div'); - flow.className = 'cm-image-block-flow'; - root.appendChild(flow); - - root.addEventListener('mousedown', event => { - const target = event.target as HTMLElement | null; - if (target?.closest('.cm-image-block-item')) { - return; - } - - event.preventDefault(); - event.stopPropagation(); - - view.focus(); - view.dispatch({ - effects: [setImageBlockSelection.of({ - anchor: this.anchor, - itemIndex: null, - })], - }); - }); - - this.items.forEach((item, index) => { - flow.appendChild(createImageFrame(view, this.anchor, item, index, this.selectedIndex)); - }); - - attachImageBlockDrag(flow, view, this.anchor); - - return root; - } - - override updateDOM(dom: HTMLElement): boolean { - if (dom.dataset[CONTEXT_MENU_BLOCK_ANCHOR_DATASET] !== String(this.anchor)) { - return false; - } - - const flow = dom.querySelector('.cm-image-block-flow'); - if (!flow) { - return false; - } - - const frames = Array.from(flow.children); - if (frames.length !== this.items.length) { - return false; - } - - const canReuse = this.items.every((item, index) => { - const frame = frames[index]; - return frame instanceof HTMLElement && frame.dataset.signature === getItemSignature(item); - }); - - if (!canReuse) { - return false; - } - - frames.forEach((frame, index) => { - if (frame instanceof HTMLElement) { - setSelectionState(frame, this.selectedIndex === index); - } - }); - - return true; - } - - override ignoreEvent(): boolean { - return true; - } -} From e93d4dc978ec14ac7dc9352ac025a8ee41c27924 Mon Sep 17 00:00:00 2001 From: landaiqing Date: Wed, 8 Apr 2026 00:35:15 +0800 Subject: [PATCH 22/26] :sparkles: Added inline images extension --- .../voidraft/internal/models/models.ts | 5 + .../internal/services/mediahttpservice.ts | 2 +- .../voidraft/internal/services/models.ts | 18 +- frontend/components.d.ts | 3 + frontend/package-lock.json | 858 +++++++++++++++- frontend/package.json | 1 + frontend/public/images/inlineImage.svg | 1 + frontend/src/assets/icons/copy-dark.svg | 5 + frontend/src/assets/icons/pencil-white.svg | 5 + .../assets/icons/resize-handle-se-dark.png | Bin 0 -> 1905 bytes .../assets/icons/resize-handle-se-light.png | Bin 0 -> 2057 bytes .../inlineImage/DrawImageDialog.vue | 283 ++++++ .../inlineImage/draw/DrawImageFooter.vue | 104 ++ .../inlineImage/draw/DrawImageToolbar.vue | 342 +++++++ .../src/components/inlineImage/draw/types.ts | 1 + .../inlineImage/draw/useInlineImageDraw.ts | 933 ++++++++++++++++++ frontend/src/i18n/locales/en-US.ts | 30 + frontend/src/i18n/locales/zh-CN.ts | 30 + frontend/src/views/editor/Editor.vue | 8 +- .../editor/extensions/codeblock/copyPaste.ts | 160 +-- .../extensions/inlineImage/clipboard.ts | 177 ++++ .../inlineImage/clipboardIntegration.ts | 180 ++++ .../editor/extensions/inlineImage/index.ts | 49 + .../extensions/inlineImage/inlineImage.ts | 295 ++++++ .../inlineImage/inlineImageParsing.test.ts | 72 ++ .../inlineImage/inlineImageParsing.ts | 174 ++++ .../inlineImage/inlineImageWidget.ts | 322 ++++++ .../editor/extensions/inlineImage/manager.ts | 65 ++ .../editor/extensions/inlineImage/types.ts | 18 + .../src/views/editor/manager/extensions.ts | 6 + .../src/views/settings/pages/TestPage.vue | 4 +- internal/common/helper/media_helper.go | 4 + internal/common/syncer/app.go | 8 +- .../syncer/resource/media_asset_adapter.go | 396 -------- .../resource/media_asset_adapter_test.go | 477 --------- internal/models/ent/client.go | 3 +- internal/models/ent/entql.go | 38 +- internal/models/ent/mediaasset.go | 44 +- internal/models/ent/mediaasset/mediaasset.go | 43 +- internal/models/ent/mediaasset/where.go | 260 ++--- internal/models/ent/mediaasset_create.go | 64 +- internal/models/ent/mediaasset_update.go | 160 +-- internal/models/ent/migrate/schema.go | 23 +- internal/models/ent/mutation.go | 239 ++--- internal/models/ent/runtime/runtime.go | 32 +- internal/models/extension.go | 6 + internal/models/schema/media_asset.go | 14 +- internal/services/media_http_service.go | 183 ++-- internal/services/media_http_service_test.go | 86 +- internal/services/sync_service.go | 10 - 50 files changed, 4488 insertions(+), 1753 deletions(-) create mode 100644 frontend/public/images/inlineImage.svg create mode 100644 frontend/src/assets/icons/copy-dark.svg create mode 100644 frontend/src/assets/icons/pencil-white.svg create mode 100644 frontend/src/assets/icons/resize-handle-se-dark.png create mode 100644 frontend/src/assets/icons/resize-handle-se-light.png create mode 100644 frontend/src/components/inlineImage/DrawImageDialog.vue create mode 100644 frontend/src/components/inlineImage/draw/DrawImageFooter.vue create mode 100644 frontend/src/components/inlineImage/draw/DrawImageToolbar.vue create mode 100644 frontend/src/components/inlineImage/draw/types.ts create mode 100644 frontend/src/components/inlineImage/draw/useInlineImageDraw.ts create mode 100644 frontend/src/views/editor/extensions/inlineImage/clipboard.ts create mode 100644 frontend/src/views/editor/extensions/inlineImage/clipboardIntegration.ts create mode 100644 frontend/src/views/editor/extensions/inlineImage/index.ts create mode 100644 frontend/src/views/editor/extensions/inlineImage/inlineImage.ts create mode 100644 frontend/src/views/editor/extensions/inlineImage/inlineImageParsing.test.ts create mode 100644 frontend/src/views/editor/extensions/inlineImage/inlineImageParsing.ts create mode 100644 frontend/src/views/editor/extensions/inlineImage/inlineImageWidget.ts create mode 100644 frontend/src/views/editor/extensions/inlineImage/manager.ts create mode 100644 frontend/src/views/editor/extensions/inlineImage/types.ts delete mode 100644 internal/common/syncer/resource/media_asset_adapter.go delete mode 100644 internal/common/syncer/resource/media_asset_adapter_test.go diff --git a/frontend/bindings/voidraft/internal/models/models.ts b/frontend/bindings/voidraft/internal/models/models.ts index ff75fc04..bc44fa51 100644 --- a/frontend/bindings/voidraft/internal/models/models.ts +++ b/frontend/bindings/voidraft/internal/models/models.ts @@ -403,6 +403,11 @@ export enum ExtensionName { */ HttpClient = "httpClient", + /** + * 编辑器内联图片 + */ + InlineImage = "inlineImage", + /** * 代码块导出图片 */ diff --git a/frontend/bindings/voidraft/internal/services/mediahttpservice.ts b/frontend/bindings/voidraft/internal/services/mediahttpservice.ts index d68629de..af3b795a 100644 --- a/frontend/bindings/voidraft/internal/services/mediahttpservice.ts +++ b/frontend/bindings/voidraft/internal/services/mediahttpservice.ts @@ -19,7 +19,7 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic import * as $models from "./models.js"; /** - * DeleteImage removes one logical image asset and deletes its local file. + * DeleteImage permanently removes one indexed image asset and its local file. */ export function DeleteImage(imageRef: string): Promise<$models.ImageDeleteResult | null> & { cancel(): void } { let $resultPromise = $Call.ByID(143616668, imageRef) as any; diff --git a/frontend/bindings/voidraft/internal/services/models.ts b/frontend/bindings/voidraft/internal/services/models.ts index 0812e8fc..89956ab2 100644 --- a/frontend/bindings/voidraft/internal/services/models.ts +++ b/frontend/bindings/voidraft/internal/services/models.ts @@ -163,9 +163,8 @@ export class HttpResponse { */ export class ImageAsset { "id": string; - "filename": string; - "original_filename"?: string; - "relative_path": string; + "filename"?: string; + "path": string; "url": string; "mime_type": string; "size": number; @@ -180,11 +179,8 @@ export class ImageAsset { if (!("id" in $$source)) { this["id"] = ""; } - if (!("filename" in $$source)) { - this["filename"] = ""; - } - if (!("relative_path" in $$source)) { - this["relative_path"] = ""; + if (!("path" in $$source)) { + this["path"] = ""; } if (!("url" in $$source)) { this["url"] = ""; @@ -227,13 +223,13 @@ export class ImageAsset { * ImageDeleteResult describes the outcome of a delete operation. */ export class ImageDeleteResult { - "relative_path": string; + "path": string; "deleted": boolean; /** Creates a new ImageDeleteResult instance. */ constructor($$source: Partial = {}) { - if (!("relative_path" in $$source)) { - this["relative_path"] = ""; + if (!("path" in $$source)) { + this["path"] = ""; } if (!("deleted" in $$source)) { this["deleted"] = false; diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 8b2a0b49..d6dc3e51 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -15,6 +15,9 @@ declare module 'vue' { AccordionItem: typeof import('./src/components/accordion/AccordionItem.vue')['default'] BlockLanguageSelector: typeof import('./src/components/toolbar/BlockLanguageSelector.vue')['default'] DocumentSelector: typeof import('./src/components/toolbar/DocumentSelector.vue')['default'] + DrawImageDialog: typeof import('./src/components/inlineImage/DrawImageDialog.vue')['default'] + DrawImageFooter: typeof import('./src/components/inlineImage/draw/DrawImageFooter.vue')['default'] + DrawImageToolbar: typeof import('./src/components/inlineImage/draw/DrawImageToolbar.vue')['default'] LinuxTitleBar: typeof import('./src/components/titlebar/LinuxTitleBar.vue')['default'] LoadingScreen: typeof import('./src/components/loading/LoadingScreen.vue')['default'] MacOSTitleBar: typeof import('./src/components/titlebar/MacOSTitleBar.vue')['default'] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ed84bb3b..77608544 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -50,6 +50,7 @@ "codemirror-lang-elixir": "^4.0.1", "colors-named": "^1.0.5", "colors-named-hex": "^1.0.4", + "fabric": "^7.2.0", "groovy-beautify": "^0.0.17", "hsl-matcher": "^1.2.4", "katex": "^0.16.44", @@ -100,6 +101,20 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, "node_modules/@babel/generator": { "version": "7.29.1", "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.1.tgz", @@ -623,6 +638,121 @@ "integrity": "sha512-5ZgNOdiiIHbcBLvJhonCGoHFfuLlfsA+CjohiZGVuyD2XMVi35YFr7vZ6eSHeWjFAUsKRFbcOqtoXsV1Wk7zXA==", "license": "MIT" }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@docsearch/css": { "version": "4.6.2", "resolved": "https://registry.npmmirror.com/@docsearch/css/-/css-4.6.2.tgz", @@ -3609,6 +3739,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.14.0.tgz", @@ -3738,7 +3878,7 @@ "version": "1.5.1", "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -3764,6 +3904,18 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/bn.js": { "version": "5.2.3", "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-5.2.3.tgz", @@ -3928,7 +4080,7 @@ "version": "5.7.1", "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -4013,6 +4165,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/canvas": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/canvas/-/canvas-3.2.3.tgz", + "integrity": "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/ccount/-/ccount-2.0.1.tgz", @@ -4097,6 +4264,13 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC", + "optional": true + }, "node_modules/cipher-base": { "version": "1.0.7", "resolved": "https://registry.npmmirror.com/cipher-base/-/cipher-base-1.0.7.tgz", @@ -4371,6 +4545,20 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", @@ -4879,6 +5067,20 @@ "lodash-es": "^4.17.21" } }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/dayjs": { "version": "1.11.20", "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz", @@ -4889,7 +5091,7 @@ "version": "4.4.3", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4903,6 +5105,39 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT", + "optional": true + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", @@ -5085,6 +5320,16 @@ "dev": true, "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "7.0.1", "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", @@ -5403,6 +5648,16 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz", @@ -5419,6 +5674,19 @@ "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", "license": "MIT" }, + "node_modules/fabric": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/fabric/-/fabric-7.2.0.tgz", + "integrity": "sha512-XSYmSqSMrlbCg+/j7/uU/PFeZuA5hHRDp7sGbDlMvz/T6BHt2MQSOYtz/AIdr+kmReA1s5jTzHJ8AjHwYUcmfQ==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "canvas": "^3.2.0", + "jsdom": "^26.1.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5534,6 +5802,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT", + "optional": true + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", @@ -5608,6 +5883,13 @@ "node": ">= 0.4" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmmirror.com/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT", + "optional": true + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", @@ -5807,6 +6089,19 @@ "url": "https://jaywcjlove.github.io/#/sponsor" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-void-elements": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-3.0.0.tgz", @@ -5818,6 +6113,20 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/https-browserify/-/https-browserify-1.0.0.tgz", @@ -5825,6 +6134,20 @@ "dev": true, "license": "MIT" }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -5841,7 +6164,7 @@ "version": "1.2.1", "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -5888,9 +6211,16 @@ "version": "2.0.4", "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC", + "optional": true + }, "node_modules/internmap": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/internmap/-/internmap-1.0.1.tgz", @@ -6003,6 +6333,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT", + "optional": true + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", @@ -6074,6 +6411,56 @@ "node": ">=10" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "license": "MIT", + "optional": true, + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=18" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", @@ -6312,6 +6699,13 @@ "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "license": "MIT" }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC", + "optional": true + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", @@ -6555,6 +6949,19 @@ "dev": true, "license": "MIT" }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -6585,11 +6992,21 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minisearch": { - "version": "7.2.0", - "resolved": "https://registry.npmmirror.com/minisearch/-/minisearch-7.2.0.tgz", - "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", - "dev": true, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "dev": true, "license": "MIT" }, "node_modules/mitt": { @@ -6598,6 +7015,13 @@ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "license": "MIT" }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmmirror.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT", + "optional": true + }, "node_modules/mlly": { "version": "1.8.2", "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.2.tgz", @@ -6614,7 +7038,7 @@ "version": "2.1.3", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/muggle-string": { @@ -6641,6 +7065,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT", + "optional": true + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", @@ -6648,6 +7079,19 @@ "dev": true, "license": "MIT" }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -6714,6 +7158,13 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmmirror.com/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "license": "MIT", + "optional": true + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", @@ -6786,6 +7237,16 @@ ], "license": "MIT" }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, "node_modules/oniguruma-parser": { "version": "0.12.1", "resolved": "https://registry.npmmirror.com/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", @@ -6892,6 +7353,32 @@ "node": ">= 0.10" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "optional": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", @@ -7124,6 +7611,34 @@ "node": ">=4" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7199,11 +7714,22 @@ "dev": true, "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -7271,11 +7797,27 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -7482,6 +8024,13 @@ "points-on-path": "^0.2.1" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "license": "MIT", + "optional": true + }, "node_modules/rw": { "version": "1.3.3", "resolved": "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz", @@ -7492,7 +8041,7 @@ "version": "5.2.1", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -7553,6 +8102,19 @@ "@parcel/watcher": "^2.4.1" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "optional": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scule": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz", @@ -7563,7 +8125,7 @@ "version": "7.7.4", "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -7741,6 +8303,53 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", @@ -7812,7 +8421,7 @@ "version": "1.1.1", "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -7822,7 +8431,7 @@ "version": "5.1.2", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/stringify-entities": { @@ -7840,6 +8449,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/style-mod": { "version": "4.1.3", "resolved": "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.3.tgz", @@ -7877,6 +8496,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT", + "optional": true + }, "node_modules/tabbable": { "version": "6.4.0", "resolved": "https://registry.npmmirror.com/tabbable/-/tabbable-6.4.0.tgz", @@ -7884,6 +8510,36 @@ "dev": true, "license": "MIT" }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/timers-browserify": { "version": "2.0.12", "resolved": "https://registry.npmmirror.com/timers-browserify/-/timers-browserify-2.0.12.tgz", @@ -7939,6 +8595,26 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmmirror.com/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmmirror.com/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT", + "optional": true + }, "node_modules/to-buffer": { "version": "1.2.2", "resolved": "https://registry.npmmirror.com/to-buffer/-/to-buffer-1.2.2.tgz", @@ -7954,6 +8630,32 @@ "node": ">= 0.4" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "optional": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/trim-lines/-/trim-lines-3.0.1.tgz", @@ -7994,6 +8696,19 @@ "dev": true, "license": "MIT" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", @@ -8288,7 +9003,7 @@ "version": "1.0.2", "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/uuid": { @@ -8862,12 +9577,83 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "optional": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/w3c-xmlserializer/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=12" + } + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "license": "MIT" }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", @@ -8933,6 +9719,35 @@ "node": ">=0.10.0" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "optional": true + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz", @@ -8943,6 +9758,13 @@ "node": ">=12" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT", + "optional": true + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 83c457ac..3215d1a6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -64,6 +64,7 @@ "codemirror-lang-elixir": "^4.0.1", "colors-named": "^1.0.5", "colors-named-hex": "^1.0.4", + "fabric": "^7.2.0", "groovy-beautify": "^0.0.17", "hsl-matcher": "^1.2.4", "katex": "^0.16.44", diff --git a/frontend/public/images/inlineImage.svg b/frontend/public/images/inlineImage.svg new file mode 100644 index 00000000..a071a659 --- /dev/null +++ b/frontend/public/images/inlineImage.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/icons/copy-dark.svg b/frontend/src/assets/icons/copy-dark.svg new file mode 100644 index 00000000..9d63294e --- /dev/null +++ b/frontend/src/assets/icons/copy-dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/assets/icons/pencil-white.svg b/frontend/src/assets/icons/pencil-white.svg new file mode 100644 index 00000000..0cc758ec --- /dev/null +++ b/frontend/src/assets/icons/pencil-white.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/assets/icons/resize-handle-se-dark.png b/frontend/src/assets/icons/resize-handle-se-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..ecff343afef701dfa3f30b31903788dd04f6f4d1 GIT binary patch literal 1905 zcmah~dsGxv93DtbAS}v+#5lwF4%nGp1lehq6?ah}cIBC?9LQn!E<3``j57nfD2m{d zJl64)XlnQ%(SvA8(L)WD5W-`mrJ{KxMT^orpcoH5Wp@??5v@OFX7BI&e&6qP@7LuV@7=(s%3eAhR8x>1Qza&s(7=7Owi5hbA~H5W)($_ z#BsabF0%*ASUv-nE0s!|P~Zwh5M%@ixr|6Tf*4`EB+;v(1A>XCIgw@=RMMo<*=$jb zVGu|A#DRkzP-ld`@?j%z3Blzu0)Lz?(w1k?C1ePlv$7&9SXr*WfI%Vp;USK&eNT6B zP~6kVdi{UXZMH#W2x4RoY(!sE2j%oAfRL2S0XzW+Y&LHKkvV`7$2&VA^@WV19U#l7 zqiui@V0YD+V%ihnyk~$~HjCvG;idv=Oz!lNI!8v^s0=WdHjCDNtpR&1U^1Qrq|CCi zrO_rcG^|B2pk*Au)R;09e=N+rr$YU6Af4IiSu;#7*eN&wMQgQjJZqt|ASNWto{2_B zYD4A9(9j@-tTzH28VP-AORkO*Axl9hLW2l-kUS)bP>{g{NhkvdIY|&sWhWb+1v4d5 z|HqehG^&CgdOaCM3nI(sI!(RXJP|DFUG*-rXy^V$QRkFN$|N;Ojd46{&NczFGdPs% zMFrL(+9@7HWWWKdu?UNWhV9Hn;nATp7{H@)nVgV?4R*3yfi&`GmWTHxhf9alW47`4Lc+b39ciM;`E`>Cl%#ev7$dnD)8-58` z5${v4xol0}(o(Z(-R?zIbZPDS%H8pAWtP1#{c@(}+_IzJv=tYp{F)P45~)~MWG-I`m{*yDGwj*m%W??zO|{kUuA@=RHHTcw6N@!kh6 zQBw|+enl&LrZ+PaQd&-BcHY;t{a{Ur`NQ2fe`WpXfV(;Qj*Be_;o$6Y8dQ(`!s!Ok3 zS^_$EyfRUTcqhibWcJaNR*t$3;>O)wXU}hXP-87Cy2EB$E#hc%BiDmzO!y4SzX-Q)^pxn3vMLeTc{P< z3I$I?qwjp)!_c(zfkAIA!W$<2w8ijkhu7!zJ3C&U?|rjm>2|^4aVWW`+~Z<%!LkOs zY?-UihK!o?Bd)~5e+)M+YMy`~qiUtcWn;i7Uue7#t&2#CDyllapK*;Cz5J};R(Q|z z%=x98%MVwlf{A;7UOjy;=I=QsaCMn?{JAlmY|aHkXI{hU?_>A2gWp{7i;V%-J~g(4 z?OU5qz5eQ*uC?7CPvBl$(=x+1ZQSoQ>s(VEmhh{pH_klwPFhpfih2>LKF-7~K>zXb z+S;5l&Gq3Yt^V(yTrxbSy>Z6W=CM;w26uSv*;?S%K3R3WosFGbn}6`#x|R>SQ*?Q= z_1kXwH`VwrY#}V=b773h0 zA>-oJC<<}WAUSCo;&!@NyPugfVZaU?faW<&CXrzy^?`>Pj)vw2%LNOE2?$h|j#B3- z^IOyfWQ@Ag%J2+lWt>AnjdC#rk1>ShezS>(aK1^|VEAu!hhtP49G_GS3paQPqhfmH zfXgm%0xT17jEgmaq+(dTD1YIEy5L621MImv3Ko@vx1mPmAH4?Vy#>@Vc81M@O$gMe z%x@!94o`Lv7O;vUdFv3@@OdhrEw2MYWOkR(3OO_dbCC~o1kSP@kv0M`H051)Pwv@R5FORuO@*eE|L7^lI2>nVT;1wx~}wW!$0Y+gmD!p6iONPfgFfp&p2xTD~*!-lpU6o&RC zpy85a;Wk`W>CHiqATI^`VN4$&$ihlj<@K1djEHv4Rcm2QPwUq02Z|c0{cSrM4rHvb z9h|cCs!elgWAll=>gv^h6esy2PfGejj^`YyLNi(M&bH8de>ESBez2=kxx1yeq_%Tl z?$Mvvl(o#mgcjqk`}S_ON#gq&G{ou8z6jDU`~jcq`*dJwH$8uK&pF$ZXPUnA*3~I@ zf^+jfZJ&`7GdVqY+p3g99bdP8oT4~3fBoV=f4S{#?K)Un|7d^5SNNtc>wnyraI|<* zlUx%wP*zo69bUcjR%h?Ib5A$?yfZFy>y=qoJ^@emOkbcw!n0D}BO^5X8^Ue@!t9g1-Ek^uhVjUUM@7PGcunIN$cACJk4PBVI9#IYtqg< zn)-eF-bd5&!|&E^*v)xDj^zv-3i-Wz%f=43WaGGqN=xhIiPtjV|Jr1yK956?ur}cd zs)`AVgokI7bqU#eU*p9SVW*7|n{T$Czfch=br;K`%i6BZ+L5}l=-p^vMb+iRwnTl_ z3w7PSI3y}-&FKZJ8#^wW=`Vxmpvbh+Zy*6?Rlpr z-B|v4cW-aBEq`XZvp(|MGUn)EF3I&keEDQTRP(w$vb75?1lRRztlR7KFAW#opJ(!} z=&mVmotFzfoaDot&qBwy56~Gi{+?b@t_{C9ZoSc4mlXH7=+w-6Q^MT4irOU4<@E7_ cs%>XNntUa9UKXU@fH@(_Nol$xi3Q$&0YbdnQ~&?~ literal 0 HcmV?d00001 diff --git a/frontend/src/components/inlineImage/DrawImageDialog.vue b/frontend/src/components/inlineImage/DrawImageDialog.vue new file mode 100644 index 00000000..a6d0f126 --- /dev/null +++ b/frontend/src/components/inlineImage/DrawImageDialog.vue @@ -0,0 +1,283 @@ + + + + + diff --git a/frontend/src/components/inlineImage/draw/DrawImageFooter.vue b/frontend/src/components/inlineImage/draw/DrawImageFooter.vue new file mode 100644 index 00000000..246fdf9a --- /dev/null +++ b/frontend/src/components/inlineImage/draw/DrawImageFooter.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/frontend/src/components/inlineImage/draw/DrawImageToolbar.vue b/frontend/src/components/inlineImage/draw/DrawImageToolbar.vue new file mode 100644 index 00000000..ce2b936d --- /dev/null +++ b/frontend/src/components/inlineImage/draw/DrawImageToolbar.vue @@ -0,0 +1,342 @@ + + + + + diff --git a/frontend/src/components/inlineImage/draw/types.ts b/frontend/src/components/inlineImage/draw/types.ts new file mode 100644 index 00000000..87a17861 --- /dev/null +++ b/frontend/src/components/inlineImage/draw/types.ts @@ -0,0 +1 @@ +export type ToolMode = 'select' | 'pan' | 'pen'; diff --git a/frontend/src/components/inlineImage/draw/useInlineImageDraw.ts b/frontend/src/components/inlineImage/draw/useInlineImageDraw.ts new file mode 100644 index 00000000..50577b41 --- /dev/null +++ b/frontend/src/components/inlineImage/draw/useInlineImageDraw.ts @@ -0,0 +1,933 @@ +import {Canvas, FabricImage, PencilBrush, type FabricObject} from 'fabric'; +import {computed, nextTick, onUnmounted, ref, shallowRef, watch} from 'vue'; +import {useI18n} from 'vue-i18n'; +import {useConfigStore} from '@/stores/configStore'; +import { + buildVersionedInlineImageUrl, + canvasToPngBlob, + deleteImageAsset, + importImageBlob, +} from '@/views/editor/extensions/inlineImage/clipboard'; +import {inlineImageDrawManager} from '@/views/editor/extensions/inlineImage/manager'; +import {updateInlineImageData} from '@/views/editor/extensions/inlineImage/inlineImageParsing'; +import type {ToolMode} from './types'; + +type CanvasSnapshot = string | Record; +type InlineCanvasObject = FabricObject & {name?: string}; + +const INLINE_IMAGE_BACKGROUND_NAME = 'inline-image-background'; +const MIN_DIALOG_WIDTH = 640; +const MIN_DIALOG_HEIGHT = 440; + +export function useInlineImageDraw() { + const state = inlineImageDrawManager.useState(); + const configStore = useConfigStore(); + const {t} = useI18n(); + + const dialogRef = ref(null); + const headerRef = ref(null); + const contentRef = ref(null); + const footerRef = ref(null); + const stageRef = ref(null); + const canvasRef = ref(null); + + const fabricCanvas = shallowRef(null); + const isLoading = ref(false); + const isSaving = ref(false); + const errorMessage = ref(''); + const toolMode = ref('pen'); + const brushColor = ref('#f42525'); + const brushWidth = ref(3); + const brushWidthOptions = [1, 3, 5, 7, 10, 15, 24]; + const zoom = ref(1); + const baseScale = ref(1); + const imageWidth = ref(1); + const imageHeight = ref(1); + const scaledWidth = ref(1); + const scaledHeight = ref(1); + const stagePaddingX = ref(0); + const stagePaddingY = ref(0); + const stageOverflow = ref<'hidden' | 'auto'>('hidden'); + const canvasOffsetX = ref(0); + const canvasOffsetY = ref(0); + const isStagePanning = ref(false); + const history = ref([]); + const historyIndex = ref(-1); + const isRestoring = ref(false); + const viewportObserver = shallowRef(null); + const dialogWidth = ref(980); + const dialogHeight = ref(720); + const chromeWidth = ref(0); + const chromeHeight = ref(0); + let viewportResizeFrame: number | null = null; + + const stagePanState = { + startX: 0, + startY: 0, + scrollLeft: 0, + scrollTop: 0, + }; + + const isVisible = computed(() => state.value.visible); + const currentTagId = computed(() => state.value.tagId); + const currentAssetRef = computed(() => state.value.assetRef); + const currentImageUrl = computed(() => state.value.imageUrl); + const currentView = computed(() => state.value.view); + const zoomLabel = computed(() => `${Math.round(baseScale.value * zoom.value * 100)}%`); + const canUndo = computed(() => historyIndex.value > 0); + const canRedo = computed(() => historyIndex.value >= 0 && historyIndex.value < history.value.length - 1); + const stageCursor = computed(() => { + if (isStagePanning.value) { + return 'grabbing'; + } + if (toolMode.value === 'pan') { + return 'grab'; + } + if (toolMode.value === 'select') { + return 'default'; + } + return 'crosshair'; + }); + + const dialogStyle = computed(() => ({ + width: `${dialogWidth.value}px`, + height: `${dialogHeight.value}px`, + '--inline-image-draw-font-family': configStore.config.editing.fontFamily || 'var(--voidraft-font-mono, system-ui, -apple-system, sans-serif)', + '--inline-image-draw-font-weight': configStore.config.editing.fontWeight || '400', + '--inline-image-draw-line-height': String(configStore.config.editing.lineHeight || 1.5), + })); + + watch(isVisible, async visible => { + if (visible) { + window.addEventListener('keydown', handleKeyDown); + await nextTick(); + startViewportObserver(); + await initCanvas(); + return; + } + + window.removeEventListener('keydown', handleKeyDown); + stopViewportObserver(); + disposeCanvas(); + errorMessage.value = ''; + }); + + watch(currentImageUrl, async imageUrl => { + if (isVisible.value && imageUrl) { + await initCanvas(); + } + }); + + watch([brushColor, brushWidth], () => { + configureBrush(); + }); + + onUnmounted(() => { + window.removeEventListener('keydown', handleKeyDown); + stopViewportObserver(); + stopStagePan(); + disposeCanvas(); + }); + + function asCanvasObject(object: unknown): InlineCanvasObject | null { + if (!object || typeof object !== 'object' || !('set' in object)) { + return null; + } + + return object as InlineCanvasObject; + } + + function isBackgroundObject(object: unknown): boolean { + return asCanvasObject(object)?.name === INLINE_IMAGE_BACKGROUND_NAME; + } + + function lockCanvasObject(object: InlineCanvasObject): void { + object.selectable = false; + object.evented = false; + object.hasControls = false; + object.lockMovementX = true; + object.lockMovementY = true; + object.lockScalingFlip = true; + } + + function normalizeBrushWidth(value: number): number { + return brushWidthOptions.reduce((closest, option) => ( + Math.abs(option - value) < Math.abs(closest - value) ? option : closest + ), brushWidthOptions[0]); + } + + function createBrush(): PencilBrush | null { + if (!fabricCanvas.value) { + return null; + } + + const brush = new PencilBrush(fabricCanvas.value); + brush.color = brushColor.value; + brush.width = brushWidth.value; + brush.decimate = 0; + brush.strokeLineCap = 'round'; + brush.strokeLineJoin = 'round'; + return brush; + } + + function configureBrush(): void { + if (!fabricCanvas.value) { + return; + } + + if (!fabricCanvas.value.freeDrawingBrush) { + fabricCanvas.value.freeDrawingBrush = createBrush() ?? undefined; + } + + const brush = fabricCanvas.value.freeDrawingBrush; + if (brush) { + brush.color = brushColor.value; + brush.width = brushWidth.value; + brush.strokeLineCap = 'round'; + brush.strokeLineJoin = 'round'; + + if (brush instanceof PencilBrush) { + brush.decimate = 0; + } + } + } + + function currentAccentColor(): string { + return getComputedStyle(document.documentElement) + .getPropertyValue('--search-focus-border') + .trim() || '#4a9eff'; + } + + function applyControlStyles(target?: unknown): void { + const canvas = fabricCanvas.value; + if (!canvas) { + return; + } + + const accent = currentAccentColor(); + const applyTo = (object: unknown) => { + const canvasObject = asCanvasObject(object); + if (!canvasObject || isBackgroundObject(canvasObject)) { + return; + } + + canvasObject.cornerColor = accent; + canvasObject.borderColor = accent; + canvasObject.cornerStrokeColor = accent; + canvasObject.cornerDashArray = [4, 4]; + canvasObject.borderDashArray = [4, 4]; + canvasObject.borderScaleFactor = 2; + canvasObject.transparentCorners = false; + canvasObject.cornerStyle = 'rect'; + }; + + if (target) { + applyTo(target); + return; + } + + canvas.getObjects().forEach(applyTo); + } + + function setTool(nextTool: ToolMode): void { + toolMode.value = nextTool; + + const canvas = fabricCanvas.value; + if (!canvas) { + return; + } + + canvas.discardActiveObject(); + canvas.isDrawingMode = nextTool === 'pen'; + canvas.selection = nextTool === 'select'; + canvas.defaultCursor = nextTool === 'pan' + ? 'grab' + : nextTool === 'select' + ? 'default' + : 'crosshair'; + + canvas.getObjects().forEach(object => { + const canvasObject = asCanvasObject(object); + if (!canvasObject) { + return; + } + + if (isBackgroundObject(canvasObject)) { + lockCanvasObject(canvasObject); + canvasObject.hasBorders = false; + return; + } + + const selectable = nextTool === 'select'; + canvasObject.selectable = selectable; + canvasObject.evented = selectable; + applyControlStyles(canvasObject); + }); + + configureBrush(); + canvas.requestRenderAll(); + } + + function resetHistory(): void { + history.value = []; + historyIndex.value = -1; + } + + function captureHistory(force = false): void { + const canvas = fabricCanvas.value; + if (!canvas) { + return; + } + if (!force && isRestoring.value) { + return; + } + + const snapshot: CanvasSnapshot = canvas.toJSON(); + const nextHistory = historyIndex.value < history.value.length - 1 + ? history.value.slice(0, historyIndex.value + 1) + : history.value.slice(); + + nextHistory.push(snapshot); + history.value = nextHistory; + historyIndex.value = nextHistory.length - 1; + } + + async function restoreHistory(index: number): Promise { + const canvas = fabricCanvas.value; + if (!canvas || index < 0 || index >= history.value.length) { + return; + } + + isRestoring.value = true; + const snapshot = history.value[index]; + + await canvas.loadFromJSON(snapshot, (_serialized, object) => { + const canvasObject = asCanvasObject(object); + if (canvasObject && (isBackgroundObject(canvasObject) || canvasObject.type === 'image')) { + lockCanvasObject(canvasObject); + canvasObject.hasBorders = false; + } + applyControlStyles(canvasObject); + }); + + setTool(toolMode.value); + configureBrush(); + canvas.requestRenderAll(); + historyIndex.value = index; + isRestoring.value = false; + } + + function undo(): void { + if (canUndo.value) { + void restoreHistory(historyIndex.value - 1); + } + } + + function redo(): void { + if (canRedo.value) { + void restoreHistory(historyIndex.value + 1); + } + } + + function deleteSelection(): boolean { + const canvas = fabricCanvas.value; + if (!canvas || isLoading.value) { + return false; + } + + const activeObjects = (canvas.getActiveObjects?.() || []).filter(object => !isBackgroundObject(object)); + if (activeObjects.length === 0) { + return false; + } + + activeObjects.forEach(object => { + canvas.remove(object); + }); + + canvas.discardActiveObject(); + canvas.requestRenderAll(); + captureHistory(); + return true; + } + + function onPathCreated(): void { + if (!isRestoring.value) { + captureHistory(); + } + } + + function onObjectModified(): void { + if (!isRestoring.value) { + captureHistory(); + } + } + + function onObjectAdded(event: {target?: unknown}): void { + if (!isRestoring.value && event?.target) { + applyControlStyles(event.target); + } + } + + function measureChrome(): void { + const headerHeight = headerRef.value?.offsetHeight || 0; + const footerHeight = footerRef.value?.offsetHeight || 0; + const contentHorizontalPadding = contentRef.value + ? contentRef.value.offsetWidth - contentRef.value.clientWidth + : 0; + const contentVerticalPadding = contentRef.value + ? contentRef.value.offsetHeight - contentRef.value.clientHeight + : 0; + + chromeWidth.value = contentHorizontalPadding; + chromeHeight.value = headerHeight + footerHeight + contentVerticalPadding; + } + + function getOverlayViewport(): {width: number; height: number} { + const overlay = dialogRef.value?.parentElement; + if (!overlay) { + return { + width: Math.max(320, window.innerWidth - 56), + height: Math.max(260, window.innerHeight - 56), + }; + } + + const style = getComputedStyle(overlay); + const horizontalPadding = (Number.parseFloat(style.paddingLeft) || 0) + (Number.parseFloat(style.paddingRight) || 0); + const verticalPadding = (Number.parseFloat(style.paddingTop) || 0) + (Number.parseFloat(style.paddingBottom) || 0); + + return { + width: Math.max(320, overlay.clientWidth - horizontalPadding), + height: Math.max(260, overlay.clientHeight - verticalPadding), + }; + } + + function updateDialogSize(): void { + const dpr = window.devicePixelRatio || 1; + const logicalWidth = imageWidth.value / dpr; + const logicalHeight = imageHeight.value / dpr; + const viewport = getOverlayViewport(); + + measureChrome(); + + const maxWidth = Math.max(Math.min(MIN_DIALOG_WIDTH, viewport.width), viewport.width); + const maxHeight = Math.max(Math.min(MIN_DIALOG_HEIGHT, viewport.height), viewport.height); + const proportionalWidth = Math.round(viewport.width * 0.84); + const proportionalHeight = Math.round(viewport.height * 0.84); + const desiredWidth = Math.max( + Math.min(MIN_DIALOG_WIDTH, viewport.width), + proportionalWidth, + Math.round(logicalWidth + chromeWidth.value + 32), + ); + const desiredHeight = Math.max( + Math.min(MIN_DIALOG_HEIGHT, viewport.height), + proportionalHeight, + Math.round(logicalHeight + chromeHeight.value + 32), + ); + + dialogWidth.value = Math.min(desiredWidth, maxWidth); + dialogHeight.value = Math.min(desiredHeight, maxHeight); + } + + function queueViewportResize(): void { + if (!isVisible.value) { + return; + } + + if (viewportResizeFrame !== null) { + cancelAnimationFrame(viewportResizeFrame); + } + + viewportResizeFrame = requestAnimationFrame(() => { + viewportResizeFrame = null; + void nextTick(() => { + updateDialogSize(); + updateCanvasScale(); + }); + }); + } + + function startViewportObserver(): void { + stopViewportObserver(); + + const overlay = dialogRef.value?.parentElement; + if (!overlay || typeof ResizeObserver === 'undefined') { + return; + } + + const observer = new ResizeObserver(() => { + queueViewportResize(); + }); + observer.observe(overlay); + viewportObserver.value = observer; + } + + function stopViewportObserver(): void { + viewportObserver.value?.disconnect(); + viewportObserver.value = null; + if (viewportResizeFrame !== null) { + cancelAnimationFrame(viewportResizeFrame); + viewportResizeFrame = null; + } + } + + function syncCanvasViewport(): void { + const canvas = fabricCanvas.value; + const wrapper = canvas?.wrapperEl; + if (!canvas || !wrapper) { + return; + } + + wrapper.style.transform = canvasOffsetX.value || canvasOffsetY.value + ? `translate(${canvasOffsetX.value}px, ${canvasOffsetY.value}px)` + : 'translate(0px, 0px)'; + wrapper.style.transformOrigin = 'top left'; + wrapper.style.willChange = isStagePanning.value ? 'transform' : ''; + canvas.calcOffset(); + } + + function setCanvasOffset(offsetX: number, offsetY: number, sync = true): void { + canvasOffsetX.value = offsetX; + canvasOffsetY.value = offsetY; + if (sync) { + syncCanvasViewport(); + } + } + + function clampCanvasOffset(offsetX: number, offsetY: number): {x: number; y: number} { + const stage = stageRef.value; + if (!stage || stageOverflow.value === 'auto') { + return {x: 0, y: 0}; + } + + const stageWidth = stage.clientWidth || scaledWidth.value; + const stageHeight = stage.clientHeight || scaledHeight.value; + const minVisibleX = Math.min(140, Math.max(48, scaledWidth.value * 0.24)); + const minVisibleY = Math.min(120, Math.max(48, scaledHeight.value * 0.24)); + const centeredLeft = stagePaddingX.value; + const centeredTop = stagePaddingY.value; + + const minOffsetX = minVisibleX - scaledWidth.value - centeredLeft; + const maxOffsetX = stageWidth - minVisibleX - centeredLeft; + const minOffsetY = minVisibleY - scaledHeight.value - centeredTop; + const maxOffsetY = stageHeight - minVisibleY - centeredTop; + + return { + x: Math.min(maxOffsetX, Math.max(minOffsetX, offsetX)), + y: Math.min(maxOffsetY, Math.max(minOffsetY, offsetY)), + }; + } + + function updateCanvasScale(anchor?: {x: number; y: number}): void { + const canvas = fabricCanvas.value; + if (!canvas) { + return; + } + + const stage = stageRef.value; + const stageWidth = stage?.clientWidth || imageWidth.value; + const stageHeight = stage?.clientHeight || imageHeight.value; + const previousScrollLeft = stage?.scrollLeft || 0; + const previousScrollTop = stage?.scrollTop || 0; + const previousScaledWidth = scaledWidth.value || stageWidth; + const previousScaledHeight = scaledHeight.value || stageHeight; + const previousPaddingX = stagePaddingX.value; + const previousPaddingY = stagePaddingY.value; + const previousAnchor = anchor || {x: stageWidth / 2, y: stageHeight / 2}; + const previousViewportAnchor = { + x: previousScrollLeft + previousAnchor.x - previousPaddingX, + y: previousScrollTop + previousAnchor.y - previousPaddingY, + }; + + const dpr = window.devicePixelRatio || 1; + const logicalWidth = imageWidth.value / dpr; + const logicalHeight = imageHeight.value / dpr; + + baseScale.value = Math.min(stageWidth / logicalWidth, stageHeight / logicalHeight, 1); + const scale = baseScale.value * zoom.value; + const nextScaledWidth = Math.round(logicalWidth * scale); + const nextScaledHeight = Math.round(logicalHeight * scale); + + canvas.setDimensions({width: `${nextScaledWidth}px`, height: `${nextScaledHeight}px`}, {cssOnly: true}); + canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); + + stageOverflow.value = nextScaledWidth > stageWidth || nextScaledHeight > stageHeight ? 'auto' : 'hidden'; + scaledWidth.value = nextScaledWidth; + scaledHeight.value = nextScaledHeight; + stagePaddingX.value = nextScaledWidth < stageWidth ? Math.floor((stageWidth - nextScaledWidth) / 2) : 0; + stagePaddingY.value = nextScaledHeight < stageHeight ? Math.floor((stageHeight - nextScaledHeight) / 2) : 0; + + if (stageOverflow.value === 'auto') { + setCanvasOffset(0, 0, false); + } else { + const clamped = clampCanvasOffset(canvasOffsetX.value, canvasOffsetY.value); + setCanvasOffset(clamped.x, clamped.y, false); + } + + if (stage) { + const anchorRatioX = previousViewportAnchor.x / Math.max(previousScaledWidth, 1); + const anchorRatioY = previousViewportAnchor.y / Math.max(previousScaledHeight, 1); + const nextAnchor = { + x: anchorRatioX * nextScaledWidth, + y: anchorRatioY * nextScaledHeight, + }; + + void nextTick(() => { + const maxScrollLeft = Math.max(0, nextScaledWidth + stagePaddingX.value * 2 - stageWidth); + const maxScrollTop = Math.max(0, nextScaledHeight + stagePaddingY.value * 2 - stageHeight); + stage.scrollLeft = Math.min(maxScrollLeft, Math.max(0, nextAnchor.x + stagePaddingX.value - previousAnchor.x)); + stage.scrollTop = Math.min(maxScrollTop, Math.max(0, nextAnchor.y + stagePaddingY.value - previousAnchor.y)); + fabricCanvas.value?.calcOffset(); + }); + } + + syncCanvasViewport(); + } + + function setZoom(nextZoom: number, anchor?: {x: number; y: number}): void { + zoom.value = Math.min(Math.max(nextZoom, 0.2), 4); + updateCanvasScale(anchor); + } + + function zoomIn(): void { + if (!isLoading.value) { + setZoom(zoom.value + 0.2); + } + } + + function zoomOut(): void { + if (!isLoading.value) { + setZoom(zoom.value - 0.2); + } + } + + function resetZoom(): void { + zoom.value = 1; + updateCanvasScale(); + } + + function handleWheel(event: WheelEvent): void { + if (isLoading.value) { + return; + } + + event.preventDefault(); + + const stage = stageRef.value; + if (!stage) { + return; + } + + const rect = stage.getBoundingClientRect(); + setZoom( + zoom.value * (event.deltaY < 0 ? 1.05 : 0.95), + { + x: event.clientX - rect.left, + y: event.clientY - rect.top, + }, + ); + } + + function handleStagePan(event: MouseEvent): void { + if (!isStagePanning.value || !stageRef.value) { + return; + } + + if (stageOverflow.value === 'auto') { + stageRef.value.scrollLeft = stagePanState.scrollLeft - (event.clientX - stagePanState.startX); + stageRef.value.scrollTop = stagePanState.scrollTop - (event.clientY - stagePanState.startY); + fabricCanvas.value?.calcOffset(); + return; + } + + const clamped = clampCanvasOffset( + stagePanState.scrollLeft + (event.clientX - stagePanState.startX), + stagePanState.scrollTop + (event.clientY - stagePanState.startY), + ); + setCanvasOffset(clamped.x, clamped.y); + } + + function shouldStartPan(event: MouseEvent): boolean { + if (!stageRef.value) { + return false; + } + + if (event.button === 1) { + return true; + } + + return event.button === 0 && toolMode.value === 'pan'; + } + + function stopStagePan(): void { + isStagePanning.value = false; + window.removeEventListener('mousemove', handleStagePan); + window.removeEventListener('mouseup', stopStagePan); + syncCanvasViewport(); + } + + function onStageMouseDown(event: MouseEvent): void { + if (isStagePanning.value || !stageRef.value || !shouldStartPan(event)) { + return; + } + + event.preventDefault(); + isStagePanning.value = true; + stagePanState.startX = event.clientX; + stagePanState.startY = event.clientY; + stagePanState.scrollLeft = stageOverflow.value === 'auto' ? stageRef.value.scrollLeft : canvasOffsetX.value; + stagePanState.scrollTop = stageOverflow.value === 'auto' ? stageRef.value.scrollTop : canvasOffsetY.value; + + window.addEventListener('mousemove', handleStagePan); + window.addEventListener('mouseup', stopStagePan); + } + + function onStageScroll(): void { + fabricCanvas.value?.calcOffset(); + } + + function bindCanvasPan(canvas: Canvas): void { + canvas.upperCanvasEl?.addEventListener('mousedown', onStageMouseDown); + canvas.lowerCanvasEl?.addEventListener('mousedown', onStageMouseDown); + } + + function unbindCanvasPan(canvas: Canvas): void { + canvas.upperCanvasEl?.removeEventListener('mousedown', onStageMouseDown); + canvas.lowerCanvasEl?.removeEventListener('mousedown', onStageMouseDown); + } + + async function loadImage(): Promise { + const canvas = fabricCanvas.value; + if (!canvas || !currentImageUrl.value) { + return; + } + + isLoading.value = true; + errorMessage.value = ''; + + try { + const image = await FabricImage.fromURL(currentImageUrl.value); + image.set({ + left: 0, + top: 0, + originX: 'left', + originY: 'top', + selectable: false, + evented: false, + hasControls: false, + hasBorders: false, + lockMovementX: true, + lockMovementY: true, + name: INLINE_IMAGE_BACKGROUND_NAME, + hoverCursor: 'default', + }); + + canvas.clear(); + imageWidth.value = image.width || 1; + imageHeight.value = image.height || 1; + canvas.setDimensions({width: imageWidth.value, height: imageHeight.value}); + zoom.value = 1; + setCanvasOffset(0, 0); + updateDialogSize(); + await nextTick(); + updateCanvasScale(); + canvas.add(image); + canvas.sendObjectToBack(image); + canvas.requestRenderAll(); + resetHistory(); + captureHistory(true); + + const suggestedWidth = Math.round(Math.max(3, Math.min(15, imageWidth.value / 300))); + brushWidth.value = normalizeBrushWidth(suggestedWidth); + setTool(toolMode.value); + } catch (error) { + console.error('[inlineImage] Failed to load draw image:', error); + errorMessage.value = t('inlineImage.drawDialog.loadFailed'); + } finally { + isLoading.value = false; + } + } + + async function initCanvas(): Promise { + disposeCanvas(); + errorMessage.value = ''; + + if (!canvasRef.value) { + return; + } + + const canvas = new Canvas(canvasRef.value, { + selection: false, + preserveObjectStacking: true, + }); + + fabricCanvas.value = canvas; + bindCanvasPan(canvas); + canvas.on('path:created', onPathCreated); + canvas.on('object:modified', onObjectModified); + canvas.on('object:added', onObjectAdded); + canvas.freeDrawingBrush = createBrush() ?? undefined; + if (canvas.upperCanvasEl) { + canvas.upperCanvasEl.style.touchAction = 'none'; + } + if (canvas.lowerCanvasEl) { + canvas.lowerCanvasEl.style.touchAction = 'none'; + } + + updateDialogSize(); + await loadImage(); + } + + function disposeCanvas(): void { + if (!fabricCanvas.value) { + return; + } + + unbindCanvasPan(fabricCanvas.value); + fabricCanvas.value.off('path:created', onPathCreated); + fabricCanvas.value.off('object:modified', onObjectModified); + fabricCanvas.value.off('object:added', onObjectAdded); + fabricCanvas.value.dispose(); + fabricCanvas.value = null; + } + + async function saveImage(): Promise { + if (!currentView.value || !currentTagId.value || !currentAssetRef.value || !canvasRef.value || isLoading.value || isSaving.value) { + return; + } + + isSaving.value = true; + errorMessage.value = ''; + + try { + const previousAssetRef = currentAssetRef.value; + const previousImageUrl = currentImageUrl.value; + const previousWidth = imageWidth.value; + const previousHeight = imageHeight.value; + const blob = await canvasToPngBlob(canvasRef.value); + const updatedAsset = await importImageBlob(blob, 'inline-image-drawn.png'); + const updatedImageUrl = buildVersionedInlineImageUrl(updatedAsset); + + try { + updateInlineImageData(currentView.value, currentTagId.value, { + assetRef: updatedAsset.id, + file: updatedImageUrl, + width: updatedAsset.width, + height: updatedAsset.height, + }); + await deleteImageAsset(previousAssetRef); + } catch (error) { + updateInlineImageData(currentView.value, currentTagId.value, { + assetRef: previousAssetRef, + file: previousImageUrl, + width: previousWidth, + height: previousHeight, + }); + try { + await deleteImageAsset(updatedAsset.id); + } catch (cleanupError) { + console.error('[inlineImage] Failed to cleanup imported draw image:', cleanupError); + } + throw error; + } + + inlineImageDrawManager.hide(); + } catch (error) { + console.error('[inlineImage] Failed to save draw image:', error); + errorMessage.value = t('inlineImage.drawDialog.saveFailed'); + } finally { + isSaving.value = false; + } + } + + function closeDialog(): void { + inlineImageDrawManager.hide(); + } + + function handleKeyDown(event: KeyboardEvent): void { + if (!isVisible.value) { + return; + } + + if (event.key === 'Escape') { + event.preventDefault(); + closeDialog(); + return; + } + + const target = event.target as HTMLElement | null; + const tagName = target?.tagName?.toLowerCase(); + if (tagName === 'input' || tagName === 'textarea' || tagName === 'select' || target?.isContentEditable) { + return; + } + + const isMod = event.metaKey || event.ctrlKey; + if (isMod && event.key === 'Enter') { + event.preventDefault(); + void saveImage(); + return; + } + + if (event.key === 'Backspace' || event.key === 'Delete') { + if (deleteSelection()) { + event.preventDefault(); + } + return; + } + + if (!isMod) { + return; + } + + const lowerKey = event.key.toLowerCase(); + if (lowerKey === 'z' && !event.shiftKey) { + event.preventDefault(); + undo(); + return; + } + if ((lowerKey === 'z' && event.shiftKey) || lowerKey === 'y') { + event.preventDefault(); + redo(); + } + } + + return { + dialogRef, + headerRef, + contentRef, + footerRef, + stageRef, + canvasRef, + dialogWidth, + dialogHeight, + dialogStyle, + isVisible, + isLoading, + isSaving, + errorMessage, + toolMode, + brushColor, + brushWidth, + brushWidthOptions, + zoomLabel, + imageWidth, + imageHeight, + canUndo, + canRedo, + stagePaddingX, + stagePaddingY, + stageOverflow, + stageCursor, + setTool, + undo, + redo, + zoomOut, + zoomIn, + resetZoom, + handleWheel, + onStageMouseDown, + onStageScroll, + saveImage, + closeDialog, + isStagePanning, + }; +} diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index f2c43646..60e8e6d1 100644 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -370,6 +370,10 @@ export default { name: 'HTTP Client', description: 'Send HTTP requests directly in the editor and view responses' }, + inlineImage: { + name: 'Inline Images', + description: 'Paste clipboard images into the editor and render them as inline image widgets' + }, blockImage: { name: 'Block Image Export', description: 'Render the current code block to an image and copy it to the clipboard', @@ -385,5 +389,31 @@ export default { monitor: { memory: 'Memory', clickToClean: 'Click to clean memory' + }, + inlineImage: { + copy: 'Copy', + copied: 'Copied!', + draw: 'Draw', + delete: 'Delete', + drawDialog: { + title: 'Image Annotation', + select: 'Select', + pan: 'Pan', + pen: 'Brush', + color: 'Color', + strokeWidth: 'Stroke Width', + undo: 'Undo', + redo: 'Redo', + zoomIn: 'Zoom In', + zoomOut: 'Zoom Out', + resetZoom: 'Reset Zoom', + loading: 'Loading image...', + zoom: 'Zoom', + cancel: 'Cancel', + save: 'Save', + saving: 'Saving...', + loadFailed: 'Failed to load image', + saveFailed: 'Failed to save image' + } } }; diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 16fa22b1..eacf3a79 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -372,6 +372,10 @@ export default { name: 'HTTP 客户端', description: '在编辑器中直接发送 HTTP 请求并查看响应' }, + inlineImage: { + name: '内联图片', + description: '支持将剪贴板图片粘贴进编辑器,并以内联图片组件渲染' + }, blockImage: { name: '代码块导出图片', description: '将当前代码块渲染为图片并复制到剪贴板', @@ -387,5 +391,31 @@ export default { monitor: { memory: '内存', clickToClean: '点击清理内存' + }, + inlineImage: { + copy: '复制', + copied: '已复制', + draw: '绘制', + delete: '删除', + drawDialog: { + title: '图片标注', + select: '选择', + pan: '拖动', + pen: '画笔', + color: '颜色', + strokeWidth: '线宽', + undo: '撤销', + redo: '重做', + zoomIn: '放大', + zoomOut: '缩小', + resetZoom: '重置缩放', + loading: '正在加载图片...', + zoom: '缩放', + cancel: '取消', + save: '保存', + saving: '保存中...', + loadFailed: '图片加载失败', + saveFailed: '图片保存失败' + } } }; diff --git a/frontend/src/views/editor/Editor.vue b/frontend/src/views/editor/Editor.vue index 201a5eec..ab607d59 100644 --- a/frontend/src/views/editor/Editor.vue +++ b/frontend/src/views/editor/Editor.vue @@ -11,6 +11,8 @@ import ContextMenu from '@/views/editor/extensions/contextMenu/ContextMenu.vue'; import {contextMenuManager} from '@/views/editor/extensions/contextMenu/manager'; import TranslatorDialog from './extensions/translator/TranslatorDialog.vue'; import {translatorManager} from './extensions/translator/manager'; +import DrawImageDialog from '@/components/inlineImage/DrawImageDialog.vue'; +import {inlineImageDrawManager} from './extensions/inlineImage/manager'; const editorStore = useEditorStore(); @@ -19,6 +21,7 @@ const configStore = useConfigStore(); const windowStore = useWindowStore(); const tabStore = useTabStore(); +const editorContainerElement = ref(null); const editorElement = ref(null); const enableLoadingAnimation = computed(() => configStore.config.general.enableLoadingAnimation); @@ -43,11 +46,12 @@ onMounted(async () => { onBeforeUnmount(() => { contextMenuManager.destroy(); translatorManager.destroy(); + inlineImageDrawManager.destroy(); }); diff --git a/frontend/src/views/editor/extensions/codeblock/copyPaste.ts b/frontend/src/views/editor/extensions/codeblock/copyPaste.ts index 3891a447..864f8b88 100644 --- a/frontend/src/views/editor/extensions/codeblock/copyPaste.ts +++ b/frontend/src/views/editor/extensions/codeblock/copyPaste.ts @@ -1,12 +1,19 @@ /** * 代码块复制粘贴扩展 - * 防止复制分隔符标记,自动替换为换行符 + * 防止复制分隔符标记,并接入 inlineImage 的图片复制/粘贴能力。 */ import {EditorSelection, EditorState} from "@codemirror/state"; import {Command, EditorView} from "@codemirror/view"; import {LANGUAGES} from "./lang-parser/languages"; import {codeBlockEvent, CONTENT_EDIT, USER_EVENTS} from "./annotation"; +import {inlineImageEnabledFacet, inlineImageOptionsFacet} from "../inlineImage"; +import { + copySelectedInlineImageIfNeeded, + pasteInlineImagesFromClipboardEvent, + pasteInlineImagesFromSystemClipboard +} from "../inlineImage/clipboardIntegration"; +import {WIDGET_TAG_REGEX} from "../inlineImage/inlineImageParsing"; import * as runtime from "@wailsio/runtime"; /** @@ -59,77 +66,48 @@ function copiedRange(state: EditorState, forCut = false) { }; } -/** - * 设置浏览器复制和剪切事件处理器,将块分隔符替换为换行符 - */ -export const codeBlockCopyCut = EditorView.domEventHandlers({ - copy(event, view) { - event.preventDefault(); - - let {text} = copiedRange(view.state); - text = text.replaceAll(blockSeparatorRegex, "\n\n"); +function normalizeCopiedText(text: string): string { + return text + .replaceAll(blockSeparatorRegex, "\n\n") + .replaceAll(WIDGET_TAG_REGEX, ""); +} - runtime.Clipboard.SetText(text).catch(() => { - const data = event.clipboardData; - if (data) { - data.clearData(); - data.setData("text/plain", text); - } - }); - }, +async function writeTextToClipboard(text: string, event?: ClipboardEvent): Promise { + try { + await runtime.Clipboard.SetText(text); + return; + } catch (error) { + console.error('[Clipboard] Failed to write to system clipboard:', error); + } - cut(event, view) { - event.preventDefault(); + const data = event?.clipboardData; + if (data) { + data.clearData(); + data.setData("text/plain", text); + return; + } - let {text} = copiedRange(view.state, true); - const {ranges} = copiedRange(view.state, true); - text = text.replaceAll(blockSeparatorRegex, "\n\n"); + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + } +} - runtime.Clipboard.SetText(text).catch(() => { - const data = event.clipboardData; - if (data) { - data.clearData(); - data.setData("text/plain", text); +async function handleCopyCut(view: EditorView, cut: boolean, event?: ClipboardEvent): Promise { + if (!cut && view.state.facet(inlineImageEnabledFacet)) { + try { + if (await copySelectedInlineImageIfNeeded(view)) { + return true; } - }); - - if (!view.state.readOnly) { - view.dispatch({ - changes: ranges, - scrollIntoView: true, - userEvent: USER_EVENTS.DELETE_CUT, - annotations: [codeBlockEvent.of(CONTENT_EDIT)], - }); - } - }, - - paste(event, view) { - if (view.state.readOnly) { - return false; + } catch (error) { + console.error('[Clipboard] Failed to copy selected image:', error); } - - event.preventDefault(); - void pasteText(view, event as ClipboardEvent); - - return true; } -}); -/** - * 复制和剪切的通用函数 - */ -const copyCut = (view: EditorView, cut: boolean): boolean => { let {text} = copiedRange(view.state, cut); const {ranges} = copiedRange(view.state, cut); - text = text.replaceAll(blockSeparatorRegex, "\n\n"); + text = normalizeCopiedText(text); - runtime.Clipboard.SetText(text).catch(err => { - console.error('[Clipboard] Failed to write to system clipboard:', err); - - if (navigator.clipboard?.writeText) { - navigator.clipboard.writeText(text); - } - }); + await writeTextToClipboard(text, event); if (cut && !view.state.readOnly) { view.dispatch({ @@ -141,7 +119,33 @@ const copyCut = (view: EditorView, cut: boolean): boolean => { } return true; -}; +} + +/** + * 设置浏览器复制和剪切事件处理器,将块分隔符替换为换行符 + */ +export const codeBlockCopyCut = EditorView.domEventHandlers({ + copy(event, view) { + event.preventDefault(); + void handleCopyCut(view, false, event as ClipboardEvent); + }, + + cut(event, view) { + event.preventDefault(); + void handleCopyCut(view, true, event as ClipboardEvent); + }, + + paste(event, view) { + if (view.state.readOnly) { + return false; + } + + event.preventDefault(); + void pasteClipboard(view, event as ClipboardEvent); + + return true; + } +}); /** * 粘贴函数 @@ -209,24 +213,42 @@ async function pasteText(view: EditorView, event?: ClipboardEvent): Promise { + if (view.state.facet(inlineImageEnabledFacet)) { + const options = view.state.facet(inlineImageOptionsFacet); + const pasted = event + ? await pasteInlineImagesFromClipboardEvent(view, event, options.maxDisplayHeight) + : await pasteInlineImagesFromSystemClipboard(view, options.maxDisplayHeight); + + if (pasted) { + return true; + } + } + + return pasteText(view, event); +} + /** * 复制命令 */ -export const copyCommand: Command = view => copyCut(view, false); +export const copyCommand: Command = view => { + void handleCopyCut(view, false); + return true; +}; /** * 剪切命令 */ -export const cutCommand: Command = view => copyCut(view, true); +export const cutCommand: Command = view => { + void handleCopyCut(view, true); + return true; +}; /** * 粘贴命令 */ -export const pasteCommand: Command = (view) => { - void (async () => { - await pasteText(view); - })(); - +export const pasteCommand: Command = view => { + void pasteClipboard(view); return true; }; diff --git a/frontend/src/views/editor/extensions/inlineImage/clipboard.ts b/frontend/src/views/editor/extensions/inlineImage/clipboard.ts new file mode 100644 index 00000000..6b65444d --- /dev/null +++ b/frontend/src/views/editor/extensions/inlineImage/clipboard.ts @@ -0,0 +1,177 @@ +import * as MediaHTTPService from '@/../bindings/voidraft/internal/services/mediahttpservice'; +import type {ImageAsset, ImageDeleteResult} from '@/../bindings/voidraft/internal/services/models'; +import {createInlineImageTag} from './inlineImageParsing'; +import type {InlineImageData} from './types'; + +export const IMAGE_MIME_TYPES = [ + 'image/png', + 'image/jpeg', + 'image/gif', + 'image/webp', + 'image/svg+xml', + 'image/apng', + 'image/avif', + 'image/bmp', + 'image/tiff', +] as const; + +export function canvasToPngBlob(canvas: HTMLCanvasElement): Promise { + return new Promise((resolve, reject) => { + canvas.toBlob(blob => { + if (blob) { + resolve(blob); + return; + } + reject(new Error('Canvas toBlob returned null')); + }, 'image/png'); + }); +} + +async function readBlobAsDataURL(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = () => reject(reader.error || new Error('Failed to read image blob')); + reader.onload = () => resolve(String(reader.result || '')); + reader.readAsDataURL(blob); + }); +} + +function fallbackVersion(value: string | undefined): string { + return value && value.trim() ? value.trim() : String(Date.now()); +} + +function fallbackFilename(mimeType: string): string { + switch (mimeType.toLowerCase()) { + case 'image/jpeg': + return 'clipboard-image.jpg'; + case 'image/webp': + return 'clipboard-image.webp'; + case 'image/gif': + return 'clipboard-image.gif'; + case 'image/svg+xml': + return 'clipboard-image.svg'; + case 'image/avif': + return 'clipboard-image.avif'; + case 'image/bmp': + return 'clipboard-image.bmp'; + case 'image/tiff': + return 'clipboard-image.tiff'; + case 'image/apng': + return 'clipboard-image.apng'; + default: + return 'clipboard-image.png'; + } +} + +async function blobToPngBlob(blob: Blob): Promise { + const image = new Image(); + const blobUrl = URL.createObjectURL(blob); + + try { + await new Promise((resolve, reject) => { + image.onload = () => resolve(); + image.onerror = () => reject(new Error('Failed to decode image blob')); + image.src = blobUrl; + }); + + const canvas = document.createElement('canvas'); + canvas.width = image.naturalWidth; + canvas.height = image.naturalHeight; + + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Failed to create canvas context'); + } + + ctx.drawImage(image, 0, 0); + return await canvasToPngBlob(canvas); + } finally { + URL.revokeObjectURL(blobUrl); + } +} + +export async function writeImageToClipboard(blob: Blob, mimeType = blob.type || 'image/png'): Promise { + const ClipboardItemCtor = globalThis.ClipboardItem; + if (!ClipboardItemCtor || !navigator.clipboard?.write) { + throw new Error('Clipboard image write is not supported in this environment'); + } + + const resolvedType = mimeType.startsWith('image/') ? mimeType : 'image/png'; + if (typeof ClipboardItemCtor.supports === 'function' && ClipboardItemCtor.supports(resolvedType)) { + await navigator.clipboard.write([ + new ClipboardItemCtor({[resolvedType]: blob}), + ]); + return; + } + + const pngBlob = resolvedType === 'image/png' ? blob : await blobToPngBlob(blob); + await navigator.clipboard.write([ + new ClipboardItemCtor({'image/png': pngBlob}), + ]); +} + +export async function copyImage(url: string): Promise { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Fetch failed: ${response.status}`); + } + + const blob = await response.blob(); + if (!blob.type.startsWith('image/')) { + throw new Error(`Not an image content type. Got: ${blob.type}`); + } + + await writeImageToClipboard(blob, blob.type); +} + +export async function importImageBlob(blob: Blob, filename?: string): Promise { + const dataBase64 = await readBlobAsDataURL(blob); + const asset = await MediaHTTPService.ImportImage({ + filename: filename || fallbackFilename(blob.type || 'image/png'), + mime_type: blob.type || undefined, + data_base64: dataBase64, + }); + + if (!asset) { + throw new Error('ImportImage returned no asset'); + } + + return asset; +} + +export async function deleteImageAsset(imageRef: string): Promise { + const result = await MediaHTTPService.DeleteImage(imageRef); + + if (!result) { + throw new Error('DeleteImage returned no result'); + } + + return result; +} + +export function buildVersionedInlineImageUrl(asset: ImageAsset): string { + const separator = asset.url.includes('?') ? '&' : '?'; + return `${asset.url}${separator}v=${encodeURIComponent(fallbackVersion(asset.updated_at))}`; +} + +export function createInlineImageDataFromAsset(asset: ImageAsset, maxDisplayHeight: number): InlineImageData { + const image: InlineImageData = { + id: crypto.randomUUID(), + assetRef: asset.id, + file: buildVersionedInlineImageUrl(asset), + width: asset.width, + height: asset.height, + }; + + const aspect = asset.height > 0 ? asset.width / asset.height : 1; + if ((asset.height / window.devicePixelRatio) > maxDisplayHeight) { + image.displayHeight = maxDisplayHeight; + image.displayWidth = maxDisplayHeight * aspect; + } + + return image; +} + +export function createInlineImageTagFromAsset(asset: ImageAsset, maxDisplayHeight: number): string { + return createInlineImageTag(createInlineImageDataFromAsset(asset, maxDisplayHeight)); +} diff --git a/frontend/src/views/editor/extensions/inlineImage/clipboardIntegration.ts b/frontend/src/views/editor/extensions/inlineImage/clipboardIntegration.ts new file mode 100644 index 00000000..90058b8d --- /dev/null +++ b/frontend/src/views/editor/extensions/inlineImage/clipboardIntegration.ts @@ -0,0 +1,180 @@ +import {EditorSelection, type Extension} from '@codemirror/state'; +import {EditorView} from '@codemirror/view'; +import {codeBlockEvent, CONTENT_EDIT, USER_EVENTS} from '../codeblock/annotation'; +import {copyImage, createInlineImageTagFromAsset, IMAGE_MIME_TYPES, importImageBlob} from './clipboard'; +import {inlineImageIsSelected, inlineImageState} from './inlineImage'; + +function isImageMimeType(type: string): boolean { + return type.startsWith('image/') || IMAGE_MIME_TYPES.includes(type as (typeof IMAGE_MIME_TYPES)[number]); +} + +function extractImageBlobsFromClipboardData(data: DataTransfer | null): Blob[] { + if (!data) { + return []; + } + + const fromItems = Array.from(data.items || []) + .filter(item => item.kind === 'file' && isImageMimeType(item.type)) + .map(item => item.getAsFile()) + .filter((file): file is File => Boolean(file)); + + if (fromItems.length > 0) { + return fromItems; + } + + return Array.from(data.files || []).filter(file => isImageMimeType(file.type)); +} + +async function buildInlineImageTags(blobs: readonly Blob[], maxDisplayHeight: number): Promise { + const tags: string[] = []; + + for (const blob of blobs) { + try { + const asset = await importImageBlob(blob); + tags.push(createInlineImageTagFromAsset(asset, maxDisplayHeight)); + } catch (error) { + console.error('[inlineImage] Failed to import pasted image:', error); + } + } + + return tags; +} + +function insertInlineImageTagsAtSelection(view: EditorView, tags: readonly string[], userEvent: string): boolean { + if (tags.length === 0) { + return false; + } + + const insert = tags.join(''); + const selection = view.state.selection.main; + + view.dispatch(view.state.update({ + changes: { + from: selection.from, + to: selection.to, + insert, + }, + selection: EditorSelection.cursor(selection.from + insert.length), + userEvent, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], + }, {scrollIntoView: true})); + + return true; +} + +function insertInlineImageTagsAtPosition(view: EditorView, pos: number, tags: readonly string[], userEvent: string): boolean { + if (tags.length === 0) { + return false; + } + + const insert = tags.join(''); + view.dispatch(view.state.update({ + changes: {from: pos, to: pos, insert}, + selection: EditorSelection.cursor(pos + insert.length), + userEvent, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], + }, {scrollIntoView: true})); + + return true; +} + +export async function copySelectedInlineImageIfNeeded(view: EditorView): Promise { + const images = view.state.field(inlineImageState, false); + if (!images) { + return false; + } + + for (const image of images) { + if (inlineImageIsSelected(image, view.state.selection.main)) { + await copyImage(image.file); + return true; + } + } + + return false; +} + +export async function pasteInlineImagesFromClipboardEvent( + view: EditorView, + event: ClipboardEvent, + maxDisplayHeight: number, +): Promise { + if (view.state.readOnly) { + return false; + } + + const blobs = extractImageBlobsFromClipboardData(event.clipboardData); + if (blobs.length === 0) { + return false; + } + + const tags = await buildInlineImageTags(blobs, maxDisplayHeight); + return insertInlineImageTagsAtSelection(view, tags, USER_EVENTS.INPUT_PASTE); +} + +export async function pasteInlineImagesFromSystemClipboard( + view: EditorView, + maxDisplayHeight: number, +): Promise { + if (view.state.readOnly || !navigator.clipboard?.read) { + return false; + } + + try { + const items = await navigator.clipboard.read(); + const blobs: Blob[] = []; + + for (const item of items) { + for (const type of item.types) { + if (!isImageMimeType(type)) { + continue; + } + blobs.push(await item.getType(type)); + break; + } + } + + if (blobs.length === 0) { + return false; + } + + const tags = await buildInlineImageTags(blobs, maxDisplayHeight); + return insertInlineImageTagsAtSelection(view, tags, USER_EVENTS.INPUT_PASTE); + } catch (error) { + console.error('[inlineImage] Failed to read clipboard images:', error); + return false; + } +} + +export function createInlineImageDropExtension(maxDisplayHeight: number): Extension { + return EditorView.domEventHandlers({ + dragover(event) { + const files = Array.from(event.dataTransfer?.files || []); + const hasImage = files.some(file => isImageMimeType(file.type)); + if (!hasImage) { + return false; + } + + event.preventDefault(); + return true; + }, + drop(event, view) { + const files = Array.from(event.dataTransfer?.files || []).filter(file => isImageMimeType(file.type)); + if (files.length === 0 || view.state.readOnly) { + return false; + } + + event.preventDefault(); + event.stopPropagation(); + view.focus(); + + const pos = view.posAtCoords({x: event.clientX, y: event.clientY}) ?? view.state.selection.main.head; + void (async () => { + const tags = await buildInlineImageTags(files, maxDisplayHeight); + insertInlineImageTagsAtPosition(view, pos, tags, 'input.drop'); + })(); + + return true; + }, + }); +} diff --git a/frontend/src/views/editor/extensions/inlineImage/index.ts b/frontend/src/views/editor/extensions/inlineImage/index.ts new file mode 100644 index 00000000..4bd40a81 --- /dev/null +++ b/frontend/src/views/editor/extensions/inlineImage/index.ts @@ -0,0 +1,49 @@ +import {Facet, type Extension} from '@codemirror/state'; +import {createInlineImageDropExtension} from './clipboardIntegration'; +import {createInlineImageWidgetExtension} from './inlineImage'; +import type {InlineImageOptions} from './types'; + +const DEFAULT_OPTIONS = { + maxDisplayHeight: 200, +} as const satisfies InlineImageOptions; + +function createDefaultOptions(): InlineImageOptions { + return { + ...DEFAULT_OPTIONS, + }; +} + +function mergeOptions( + base: InlineImageOptions, + patch: Partial, +): InlineImageOptions { + return { + ...base, + ...patch, + }; +} + +export const inlineImageEnabledFacet = Facet.define({ + combine: values => values.some(Boolean), +}); + +export const inlineImageOptionsFacet = Facet.define, InlineImageOptions>({ + combine: values => + values.reduce( + (merged, value) => mergeOptions(merged, value), + createDefaultOptions(), + ), +}); + +export function createInlineImageExtension(options: Partial = {}): Extension { + const resolvedOptions = mergeOptions(createDefaultOptions(), options); + + return [ + inlineImageEnabledFacet.of(true), + inlineImageOptionsFacet.of(options), + createInlineImageWidgetExtension(), + createInlineImageDropExtension(resolvedOptions.maxDisplayHeight), + ]; +} + +export default createInlineImageExtension; diff --git a/frontend/src/views/editor/extensions/inlineImage/inlineImage.ts b/frontend/src/views/editor/extensions/inlineImage/inlineImage.ts new file mode 100644 index 00000000..366ecceb --- /dev/null +++ b/frontend/src/views/editor/extensions/inlineImage/inlineImage.ts @@ -0,0 +1,295 @@ +import {Compartment, RangeSetBuilder, StateField, type Extension, type SelectionRange, type Transaction} from '@codemirror/state'; +import {foldEffect, foldState, unfoldEffect} from '@codemirror/language'; +import {Decoration, type DecorationSet, EditorView, ViewPlugin} from '@codemirror/view'; +import copyDarkIconUrl from '@/assets/icons/copy-dark.svg'; +import pencilWhiteIconUrl from '@/assets/icons/pencil-white.svg'; +import resizeHandleDarkUrl from '@/assets/icons/resize-handle-se-dark.png'; +import resizeHandleLightUrl from '@/assets/icons/resize-handle-se-light.png'; +import {parseInlineImages} from './inlineImageParsing'; +import {InlineImageWidget} from './inlineImageWidget'; +import type {ParsedInlineImage} from './types'; + +export const inlineImageState = StateField.define({ + create(state) { + return parseInlineImages(state); + }, + update(images, transaction) { + if (transaction.docChanged) { + return parseInlineImages(transaction.state); + } + return images; + }, +}); + +function createAtomicRanges(view: EditorView): DecorationSet { + const builder = new RangeSetBuilder(); + + view.state.field(inlineImageState).forEach(image => { + builder.add(image.from, image.to, Decoration.mark({})); + }); + + return builder.finish(); +} + +const atomicInlineImages = ViewPlugin.fromClass(class { + atomicRanges: DecorationSet; + + constructor(view: EditorView) { + this.atomicRanges = createAtomicRanges(view); + } + + update(update: {docChanged: boolean; view: EditorView}) { + if (update.docChanged) { + this.atomicRanges = createAtomicRanges(update.view); + } + } +}, { + provide: plugin => EditorView.atomicRanges.of(view => view.plugin(plugin)?.atomicRanges || Decoration.none), +}); + +function transactionHasFoldEffect(transaction: Transaction): boolean { + return transaction.effects.some(effect => effect.is(foldEffect) || effect.is(unfoldEffect)); +} + +function createInlineImageTheme(): Extension { + return EditorView.baseTheme({ + '&.cm-editor.resizing-image': { + cursor: 'nwse-resize', + }, + '.inline-image': { + '--outline-color': '#2482ce', + '--snapped-outline-color': '#39a363', + '--handle-color': '#ccc', + '--image-border-color': '#c9c9c9', + padding: '6px 2px', + display: 'inline-block', + position: 'relative', + verticalAlign: 'middle', + }, + '&.cm-dark .inline-image': { + '--outline-color': '#0060c7', + '--snapped-outline-color': '#39a363', + '--handle-color': '#192736', + '--image-border-color': '#252525', + }, + '.inline-image.folded': { + padding: '0', + }, + '.inline-image.folded.selected': { + padding: '0', + }, + '.inline-image.folded .resize-handle': { + display: 'none', + }, + '.inline-image .inner': { + position: 'relative', + border: '1px solid var(--image-border-color)', + }, + '.inline-image .inner:hover .buttons-container': { + opacity: '1', + }, + '.inline-image img': { + display: 'block', + maxWidth: '100%', + minWidth: '16px', + minHeight: '16px', + objectFit: 'contain', + }, + '.inline-image .highlight-border': { + display: 'none', + position: 'absolute', + top: '-2px', + right: '-2px', + bottom: '-2px', + left: '-2px', + border: '3px solid var(--outline-color)', + boxSizing: 'border-box', + pointerEvents: 'none', + }, + '.inline-image .buttons-container': { + position: 'absolute', + inset: '0', + display: 'flex', + padding: '7px', + alignItems: 'flex-start', + justifyContent: 'left', + opacity: '0', + overflow: 'hidden', + containerType: 'inline-size', + pointerEvents: 'none', + }, + '.inline-image .buttons-container button': { + height: '24px', + fontSize: '12px', + backgroundColor: '#646e71', + color: '#fff', + opacity: '1', + transition: 'background-color 200ms', + backgroundImage: `url("${copyDarkIconUrl}")`, + backgroundSize: '13px', + backgroundPosition: '6px center', + backgroundRepeat: 'no-repeat', + padding: '3px 7px 3px 22px', + border: 'none', + borderRadius: '3px', + cursor: 'pointer', + boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)', + minWidth: '0', + whiteSpace: 'nowrap', + overflow: 'hidden', + marginRight: '4px', + pointerEvents: 'auto', + }, + '.inline-image .buttons-container button:hover': { + backgroundColor: '#51595c', + opacity: '1', + }, + '.inline-image .buttons-container button.draw': { + backgroundImage: `url("${pencilWhiteIconUrl}")`, + }, + '.inline-image .buttons-container button.delete': { + backgroundImage: 'none', + padding: '3px 8px', + backgroundColor: '#8f3d3d', + }, + '.inline-image .buttons-container button.delete:hover': { + backgroundColor: '#7c3131', + }, + '.inline-image.selected': { + '--handle-color': 'var(--outline-color)', + }, + '.inline-image.selected .highlight-border': { + display: 'block', + }, + '.inline-image.selected .resize-handle': { + opacity: '1', + }, + '.inline-image.selected .resize-handle .icon': { + backgroundImage: `url("${resizeHandleDarkUrl}")`, + }, + '.inline-image:hover .resize-handle': { + opacity: '0.5', + }, + '.inline-image:hover .resize-handle:hover': { + opacity: '1', + }, + '.inline-image .resize-handle': { + opacity: '0', + transition: 'opacity 200ms', + width: '0', + height: '0', + position: 'absolute', + right: '2px', + bottom: '6px', + borderBottom: '10px solid var(--handle-color)', + borderRight: '10px solid var(--handle-color)', + borderLeft: '10px solid transparent', + borderTop: '10px solid transparent', + cursor: 'nwse-resize', + zIndex: '10', + }, + '.inline-image .resize-handle .icon': { + position: 'absolute', + top: '-4px', + left: '-4px', + backgroundImage: `url("${resizeHandleLightUrl}")`, + backgroundSize: '100%', + width: '12px', + height: '12px', + }, + '&.cm-dark .inline-image .resize-handle .icon': { + backgroundImage: `url("${resizeHandleDarkUrl}")`, + }, + '.inline-image.resizing .buttons-container': { + display: 'none', + }, + '.inline-image.resizing .resize-handle': { + opacity: '1', + transition: 'none', + }, + '.inline-image.resizing.snapped': { + '--outline-color': 'var(--snapped-outline-color)', + }, + }); +} + +export interface InlineImageWidgetExtensionOptions { + interactive?: boolean; +} + +export function createInlineImageWidgetExtension(options: InlineImageWidgetExtensionOptions = {}): Extension { + const interactive = options.interactive ?? true; + const domEventCompartment = interactive ? new Compartment() : null; + + const decorate = (state: EditorView['state']): DecorationSet => { + const builder = new RangeSetBuilder(); + const selection = state.selection.main; + const foldStarts = new Set(); + const foldRanges = state.field(foldState, false); + + if (foldRanges) { + foldRanges.between(0, state.doc.length, from => { + foldStarts.add(from); + }); + } + + let foundSelectedInlineImage = false; + + state.field(inlineImageState).forEach(image => { + const isFolded = foldStarts.has(image.to); + const isSelected = !foundSelectedInlineImage && inlineImageIsSelected(image, selection); + + if (isSelected) { + foundSelectedInlineImage = true; + } + + builder.add(image.from, image.to, Decoration.replace({ + widget: new InlineImageWidget({ + id: image.id, + assetRef: image.assetRef, + path: image.file, + width: image.width, + height: image.height, + displayWidth: image.displayWidth, + displayHeight: image.displayHeight, + selected: isSelected, + isFolded, + interactive, + domEventCompartment: domEventCompartment ?? undefined, + }), + inclusive: false, + block: false, + side: 0, + })); + }); + + return builder.finish(); + }; + + const inlineImagesField = StateField.define({ + create(state) { + return decorate(state); + }, + update(widgets, transaction) { + if (transaction.docChanged || transaction.selection || transactionHasFoldEffect(transaction)) { + return decorate(transaction.state); + } + return widgets; + }, + provide(field) { + return EditorView.decorations.from(field); + }, + }); + + return [ + inlineImageState, + inlineImagesField, + atomicInlineImages, + createInlineImageTheme(), + ...(domEventCompartment ? [domEventCompartment.of([])] : []), + ]; +} + +export function inlineImageIsSelected(image: ParsedInlineImage, selection: SelectionRange): boolean { + return selection.from === selection.to && (selection.from === image.from || selection.from === image.to); +} diff --git a/frontend/src/views/editor/extensions/inlineImage/inlineImageParsing.test.ts b/frontend/src/views/editor/extensions/inlineImage/inlineImageParsing.test.ts new file mode 100644 index 00000000..4d3a71b0 --- /dev/null +++ b/frontend/src/views/editor/extensions/inlineImage/inlineImageParsing.test.ts @@ -0,0 +1,72 @@ +import {describe, expect, it} from "vitest"; +import {createInlineImageTag, parseInlineImagesFromString} from "./inlineImageParsing"; + +describe("inlineImage image parsing", () => { + it("parses image tags with required and optional params", () => { + const tag = "<∞img;id=abc;asset=sha256_asset;file=/media/2026/04/test.png;w=1200;h=630;dw=300;dh=200∞>"; + const content = `before ${tag} after`; + + const images = parseInlineImagesFromString(content); + + expect(images).toHaveLength(1); + expect(images[0]).toEqual({ + from: content.indexOf(tag), + to: content.indexOf(tag) + tag.length, + id: "abc", + assetRef: "sha256_asset", + file: "/media/2026/04/test.png", + width: 1200, + height: 630, + displayWidth: 300, + displayHeight: 200, + }); + }); + + it("ignores tags missing required params", () => { + const tag1 = "<∞img;id=1;file=/media/a.png;w=10;h=20∞>"; + const invalid = "<∞img;id=2;file=/media/b.png;w=10∞>"; + const tag2 = "<∞img;id=3;file=/media/c.png;w=30;h=40;dw=15∞>"; + const content = `${tag1} middle ${invalid} tail ${tag2}`; + + const images = parseInlineImagesFromString(content); + + expect(images).toHaveLength(2); + expect(images[0].id).toBe("1"); + expect(images[1].id).toBe("3"); + expect(images[1].displayWidth).toBe(15); + expect(images[1].displayHeight).toBeUndefined(); + }); + + it("does not leak regex state between calls", () => { + const tag = "<∞img;id=1;file=/media/a.png;w=10;h=20∞>"; + const first = parseInlineImagesFromString(tag); + const second = parseInlineImagesFromString(`prefix ${tag}`); + + expect(first).toHaveLength(1); + expect(second).toHaveLength(1); + expect(second[0].from).toBe("prefix ".length); + }); +}); + +describe("inlineImage image tag creation", () => { + it("creates tag with display dimensions", () => { + expect(createInlineImageTag({ + id: "1", + assetRef: "asset-id", + file: "/media/2026/04/test.png", + width: 100, + height: 200, + displayWidth: 50, + displayHeight: 60, + })).toBe("<∞img;id=1;asset=asset-id;file=/media/2026/04/test.png;w=100;h=200;dw=50;dh=60∞>"); + }); + + it("omits display dimensions when missing", () => { + expect(createInlineImageTag({ + id: "1", + file: "/media/2026/04/test.png", + width: 100, + height: 200, + })).toBe("<∞img;id=1;file=/media/2026/04/test.png;w=100;h=200∞>"); + }); +}); diff --git a/frontend/src/views/editor/extensions/inlineImage/inlineImageParsing.ts b/frontend/src/views/editor/extensions/inlineImage/inlineImageParsing.ts new file mode 100644 index 00000000..9a56cbce --- /dev/null +++ b/frontend/src/views/editor/extensions/inlineImage/inlineImageParsing.ts @@ -0,0 +1,174 @@ +import {EditorSelection, type EditorState} from '@codemirror/state'; +import type {EditorView} from '@codemirror/view'; +import {codeBlockEvent, CONTENT_EDIT} from '../codeblock/annotation'; +import type {InlineImageData, ParsedInlineImage} from './types'; + +export const INLINE_IMAGE_TAG_REGEX = /<∞img;([^∞>]*)∞>/g; +export const WIDGET_TAG_REGEX = /<∞.*?∞>/g; +export const WIDGET_TAG_REGEX_NON_GLOBAL = /<∞.*?∞>/; + +const REQUIRED_PARAMS = ['id', 'file', 'w', 'h'] as const; + +export function parseInlineImages(state: EditorState): ParsedInlineImage[] { + return parseInlineImagesFromString(state.doc.sliceString(0, state.doc.length)); +} + +/** + * 解析以下格式的图片标签: + * <∞img;id=uuid;file=/media/2026/04/foo.png;w=1200;h=630;dw=324;dh=170∞> + */ +export function parseInlineImagesFromString(content: string): ParsedInlineImage[] { + INLINE_IMAGE_TAG_REGEX.lastIndex = 0; + + let match: RegExpExecArray | null; + const images: ParsedInlineImage[] = []; + + while ((match = INLINE_IMAGE_TAG_REGEX.exec(content)) !== null) { + try { + const params: Record = {}; + + for (const part of match[1].split(';')) { + if (!part) { + continue; + } + + const eqIndex = part.indexOf('='); + if (eqIndex === -1) { + params[part] = true; + continue; + } + + params[part.slice(0, eqIndex)] = part.slice(eqIndex + 1); + } + + if (!REQUIRED_PARAMS.every(param => Object.hasOwn(params, param))) { + continue; + } + + images.push({ + from: match.index, + to: match.index + match[0].length, + id: String(params.id), + assetRef: params.asset ? String(params.asset) : undefined, + file: String(params.file), + width: Number(params.w), + height: Number(params.h), + displayWidth: params.dw ? Number(params.dw) : undefined, + displayHeight: params.dh ? Number(params.dh) : undefined, + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(`[inlineImage] Bad <∞img> tag at index ${match.index}: ${message}. Tag: ${match[0]}`); + } + } + + return images; +} + +export function createInlineImageTag(image: InlineImageData): string { + const params = [ + `id=${image.id}`, + ...(image.assetRef ? [`asset=${image.assetRef}`] : []), + `file=${image.file}`, + `w=${image.width}`, + `h=${image.height}`, + ]; + + if (image.displayWidth) { + params.push(`dw=${image.displayWidth}`); + } + if (image.displayHeight) { + params.push(`dh=${image.displayHeight}`); + } + + return `<∞img;${params.join(';')}∞>`; +} + +export function setInlineImageDisplayDimensions( + view: EditorView, + id: string, + width: number, + height: number, +): void { + const images = Object.fromEntries(parseInlineImages(view.state).map(image => [image.id, image])); + const image = images[id]; + + if (!image) { + return; + } + + image.displayWidth = width; + image.displayHeight = height; + + view.dispatch(view.state.update({ + changes: { + from: image.from, + to: image.to, + insert: createInlineImageTag(image), + }, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], + })); +} + +export function setInlineImageFile(view: EditorView, id: string, file: string): void { + const images = Object.fromEntries(parseInlineImages(view.state).map(image => [image.id, image])); + const image = images[id]; + + if (!image) { + console.error(`[inlineImage] Image with id ${id} not found`); + return; + } + + image.file = file; + const nextTag = createInlineImageTag(image); + + view.dispatch(view.state.update({ + changes: { + from: image.from, + to: image.to, + insert: nextTag, + }, + selection: EditorSelection.cursor(image.from + nextTag.length, -1), + }, {scrollIntoView: true})); +} + +export function updateInlineImageData(view: EditorView, id: string, patch: Partial): void { + const images = Object.fromEntries(parseInlineImages(view.state).map(image => [image.id, image])); + const image = images[id]; + + if (!image) { + console.error(`[inlineImage] Image with id ${id} not found`); + return; + } + + Object.assign(image, patch); + const nextTag = createInlineImageTag(image); + + view.dispatch(view.state.update({ + changes: { + from: image.from, + to: image.to, + insert: nextTag, + }, + selection: EditorSelection.cursor(image.from + nextTag.length, -1), + }, {scrollIntoView: true})); +} + +export function removeInlineImage(view: EditorView, id: string): void { + const images = Object.fromEntries(parseInlineImages(view.state).map(image => [image.id, image])); + const image = images[id]; + + if (!image) { + console.error(`[inlineImage] Image with id ${id} not found`); + return; + } + + view.dispatch(view.state.update({ + changes: { + from: image.from, + to: image.to, + insert: '', + }, + annotations: [codeBlockEvent.of(CONTENT_EDIT)], + }, {scrollIntoView: true})); +} diff --git a/frontend/src/views/editor/extensions/inlineImage/inlineImageWidget.ts b/frontend/src/views/editor/extensions/inlineImage/inlineImageWidget.ts new file mode 100644 index 00000000..4e26db0d --- /dev/null +++ b/frontend/src/views/editor/extensions/inlineImage/inlineImageWidget.ts @@ -0,0 +1,322 @@ +import type {Compartment} from '@codemirror/state'; +import {EditorView, WidgetType} from '@codemirror/view'; +import i18n from '@/i18n'; +import {copyImage, deleteImageAsset} from './clipboard'; +import {inlineImageDrawManager} from './manager'; +import {removeInlineImage, setInlineImageDisplayDimensions} from './inlineImageParsing'; + +const FOLDED_HEIGHT = 16; + +function t(key: string): string { + return i18n.global.t(key); +} + +export interface InlineImageWidgetConfig { + id: string; + assetRef?: string; + path: string; + width: number; + height: number; + displayWidth?: number; + displayHeight?: number; + selected?: boolean; + isFolded?: boolean; + interactive?: boolean; + domEventCompartment?: Compartment; +} + +export class InlineImageWidget extends WidgetType { + readonly id: string; + readonly assetRef?: string; + readonly path: string; + readonly width: number; + readonly height: number; + readonly displayWidth?: number; + readonly displayHeight?: number; + readonly selected: boolean; + readonly isFolded: boolean; + readonly interactive: boolean; + readonly domEventCompartment?: Compartment; + readonly idealWidth: number; + readonly idealHeight: number; + + constructor(config: InlineImageWidgetConfig) { + super(); + this.id = config.id; + this.assetRef = config.assetRef; + this.path = config.path; + this.width = config.width; + this.height = config.height; + this.displayWidth = config.displayWidth; + this.displayHeight = config.displayHeight; + this.selected = Boolean(config.selected); + this.isFolded = Boolean(config.isFolded); + this.interactive = config.interactive ?? true; + this.domEventCompartment = config.domEventCompartment; + this.idealWidth = this.width / window.devicePixelRatio; + this.idealHeight = this.height / window.devicePixelRatio; + } + + override eq(other: InlineImageWidget): boolean { + return other.path === this.path + && other.width === this.width + && other.height === this.height + && other.displayWidth === this.displayWidth + && other.displayHeight === this.displayHeight + && other.selected === this.selected + && other.isFolded === this.isFolded + && other.id === this.id + && other.assetRef === this.assetRef + && other.interactive === this.interactive; + } + + override toDOM(view: EditorView): HTMLElement { + const wrap = document.createElement('div'); + wrap.className = this.getClassName(); + wrap.dataset.id = this.id; + wrap.dataset.idealWidth = String(this.idealWidth); + wrap.dataset.idealHeight = String(this.idealHeight); + + const inner = document.createElement('div'); + inner.className = 'inner'; + wrap.appendChild(inner); + + const highlightBorder = document.createElement('div'); + highlightBorder.className = 'highlight-border'; + inner.appendChild(highlightBorder); + + let copyButton: HTMLButtonElement | null = null; + let drawButton: HTMLButtonElement | null = null; + let deleteButton: HTMLButtonElement | null = null; + + if (this.interactive && !this.isFolded) { + const buttonsContainer = document.createElement('div'); + buttonsContainer.className = 'buttons-container'; + inner.appendChild(buttonsContainer); + + copyButton = document.createElement('button'); + copyButton.type = 'button'; + copyButton.title = t('inlineImage.copy'); + copyButton.innerHTML = `${t('inlineImage.copy')}`; + buttonsContainer.appendChild(copyButton); + + drawButton = document.createElement('button'); + drawButton.type = 'button'; + drawButton.className = 'draw'; + drawButton.title = t('inlineImage.draw'); + drawButton.innerHTML = `${t('inlineImage.draw')}`; + buttonsContainer.appendChild(drawButton); + + deleteButton = document.createElement('button'); + deleteButton.type = 'button'; + deleteButton.className = 'delete'; + deleteButton.title = t('inlineImage.delete'); + deleteButton.innerHTML = `${t('inlineImage.delete')}`; + buttonsContainer.appendChild(deleteButton); + + copyButton.addEventListener('mousedown', event => { + event.preventDefault(); + }); + drawButton.addEventListener('mousedown', event => { + event.preventDefault(); + }); + deleteButton.addEventListener('mousedown', event => { + event.preventDefault(); + }); + } + + const image = document.createElement('img'); + image.src = this.path; + image.style.width = this.getWidth(); + image.style.height = this.getHeight(); + inner.appendChild(image); + + if (copyButton) { + copyButton.addEventListener('click', async event => { + event.preventDefault(); + try { + await copyImage(image.src); + copyButton!.innerText = t('inlineImage.copied'); + setTimeout(() => { + copyButton!.innerHTML = `${t('inlineImage.copy')}`; + }, 2000); + } catch (error) { + console.error('[inlineImage] Failed to copy image:', error); + } + }); + } + + if (drawButton) { + drawButton.addEventListener('click', event => { + event.preventDefault(); + inlineImageDrawManager.show(view, this.id, this.assetRef || this.path, this.path); + }); + } + + if (deleteButton) { + deleteButton.addEventListener('click', async event => { + event.preventDefault(); + try { + await deleteImageAsset(this.assetRef || this.path); + removeInlineImage(view, this.id); + } catch (error) { + console.error('[inlineImage] Failed to delete image:', error); + } + }); + } + + if (this.interactive && !this.isFolded && this.domEventCompartment) { + const resizeHandle = document.createElement('div'); + resizeHandle.className = 'resize-handle'; + resizeHandle.innerHTML = '
'; + wrap.appendChild(resizeHandle); + this.attachResizeBehavior(view, wrap, image, resizeHandle); + } + + return wrap; + } + + override updateDOM(dom: HTMLElement): boolean { + dom.className = this.getClassName(); + dom.dataset.id = this.id; + dom.dataset.idealWidth = String(this.idealWidth); + dom.dataset.idealHeight = String(this.idealHeight); + + const image = dom.querySelector('img'); + if (!(image instanceof HTMLImageElement)) { + return false; + } + + image.src = this.path; + image.style.width = this.getWidth(); + image.style.height = this.getHeight(); + return true; + } + + override ignoreEvent(): boolean { + return false; + } + + private getClassName(): string { + return `inline-image${this.selected ? ' selected' : ''}${this.isFolded ? ' folded' : ''}`; + } + + private getWidth(): string { + let width: number | undefined; + + if (this.isFolded) { + width = FOLDED_HEIGHT * (this.width / this.height); + } else if (this.displayWidth) { + width = this.displayWidth; + } else { + width = this.idealWidth; + } + + return width ? `${width}px` : ''; + } + + private getHeight(): string { + let height: number; + + if (this.isFolded) { + height = FOLDED_HEIGHT; + } else if (this.displayHeight) { + height = this.displayHeight; + } else { + height = this.idealHeight; + } + + return `${height}px`; + } + + private attachResizeBehavior( + view: EditorView, + wrap: HTMLDivElement, + image: HTMLImageElement, + resizeHandle: HTMLDivElement, + ): void { + let initialWidth = 0; + let initialHeight = 0; + let initialX = 0; + let initialY = 0; + let shouldSnap = true; + + const onMouseMove = (event: MouseEvent) => { + const idealWidth = Number(wrap.dataset.idealWidth); + const idealHeight = Number(wrap.dataset.idealHeight); + const aspect = idealWidth / idealHeight; + + let width = initialWidth + (event.pageX - initialX); + let height = initialHeight + (event.pageY - initialY); + + const heightFromWidth = width / aspect; + const widthFromHeight = height * aspect; + + if (heightFromWidth <= height) { + height = heightFromWidth; + } else { + width = widthFromHeight; + } + + const snapTolerance = 10; + if (shouldSnap) { + if (Math.abs(width - idealWidth) <= snapTolerance || Math.abs(height - idealHeight) <= snapTolerance) { + height = idealHeight; + width = idealWidth; + wrap.classList.add('snapped'); + } else { + wrap.classList.remove('snapped'); + } + } else if (Math.abs(width - idealWidth) > snapTolerance && Math.abs(height - idealHeight) > snapTolerance) { + shouldSnap = true; + } + + width = Math.max(width, 16); + height = width / aspect; + if (height < 17) { + height = 17; + width = height * aspect; + } + + image.style.width = `${width}px`; + image.style.height = `${height}px`; + }; + + const endResize = () => { + view.dispatch({ + effects: [this.domEventCompartment!.reconfigure([])], + }); + setInlineImageDisplayDimensions(view, wrap.dataset.id || '', image.width, image.height); + setTimeout(() => { + wrap.classList.remove('resizing'); + }, 200); + }; + + resizeHandle.addEventListener('mousedown', event => { + event.preventDefault(); + initialWidth = image.getBoundingClientRect().width; + initialHeight = image.getBoundingClientRect().height; + initialX = event.pageX; + initialY = event.pageY; + shouldSnap = initialWidth !== this.idealWidth; + wrap.classList.add('resizing'); + + view.dispatch({ + effects: [this.domEventCompartment!.reconfigure([ + EditorView.domEventObservers({ + mousemove: moveEvent => { + onMouseMove(moveEvent as MouseEvent); + }, + mouseup: () => { + endResize(); + }, + mouseleave: () => { + endResize(); + }, + }), + EditorView.editorAttributes.of({class: 'resizing-image'}), + ])], + }); + }); + } +} diff --git a/frontend/src/views/editor/extensions/inlineImage/manager.ts b/frontend/src/views/editor/extensions/inlineImage/manager.ts new file mode 100644 index 00000000..86e895a7 --- /dev/null +++ b/frontend/src/views/editor/extensions/inlineImage/manager.ts @@ -0,0 +1,65 @@ +import type {EditorView} from '@codemirror/view'; +import {shallowReadonly, shallowRef, type ShallowRef} from 'vue'; + +interface InlineImageDrawState { + visible: boolean; + tagId: string; + assetRef: string; + imageUrl: string; + view: EditorView | null; +} + +class InlineImageDrawManager { + private state: ShallowRef = shallowRef({ + visible: false, + tagId: '', + assetRef: '', + imageUrl: '', + view: null, + }); + + useState() { + return shallowReadonly(this.state); + } + + show(view: EditorView, tagId: string, assetRef: string, imageUrl: string): void { + this.state.value = { + visible: true, + tagId, + assetRef, + imageUrl, + view, + }; + } + + hide(): void { + if (!this.state.value.visible) { + return; + } + + const view = this.state.value.view; + this.state.value = { + visible: false, + tagId: '', + assetRef: '', + imageUrl: '', + view: null, + }; + + if (view) { + view.focus(); + } + } + + destroy(): void { + this.state.value = { + visible: false, + tagId: '', + assetRef: '', + imageUrl: '', + view: null, + }; + } +} + +export const inlineImageDrawManager = new InlineImageDrawManager(); diff --git a/frontend/src/views/editor/extensions/inlineImage/types.ts b/frontend/src/views/editor/extensions/inlineImage/types.ts new file mode 100644 index 00000000..c99bbc89 --- /dev/null +++ b/frontend/src/views/editor/extensions/inlineImage/types.ts @@ -0,0 +1,18 @@ +export interface InlineImageOptions { + maxDisplayHeight: number; +} + +export interface InlineImageData { + id: string; + assetRef?: string; + file: string; + width: number; + height: number; + displayWidth?: number; + displayHeight?: number; +} + +export interface ParsedInlineImage extends InlineImageData { + from: number; + to: number; +} diff --git a/frontend/src/views/editor/manager/extensions.ts b/frontend/src/views/editor/manager/extensions.ts index ef5f2090..86f4ed97 100644 --- a/frontend/src/views/editor/manager/extensions.ts +++ b/frontend/src/views/editor/manager/extensions.ts @@ -15,6 +15,7 @@ import {highlightActiveLineGutter, highlightWhitespace, highlightTrailingWhitesp import createEditorContextMenu from '../extensions/contextMenu'; import {blockLineNumbers} from '../extensions/codeblock'; import {createHttpClientExtension} from '../extensions/httpclient'; +import {createInlineImageExtension} from '../extensions/inlineImage'; import {createBlockImageExtension} from '../extensions/blockImage'; import {createBlockReadonlyExtension} from '../extensions/blockReadonly'; import {ExtensionName} from '@/../bindings/voidraft/internal/models/models'; @@ -107,6 +108,11 @@ const EXTENSION_REGISTRY: Record = { displayNameKey: 'extensions.httpClient.name', descriptionKey: 'extensions.httpClient.description' }, + [ExtensionName.InlineImage]: { + definition: defineExtension(() => createInlineImageExtension()), + displayNameKey: 'extensions.inlineImage.name', + descriptionKey: 'extensions.inlineImage.description' + }, [ExtensionName.BlockImage]: { definition: defineExtension(() => createBlockImageExtension()), displayNameKey: 'extensions.blockImage.name', diff --git a/frontend/src/views/settings/pages/TestPage.vue b/frontend/src/views/settings/pages/TestPage.vue index d99a1486..78555bb3 100644 --- a/frontend/src/views/settings/pages/TestPage.vue +++ b/frontend/src/views/settings/pages/TestPage.vue @@ -212,11 +212,11 @@
Relative Path - {{ selectedImage.relative_path }} + {{ selectedImage.path }}
Original Name - {{ selectedImage.original_filename || '-' }} + {{ selectedImage.filename || '-' }}
Size diff --git a/internal/common/helper/media_helper.go b/internal/common/helper/media_helper.go index ae4c1d46..3042e3d4 100644 --- a/internal/common/helper/media_helper.go +++ b/internal/common/helper/media_helper.go @@ -11,6 +11,7 @@ import ( _ "image/jpeg" _ "image/png" "net/http" + "net/url" "os" "path" "path/filepath" @@ -93,6 +94,9 @@ func (h *MediaHelper) NormalizeImageReference(value string, routePrefix string) if clean == "" { return "", fmt.Errorf("image path is required") } + if parsed, err := url.Parse(clean); err == nil && parsed.Scheme != "" && strings.TrimSpace(parsed.Path) != "" { + clean = parsed.Path + } if strings.TrimSpace(routePrefix) != "" && strings.HasPrefix(clean, routePrefix+"/") { clean = strings.TrimPrefix(clean, routePrefix+"/") } diff --git a/internal/common/syncer/app.go b/internal/common/syncer/app.go index 12a2e11a..04cd4e2e 100644 --- a/internal/common/syncer/app.go +++ b/internal/common/syncer/app.go @@ -25,9 +25,8 @@ const ( // Options describes app construction options. type Options struct { - Logger Logger - MaxSyncAttempts int - MediaRootResolve func() string + Logger Logger + MaxSyncAttempts int } // App coordinates the sync system. @@ -57,9 +56,6 @@ func NewApp(client *ent.Client, options Options) *App { resource2.NewKeyBindingAdapter(client), resource2.NewThemeAdapter(client), } - if options.MediaRootResolve != nil { - adapters = append(adapters, resource2.NewMediaAssetAdapter(client, options.MediaRootResolve)) - } return &App{ logger: options.Logger, diff --git a/internal/common/syncer/resource/media_asset_adapter.go b/internal/common/syncer/resource/media_asset_adapter.go deleted file mode 100644 index fbeb4f01..00000000 --- a/internal/common/syncer/resource/media_asset_adapter.go +++ /dev/null @@ -1,396 +0,0 @@ -package resource - -import ( - "context" - "fmt" - "os" - "path/filepath" - "strings" - "time" - "voidraft/internal/common/helper" - "voidraft/internal/common/syncer/snapshot" - "voidraft/internal/models/ent" - "voidraft/internal/models/ent/mediaasset" - schemamixin "voidraft/internal/models/schema/mixin" -) - -const mediaAssetBlobName = "original.bin" - -const mediaAssetExportBatchSize = 256 - -// MediaAssetAdapter syncs indexed media assets and their original image blobs. -type MediaAssetAdapter struct { - client *ent.Client - mediaHelper *helper.MediaHelper - mediaRootResolve func() string -} - -// NewMediaAssetAdapter creates a media asset adapter. -func NewMediaAssetAdapter(client *ent.Client, mediaRootResolve func() string) *MediaAssetAdapter { - return &MediaAssetAdapter{ - client: client, - mediaHelper: helper.NewMediaHelper(), - mediaRootResolve: mediaRootResolve, - } -} - -// Kind returns the adapter resource kind. -func (a *MediaAssetAdapter) Kind() string { - return "media_assets" -} - -// Export exports indexed media assets and their original image blobs. -func (a *MediaAssetAdapter) Export(ctx context.Context) ([]snapshot.Record, error) { - rootPath, err := a.mediaRoot() - if err != nil { - return nil, err - } - - records := make([]snapshot.Record, 0) - lastAssetID := "" - for { - query := a.client.MediaAsset.Query(). - Order(mediaasset.ByAssetID()). - Limit(mediaAssetExportBatchSize) - if lastAssetID != "" { - query = query.Where(mediaasset.AssetIDGT(lastAssetID)) - } - - assets, err := query.All(exportContext(ctx)) - if err != nil { - return nil, err - } - if len(assets) == 0 { - return records, nil - } - - for _, item := range assets { - values := map[string]interface{}{ - mediaasset.FieldCreatedAt: item.CreatedAt, - mediaasset.FieldUpdatedAt: item.UpdatedAt, - mediaasset.FieldDeletedAt: nullableStringValue(item.DeletedAt), - mediaasset.FieldAssetID: item.AssetID, - mediaasset.FieldRelativePath: item.RelativePath, - mediaasset.FieldMimeType: item.MimeType, - mediaasset.FieldSize: item.Size, - mediaasset.FieldWidth: item.Width, - mediaasset.FieldHeight: item.Height, - } - if item.OriginalFilename != nil { - values[mediaasset.FieldOriginalFilename] = *item.OriginalFilename - } - - record, err := a.buildExportRecord(ctx, rootPath, item, values) - if err != nil { - return nil, err - } - if record.Kind == "" { - continue - } - records = append(records, record) - } - - lastAssetID = assets[len(assets)-1].AssetID - } -} - -// Apply applies media asset records to the local database and filesystem. -func (a *MediaAssetAdapter) Apply(ctx context.Context, records []snapshot.Record) error { - applyCtx := importContext(ctx) - rootPath, err := a.mediaRoot() - if err != nil { - return err - } - - for _, record := range records { - found, err := a.client.MediaAsset.Query(). - Where(mediaasset.AssetIDEQ(record.ID)). - First(schemamixin.SkipSoftDelete(applyCtx)) - switch { - case ent.IsNotFound(err): - if err := a.create(applyCtx, rootPath, record); err != nil { - return err - } - case err != nil: - return err - default: - if shouldApplyRecord(found.UpdatedAt, record) { - if err := a.update(applyCtx, rootPath, found, record); err != nil { - return err - } - } - } - } - - return nil -} - -func (a *MediaAssetAdapter) create(ctx context.Context, rootPath string, record snapshot.Record) error { - relativePath, _, err := a.mediaHelper.ResolveManagedImagePath(rootPath, stringValue(record, mediaasset.FieldRelativePath), "") - if err != nil { - return fmt.Errorf("resolve media asset path %s: %w", record.ID, err) - } - - builder := a.client.MediaAsset.Create(). - SetAssetID(record.ID). - SetRelativePath(relativePath). - SetMimeType(stringValue(record, mediaasset.FieldMimeType)). - SetSize(int64Value(record, mediaasset.FieldSize)). - SetWidth(intValue(record, mediaasset.FieldWidth)). - SetHeight(intValue(record, mediaasset.FieldHeight)). - SetCreatedAt(stringValue(record, mediaasset.FieldCreatedAt)). - SetUpdatedAt(stringValue(record, mediaasset.FieldUpdatedAt)). - SetNillableOriginalFilename(optionalStringValue(record, mediaasset.FieldOriginalFilename)) - - if deletedAt := recordDeletedAtString(record); deletedAt != nil { - return builder.SetDeletedAt(*deletedAt).Exec(ctx) - } - - prepared, err := a.prepareActiveRecordBlob(rootPath, record) - if err != nil { - return err - } - return a.applyActiveBlob(prepared.absPath, prepared.data, "create synced media asset", builder.Exec, ctx) -} - -func (a *MediaAssetAdapter) update(ctx context.Context, rootPath string, current *ent.MediaAsset, record snapshot.Record) error { - nextRelativePath, _, err := a.mediaHelper.ResolveManagedImagePath(rootPath, stringValue(record, mediaasset.FieldRelativePath), "") - if err != nil { - return fmt.Errorf("resolve media asset path %s: %w", record.ID, err) - } - update := a.client.MediaAsset.UpdateOneID(current.ID). - SetRelativePath(nextRelativePath). - SetMimeType(stringValue(record, mediaasset.FieldMimeType)). - SetSize(int64Value(record, mediaasset.FieldSize)). - SetWidth(intValue(record, mediaasset.FieldWidth)). - SetHeight(intValue(record, mediaasset.FieldHeight)). - SetUpdatedAt(stringValue(record, mediaasset.FieldUpdatedAt)) - if name := optionalStringValue(record, mediaasset.FieldOriginalFilename); name != nil { - update.SetOriginalFilename(*name) - } else { - update.ClearOriginalFilename() - } - - if deletedAt := recordDeletedAtString(record); deletedAt != nil { - update.SetDeletedAt(*deletedAt) - if err := update.Exec(ctx); err != nil { - return err - } - if err := a.removeBlobAtPath(rootPath, current.RelativePath); err != nil { - return err - } - if current.RelativePath != nextRelativePath { - if err := a.removeBlobAtPath(rootPath, nextRelativePath); err != nil { - return err - } - } - return nil - } - - update.ClearDeletedAt() - prepared, err := a.prepareActiveRecordBlob(rootPath, record) - if err != nil { - return err - } - if err := a.applyActiveBlob(prepared.absPath, prepared.data, "update synced media asset", update.Exec, ctx); err != nil { - return err - } - if current.RelativePath != nextRelativePath { - if err := a.removeBlobAtPath(rootPath, current.RelativePath); err != nil { - return err - } - } - return nil -} - -func (a *MediaAssetAdapter) mediaRoot() (string, error) { - if a.mediaRootResolve == nil { - return "", fmt.Errorf("media root resolver is not configured") - } - rootPath := strings.TrimSpace(a.mediaRootResolve()) - if rootPath == "" { - return "", fmt.Errorf("media root path is not configured") - } - return rootPath, nil -} - -func (a *MediaAssetAdapter) buildExportRecord(ctx context.Context, rootPath string, item *ent.MediaAsset, values map[string]interface{}) (snapshot.Record, error) { - relativePath, blobPath, err := a.mediaHelper.ResolveManagedImagePath(rootPath, item.RelativePath, "") - if err != nil { - return snapshot.Record{}, fmt.Errorf("resolve media asset path %s: %w", item.AssetID, err) - } - values[mediaasset.FieldRelativePath] = relativePath - - if item.DeletedAt != nil { - record, err := snapshot.NewRecord(a.Kind(), item.AssetID, values, nil) - if err != nil { - return snapshot.Record{}, fmt.Errorf("build media asset record %s: %w", item.AssetID, err) - } - return record, nil - } - - info, err := os.Stat(blobPath) - if err != nil { - if os.IsNotExist(err) { - restored, restoreErr := a.mediaHelper.RestoreLatestStagedFile(blobPath) - if restoreErr != nil { - return snapshot.Record{}, fmt.Errorf("restore staged media asset blob %s: %w", item.AssetID, restoreErr) - } - if restored { - info, err = os.Stat(blobPath) - } - } - if err != nil { - if os.IsNotExist(err) { - return snapshot.Record{}, fmt.Errorf("active media asset blob %s is missing at %s", item.AssetID, relativePath) - } - return snapshot.Record{}, fmt.Errorf("stat media asset blob %s: %w", item.AssetID, err) - } - } - if info.IsDir() { - return snapshot.Record{}, fmt.Errorf("media asset blob %s resolves to a directory", item.AssetID) - } - if err := a.mediaHelper.DiscardStagedFiles(blobPath); err != nil { - return snapshot.Record{}, fmt.Errorf("discard staged media asset blobs %s: %w", item.AssetID, err) - } - - record, err := snapshot.NewRecordWithBlobFiles(a.Kind(), item.AssetID, values, map[string]string{ - mediaAssetBlobName: blobPath, - }) - if err != nil { - return snapshot.Record{}, fmt.Errorf("build media asset record %s: %w", item.AssetID, err) - } - return record, nil -} - -type preparedMediaBlob struct { - absPath string - data []byte -} - -func (a *MediaAssetAdapter) prepareActiveRecordBlob(rootPath string, record snapshot.Record) (*preparedMediaBlob, error) { - if !record.HasBlobs() { - return nil, fmt.Errorf("active media asset record %s is missing blob", record.ID) - } - - data, ok, err := record.BlobBytes(mediaAssetBlobName) - if err != nil { - return nil, fmt.Errorf("read media blob: %w", err) - } - if !ok { - return nil, fmt.Errorf("active media asset record %s is missing blob", record.ID) - } - - expectedAssetID := strings.TrimSpace(stringValue(record, mediaasset.FieldAssetID)) - if expectedAssetID != "" && expectedAssetID != record.ID { - return nil, fmt.Errorf("media asset record id mismatch: %s != %s", expectedAssetID, record.ID) - } - - relativePath, absPath, err := a.mediaHelper.ResolveManagedImagePath(rootPath, stringValue(record, mediaasset.FieldRelativePath), "") - if err != nil { - return nil, fmt.Errorf("resolve media blob path: %w", err) - } - - payload, err := a.mediaHelper.InspectImagePayload(data, stringValue(record, mediaasset.FieldMimeType), filepath.Base(relativePath)) - if err != nil { - return nil, fmt.Errorf("inspect media blob %s: %w", record.ID, err) - } - if payload.Digest != record.ID { - return nil, fmt.Errorf("media asset %s blob digest mismatch", record.ID) - } - if expectedMime := strings.TrimSpace(stringValue(record, mediaasset.FieldMimeType)); expectedMime != "" && !strings.EqualFold(payload.MimeType, expectedMime) { - return nil, fmt.Errorf("media asset %s blob mime mismatch", record.ID) - } - if expectedSize := int64Value(record, mediaasset.FieldSize); expectedSize != 0 && int64(len(data)) != expectedSize { - return nil, fmt.Errorf("media asset %s blob size mismatch", record.ID) - } - if expectedWidth := intValue(record, mediaasset.FieldWidth); expectedWidth != 0 && payload.Width != expectedWidth { - return nil, fmt.Errorf("media asset %s blob width mismatch", record.ID) - } - if expectedHeight := intValue(record, mediaasset.FieldHeight); expectedHeight != 0 && payload.Height != expectedHeight { - return nil, fmt.Errorf("media asset %s blob height mismatch", record.ID) - } - - return &preparedMediaBlob{ - absPath: absPath, - data: data, - }, nil -} - -func (a *MediaAssetAdapter) applyActiveBlob(absPath string, data []byte, action string, persist func(context.Context) error, ctx context.Context) error { - stagedPath, err := a.mediaHelper.StageFile(absPath, time.Time{}) - if err != nil { - return err - } - if err := a.mediaHelper.WriteBinaryFile(absPath, data); err != nil { - rollbackErr := a.mediaHelper.RollbackFileChange(absPath, stagedPath) - return helper.WrapRollbackError(action, err, rollbackErr) - } - if err := persist(ctx); err != nil { - rollbackErr := a.mediaHelper.RollbackFileChange(absPath, stagedPath) - return helper.WrapRollbackError(action, err, rollbackErr) - } - if err := a.mediaHelper.DiscardStagedFile(stagedPath); err != nil { - return fmt.Errorf("discard staged media blob: %w", err) - } - return nil -} - -func (a *MediaAssetAdapter) removeBlobAtPath(rootPath string, relativePath string) error { - if strings.TrimSpace(relativePath) == "" { - return nil - } - _, absPath, err := a.mediaHelper.ResolveManagedImagePath(rootPath, relativePath, "") - if err != nil { - return nil - } - if _, err := a.mediaHelper.RemoveImageArtifacts(absPath); err != nil { - return err - } - if err := a.mediaHelper.DiscardStagedFiles(absPath); err != nil { - return err - } - a.mediaHelper.TrimEmptyMediaDirs(rootPath, absPath) - return nil -} - -func nullableStringValue(value *string) string { - if value == nil { - return "" - } - return *value -} - -func optionalStringValue(record snapshot.Record, key string) *string { - value := strings.TrimSpace(stringValue(record, key)) - if value == "" { - return nil - } - return &value -} - -func int64Value(record snapshot.Record, key string) int64 { - switch value := record.Values[key].(type) { - case int64: - return value - case int: - return int64(value) - case float64: - return int64(value) - default: - return 0 - } -} - -func intValue(record snapshot.Record, key string) int { - switch value := record.Values[key].(type) { - case int: - return value - case int64: - return int(value) - case float64: - return int(value) - default: - return 0 - } -} diff --git a/internal/common/syncer/resource/media_asset_adapter_test.go b/internal/common/syncer/resource/media_asset_adapter_test.go deleted file mode 100644 index a6ee4075..00000000 --- a/internal/common/syncer/resource/media_asset_adapter_test.go +++ /dev/null @@ -1,477 +0,0 @@ -package resource - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "fmt" - "image" - "image/color" - "image/png" - "os" - "path/filepath" - "testing" - "time" - "voidraft/internal/common/syncer/snapshot" - "voidraft/internal/models/ent" - "voidraft/internal/models/ent/enttest" - "voidraft/internal/models/ent/mediaasset" - schemamixin "voidraft/internal/models/schema/mixin" - - _ "github.com/mattn/go-sqlite3" -) - -func failMediaAssetMutations(client *ent.Client, op ent.Op, err error) { - client.MediaAsset.Use(func(next ent.Mutator) ent.Mutator { - return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) { - if m.Op().Is(op) { - return nil, err - } - return next.Mutate(ctx, m) - }) - }) -} - -func TestMediaAssetAdapterExportAndApply(t *testing.T) { - sourceClient := enttest.Open(t, "sqlite3", "file:mediaasset_export?mode=memory&_fk=1") - defer sourceClient.Close() - - sourceRoot := filepath.Join(t.TempDir(), "media") - const relativePath = "images/2026/03/31/example.png" - sourceBytes := mustEncodePNG(t, 3, 2) - assetID := sha256Hex(sourceBytes) - writeBlob(t, sourceRoot, relativePath, sourceBytes) - - _, err := sourceClient.MediaAsset.Create(). - SetAssetID(assetID). - SetOriginalFilename("example.png"). - SetRelativePath(relativePath). - SetMimeType("image/png"). - SetSize(int64(len(sourceBytes))). - SetWidth(3). - SetHeight(2). - SetCreatedAt("2026-03-31T10:00:00Z"). - SetUpdatedAt("2026-03-31T10:00:00Z"). - Save(context.Background()) - if err != nil { - t.Fatalf("create source media asset: %v", err) - } - - exportAdapter := NewMediaAssetAdapter(sourceClient, func() string { return sourceRoot }) - records, err := exportAdapter.Export(context.Background()) - if err != nil { - t.Fatalf("export records: %v", err) - } - if len(records) != 1 { - t.Fatalf("expected 1 exported record, got %d", len(records)) - } - got, ok, err := records[0].BlobBytes(mediaAssetBlobName) - if err != nil { - t.Fatalf("read exported blob: %v", err) - } - if !ok || !bytes.Equal(got, sourceBytes) { - t.Fatal("expected exported blob bytes to match original image") - } - - targetClient := enttest.Open(t, "sqlite3", "file:mediaasset_apply?mode=memory&_fk=1") - defer targetClient.Close() - - targetRoot := filepath.Join(t.TempDir(), "media") - applyAdapter := NewMediaAssetAdapter(targetClient, func() string { return targetRoot }) - if err := applyAdapter.Apply(context.Background(), records); err != nil { - t.Fatalf("apply records: %v", err) - } - - saved, err := targetClient.MediaAsset.Query(). - Where(mediaasset.AssetIDEQ(assetID)). - Only(context.Background()) - if err != nil { - t.Fatalf("query applied media asset: %v", err) - } - if saved.RelativePath != relativePath { - t.Fatalf("expected relative path %q, got %q", relativePath, saved.RelativePath) - } - - targetBytes, err := os.ReadFile(filepath.Join(targetRoot, filepath.FromSlash(relativePath))) - if err != nil { - t.Fatalf("read applied blob: %v", err) - } - if !bytes.Equal(targetBytes, sourceBytes) { - t.Fatal("expected applied blob bytes to match original image") - } -} - -func TestMediaAssetAdapterExportFailsWhenActiveFileMissing(t *testing.T) { - client := enttest.Open(t, "sqlite3", "file:mediaasset_missing?mode=memory&_fk=1") - defer client.Close() - - root := filepath.Join(t.TempDir(), "media") - sourceBytes := mustEncodePNG(t, 1, 1) - assetID := sha256Hex(sourceBytes) - - _, err := client.MediaAsset.Create(). - SetAssetID(assetID). - SetOriginalFilename("missing.png"). - SetRelativePath("images/2026/03/31/missing.png"). - SetMimeType("image/png"). - SetSize(int64(len(sourceBytes))). - SetWidth(1). - SetHeight(1). - SetCreatedAt("2026-03-31T10:00:00Z"). - SetUpdatedAt("2026-03-31T10:00:00Z"). - Save(context.Background()) - if err != nil { - t.Fatalf("create media asset: %v", err) - } - - adapter := NewMediaAssetAdapter(client, func() string { return root }) - if _, err := adapter.Export(context.Background()); err == nil { - t.Fatal("expected export to fail when active blob is missing") - } - - _, err = client.MediaAsset.Query(). - Where(mediaasset.AssetIDEQ(assetID)). - Only(schemamixin.SkipSoftDelete(context.Background())) - if err != nil { - t.Fatalf("expected missing-file media asset index row to remain, err=%v", err) - } -} - -func TestMediaAssetAdapterExportRestoresStagedBlob(t *testing.T) { - client := enttest.Open(t, "sqlite3", "file:mediaasset_staged_export?mode=memory&_fk=1") - defer client.Close() - - root := filepath.Join(t.TempDir(), "media") - const relativePath = "images/2026/03/31/staged.png" - sourceBytes := mustEncodePNG(t, 2, 2) - assetID := sha256Hex(sourceBytes) - writeBlob(t, root, relativePath, sourceBytes) - - _, err := client.MediaAsset.Create(). - SetAssetID(assetID). - SetOriginalFilename("staged.png"). - SetRelativePath(relativePath). - SetMimeType("image/png"). - SetSize(int64(len(sourceBytes))). - SetWidth(2). - SetHeight(2). - SetCreatedAt("2026-03-31T10:00:00Z"). - SetUpdatedAt("2026-03-31T10:00:00Z"). - Save(context.Background()) - if err != nil { - t.Fatalf("create media asset: %v", err) - } - - adapter := NewMediaAssetAdapter(client, func() string { return root }) - absPath := filepath.Join(root, filepath.FromSlash(relativePath)) - stagedPath, err := adapter.mediaHelper.StageFile(absPath, time.Date(2026, 3, 31, 12, 0, 0, 0, time.UTC)) - if err != nil { - t.Fatalf("stage blob: %v", err) - } - if stagedPath == "" { - t.Fatal("expected staged blob path") - } - - records, err := adapter.Export(context.Background()) - if err != nil { - t.Fatalf("export staged blob: %v", err) - } - if len(records) != 1 { - t.Fatalf("expected 1 record after staged restore, got %d", len(records)) - } - if _, err := os.Stat(absPath); err != nil { - t.Fatalf("expected staged blob to be restored: %v", err) - } - if _, err := os.Stat(stagedPath); !os.IsNotExist(err) { - t.Fatalf("expected staged blob to be consumed, stat err=%v", err) - } -} - -func TestMediaAssetAdapterApplyDeletesLocalFile(t *testing.T) { - client := enttest.Open(t, "sqlite3", "file:mediaasset_delete?mode=memory&_fk=1") - defer client.Close() - - root := filepath.Join(t.TempDir(), "media") - const relativePath = "images/2026/03/31/deleted.png" - sourceBytes := mustEncodePNG(t, 2, 2) - assetID := sha256Hex(sourceBytes) - writeBlob(t, root, relativePath, sourceBytes) - sidecarPath := filepath.Join(root, "images", "2026", "03", "31", "deleted.json") - if err := os.WriteFile(sidecarPath, []byte(`{"keep":true}`), 0644); err != nil { - t.Fatalf("write sidecar: %v", err) - } - - _, err := client.MediaAsset.Create(). - SetAssetID(assetID). - SetOriginalFilename("deleted.png"). - SetRelativePath(relativePath). - SetMimeType("image/png"). - SetSize(int64(len(sourceBytes))). - SetWidth(2). - SetHeight(2). - SetCreatedAt("2026-03-31T10:00:00Z"). - SetUpdatedAt("2026-03-31T10:00:00Z"). - Save(context.Background()) - if err != nil { - t.Fatalf("create media asset: %v", err) - } - - record, err := snapshot.NewRecord("media_assets", assetID, map[string]interface{}{ - mediaasset.FieldCreatedAt: "2026-03-31T10:00:00Z", - mediaasset.FieldUpdatedAt: "2026-03-31T10:05:00Z", - mediaasset.FieldDeletedAt: "2026-03-31T10:06:00Z", - mediaasset.FieldAssetID: assetID, - mediaasset.FieldOriginalFilename: "deleted.png", - mediaasset.FieldRelativePath: relativePath, - mediaasset.FieldMimeType: "image/png", - mediaasset.FieldSize: int64(len(sourceBytes)), - mediaasset.FieldWidth: 2, - mediaasset.FieldHeight: 2, - }, nil) - if err != nil { - t.Fatalf("build delete record: %v", err) - } - - adapter := NewMediaAssetAdapter(client, func() string { return root }) - if err := adapter.Apply(context.Background(), []snapshot.Record{record}); err != nil { - t.Fatalf("apply delete record: %v", err) - } - - reloaded, err := client.MediaAsset.Query(). - Where(mediaasset.AssetIDEQ(assetID)). - Only(schemamixin.SkipSoftDelete(context.Background())) - if err != nil { - t.Fatalf("reload deleted media asset: %v", err) - } - if reloaded.DeletedAt == nil { - t.Fatal("expected deleted_at to be set after applying delete record") - } - if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(relativePath))); !os.IsNotExist(err) { - t.Fatalf("expected blob to be removed, stat err=%v", err) - } - if _, err := os.Stat(sidecarPath); err != nil { - t.Fatalf("expected sidecar to remain untouched, stat err=%v", err) - } -} - -func TestMediaAssetAdapterApplyRejectsUnsafeRelativePath(t *testing.T) { - client := enttest.Open(t, "sqlite3", "file:mediaasset_unsafe_path?mode=memory&_fk=1") - defer client.Close() - - root := filepath.Join(t.TempDir(), "media") - sourceBytes := mustEncodePNG(t, 2, 2) - assetID := sha256Hex(sourceBytes) - record, err := snapshot.NewRecord("media_assets", assetID, map[string]interface{}{ - mediaasset.FieldCreatedAt: "2026-03-31T10:00:00Z", - mediaasset.FieldUpdatedAt: "2026-03-31T10:05:00Z", - mediaasset.FieldDeletedAt: "", - mediaasset.FieldAssetID: assetID, - mediaasset.FieldOriginalFilename: "escape.png", - mediaasset.FieldRelativePath: "images/../../escape.png", - mediaasset.FieldMimeType: "image/png", - mediaasset.FieldSize: int64(len(sourceBytes)), - mediaasset.FieldWidth: 2, - mediaasset.FieldHeight: 2, - }, map[string][]byte{ - mediaAssetBlobName: sourceBytes, - }) - if err != nil { - t.Fatalf("build unsafe record: %v", err) - } - - adapter := NewMediaAssetAdapter(client, func() string { return root }) - if err := adapter.Apply(context.Background(), []snapshot.Record{record}); err == nil { - t.Fatal("expected apply to reject unsafe relative path") - } - if _, err := os.Stat(filepath.Join(filepath.Dir(root), "escape.png")); !os.IsNotExist(err) { - t.Fatalf("expected no file to be written outside media root, stat err=%v", err) - } -} - -func TestMediaAssetAdapterApplyRejectsBlobDigestMismatch(t *testing.T) { - client := enttest.Open(t, "sqlite3", "file:mediaasset_digest_mismatch?mode=memory&_fk=1") - defer client.Close() - - root := filepath.Join(t.TempDir(), "media") - sourceBytes := mustEncodePNG(t, 2, 2) - record, err := snapshot.NewRecord("media_assets", "deadbeef", map[string]interface{}{ - mediaasset.FieldCreatedAt: "2026-03-31T10:00:00Z", - mediaasset.FieldUpdatedAt: "2026-03-31T10:05:00Z", - mediaasset.FieldDeletedAt: "", - mediaasset.FieldAssetID: "deadbeef", - mediaasset.FieldOriginalFilename: "mismatch.png", - mediaasset.FieldRelativePath: "images/2026/03/31/mismatch.png", - mediaasset.FieldMimeType: "image/png", - mediaasset.FieldSize: int64(len(sourceBytes)), - mediaasset.FieldWidth: 2, - mediaasset.FieldHeight: 2, - }, map[string][]byte{ - mediaAssetBlobName: sourceBytes, - }) - if err != nil { - t.Fatalf("build mismatch record: %v", err) - } - - adapter := NewMediaAssetAdapter(client, func() string { return root }) - if err := adapter.Apply(context.Background(), []snapshot.Record{record}); err == nil { - t.Fatal("expected apply to reject blob digest mismatch") - } - if _, err := client.MediaAsset.Query().Where(mediaasset.RelativePathEQ("images/2026/03/31/mismatch.png")).Only(schemamixin.SkipSoftDelete(context.Background())); !ent.IsNotFound(err) { - t.Fatalf("expected no index row to be created, err=%v", err) - } -} - -func TestMediaAssetAdapterApplyCreateRollsBackFileWhenInsertFails(t *testing.T) { - client := enttest.Open(t, "sqlite3", "file:mediaasset_create_rollback?mode=memory&_fk=1") - defer client.Close() - - root := filepath.Join(t.TempDir(), "media") - sourceBytes := mustEncodePNG(t, 2, 2) - assetID := sha256Hex(sourceBytes) - record, err := snapshot.NewRecord("media_assets", assetID, map[string]interface{}{ - mediaasset.FieldCreatedAt: "2026-03-31T10:00:00Z", - mediaasset.FieldUpdatedAt: "2026-03-31T10:05:00Z", - mediaasset.FieldDeletedAt: "", - mediaasset.FieldAssetID: assetID, - mediaasset.FieldOriginalFilename: "create-rollback.png", - mediaasset.FieldRelativePath: "images/2026/03/31/create-rollback.png", - mediaasset.FieldMimeType: "image/png", - mediaasset.FieldSize: int64(len(sourceBytes)), - mediaasset.FieldWidth: 2, - mediaasset.FieldHeight: 2, - }, map[string][]byte{ - mediaAssetBlobName: sourceBytes, - }) - if err != nil { - t.Fatalf("build create record: %v", err) - } - - failMediaAssetMutations(client, ent.OpCreate, fmt.Errorf("forced create failure")) - - adapter := NewMediaAssetAdapter(client, func() string { return root }) - if err := adapter.Apply(context.Background(), []snapshot.Record{record}); err == nil { - t.Fatal("expected create apply to fail") - } - - absPath := filepath.Join(root, "images", "2026", "03", "31", "create-rollback.png") - if _, err := os.Stat(absPath); !os.IsNotExist(err) { - t.Fatalf("expected created blob to be rolled back, stat err=%v", err) - } - _, err = client.MediaAsset.Query(). - Where(mediaasset.AssetIDEQ(assetID)). - Only(schemamixin.SkipSoftDelete(context.Background())) - if !ent.IsNotFound(err) { - t.Fatalf("expected index row creation to be rolled back, err=%v", err) - } -} - -func TestMediaAssetAdapterApplyUpdateRollsBackStagedPathWhenUpdateFails(t *testing.T) { - client := enttest.Open(t, "sqlite3", "file:mediaasset_update_rollback?mode=memory&_fk=1") - defer client.Close() - - root := filepath.Join(t.TempDir(), "media") - sourceBytes := mustEncodePNG(t, 2, 2) - assetID := sha256Hex(sourceBytes) - const oldRelativePath = "images/2026/03/31/original.png" - const nextRelativePath = "images/2026/03/31/updated.png" - writeBlob(t, root, oldRelativePath, sourceBytes) - - _, err := client.MediaAsset.Create(). - SetAssetID(assetID). - SetOriginalFilename("original.png"). - SetRelativePath(oldRelativePath). - SetMimeType("image/png"). - SetSize(int64(len(sourceBytes))). - SetWidth(2). - SetHeight(2). - SetCreatedAt("2026-03-31T10:00:00Z"). - SetUpdatedAt("2026-03-31T10:00:00Z"). - Save(context.Background()) - if err != nil { - t.Fatalf("create original media asset: %v", err) - } - - record, err := snapshot.NewRecord("media_assets", assetID, map[string]interface{}{ - mediaasset.FieldCreatedAt: "2026-03-31T10:00:00Z", - mediaasset.FieldUpdatedAt: "2026-03-31T10:05:00Z", - mediaasset.FieldDeletedAt: "", - mediaasset.FieldAssetID: assetID, - mediaasset.FieldOriginalFilename: "updated.png", - mediaasset.FieldRelativePath: nextRelativePath, - mediaasset.FieldMimeType: "image/png", - mediaasset.FieldSize: int64(len(sourceBytes)), - mediaasset.FieldWidth: 2, - mediaasset.FieldHeight: 2, - }, map[string][]byte{ - mediaAssetBlobName: sourceBytes, - }) - if err != nil { - t.Fatalf("build update record: %v", err) - } - - failMediaAssetMutations(client, ent.OpUpdate|ent.OpUpdateOne, fmt.Errorf("forced update failure")) - - adapter := NewMediaAssetAdapter(client, func() string { return root }) - if err := adapter.Apply(context.Background(), []snapshot.Record{record}); err == nil { - t.Fatal("expected update apply to fail") - } - - oldPath := filepath.Join(root, filepath.FromSlash(oldRelativePath)) - oldBytes, err := os.ReadFile(oldPath) - if err != nil { - t.Fatalf("expected original blob to remain: %v", err) - } - if !bytes.Equal(oldBytes, sourceBytes) { - t.Fatal("expected original blob bytes to remain unchanged") - } - - nextPath := filepath.Join(root, filepath.FromSlash(nextRelativePath)) - if _, err := os.Stat(nextPath); !os.IsNotExist(err) { - t.Fatalf("expected staged target blob to be rolled back, stat err=%v", err) - } - - reloaded, err := client.MediaAsset.Query(). - Where(mediaasset.AssetIDEQ(assetID)). - Only(schemamixin.SkipSoftDelete(context.Background())) - if err != nil { - t.Fatalf("reload asset after failed update: %v", err) - } - if reloaded.RelativePath != oldRelativePath { - t.Fatalf("expected relative path to remain %q, got %q", oldRelativePath, reloaded.RelativePath) - } -} - -func mustEncodePNG(t *testing.T, width int, height int) []byte { - t.Helper() - - img := image.NewRGBA(image.Rect(0, 0, width, height)) - for y := 0; y < height; y++ { - for x := 0; x < width; x++ { - img.Set(x, y, color.RGBA{R: 90, G: 140, B: 220, A: 255}) - } - } - - var buffer bytes.Buffer - if err := png.Encode(&buffer, img); err != nil { - t.Fatalf("encode png: %v", err) - } - return buffer.Bytes() -} - -func sha256Hex(data []byte) string { - sum := sha256.Sum256(data) - return hex.EncodeToString(sum[:]) -} - -func writeBlob(t *testing.T, root string, relativePath string, data []byte) { - t.Helper() - - absPath := filepath.Join(root, filepath.FromSlash(relativePath)) - if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { - t.Fatalf("mkdir blob dir: %v", err) - } - if err := os.WriteFile(absPath, data, 0644); err != nil { - t.Fatalf("write blob: %v", err) - } -} diff --git a/internal/models/ent/client.go b/internal/models/ent/client.go index 9bb701f9..4367d90f 100644 --- a/internal/models/ent/client.go +++ b/internal/models/ent/client.go @@ -760,8 +760,7 @@ func (c *MediaAssetClient) Hooks() []Hook { // Interceptors returns the client interceptors. func (c *MediaAssetClient) Interceptors() []Interceptor { - inters := c.inters.MediaAsset - return append(inters[:len(inters):len(inters)], mediaasset.Interceptors[:]...) + return c.inters.MediaAsset } func (c *MediaAssetClient) mutate(ctx context.Context, m *MediaAssetMutation) (Value, error) { diff --git a/internal/models/ent/entql.go b/internal/models/ent/entql.go index 245d0767..dac075e0 100644 --- a/internal/models/ent/entql.go +++ b/internal/models/ent/entql.go @@ -96,17 +96,16 @@ var schemaGraph = func() *sqlgraph.Schema { }, Type: "MediaAsset", Fields: map[string]*sqlgraph.FieldSpec{ - mediaasset.FieldCreatedAt: {Type: field.TypeString, Column: mediaasset.FieldCreatedAt}, - mediaasset.FieldUpdatedAt: {Type: field.TypeString, Column: mediaasset.FieldUpdatedAt}, - mediaasset.FieldDeletedAt: {Type: field.TypeString, Column: mediaasset.FieldDeletedAt}, - mediaasset.FieldUUID: {Type: field.TypeString, Column: mediaasset.FieldUUID}, - mediaasset.FieldAssetID: {Type: field.TypeString, Column: mediaasset.FieldAssetID}, - mediaasset.FieldOriginalFilename: {Type: field.TypeString, Column: mediaasset.FieldOriginalFilename}, - mediaasset.FieldRelativePath: {Type: field.TypeString, Column: mediaasset.FieldRelativePath}, - mediaasset.FieldMimeType: {Type: field.TypeString, Column: mediaasset.FieldMimeType}, - mediaasset.FieldSize: {Type: field.TypeInt64, Column: mediaasset.FieldSize}, - mediaasset.FieldWidth: {Type: field.TypeInt, Column: mediaasset.FieldWidth}, - mediaasset.FieldHeight: {Type: field.TypeInt, Column: mediaasset.FieldHeight}, + mediaasset.FieldCreatedAt: {Type: field.TypeString, Column: mediaasset.FieldCreatedAt}, + mediaasset.FieldUpdatedAt: {Type: field.TypeString, Column: mediaasset.FieldUpdatedAt}, + mediaasset.FieldUUID: {Type: field.TypeString, Column: mediaasset.FieldUUID}, + mediaasset.FieldAssetID: {Type: field.TypeString, Column: mediaasset.FieldAssetID}, + mediaasset.FieldFilename: {Type: field.TypeString, Column: mediaasset.FieldFilename}, + mediaasset.FieldPath: {Type: field.TypeString, Column: mediaasset.FieldPath}, + mediaasset.FieldMimeType: {Type: field.TypeString, Column: mediaasset.FieldMimeType}, + mediaasset.FieldSize: {Type: field.TypeInt64, Column: mediaasset.FieldSize}, + mediaasset.FieldWidth: {Type: field.TypeInt, Column: mediaasset.FieldWidth}, + mediaasset.FieldHeight: {Type: field.TypeInt, Column: mediaasset.FieldHeight}, }, } graph.Nodes[4] = &sqlgraph.Node{ @@ -448,11 +447,6 @@ func (f *MediaAssetFilter) WhereUpdatedAt(p entql.StringP) { f.Where(p.Field(mediaasset.FieldUpdatedAt)) } -// WhereDeletedAt applies the entql string predicate on the deleted_at field. -func (f *MediaAssetFilter) WhereDeletedAt(p entql.StringP) { - f.Where(p.Field(mediaasset.FieldDeletedAt)) -} - // WhereUUID applies the entql string predicate on the uuid field. func (f *MediaAssetFilter) WhereUUID(p entql.StringP) { f.Where(p.Field(mediaasset.FieldUUID)) @@ -463,14 +457,14 @@ func (f *MediaAssetFilter) WhereAssetID(p entql.StringP) { f.Where(p.Field(mediaasset.FieldAssetID)) } -// WhereOriginalFilename applies the entql string predicate on the original_filename field. -func (f *MediaAssetFilter) WhereOriginalFilename(p entql.StringP) { - f.Where(p.Field(mediaasset.FieldOriginalFilename)) +// WhereFilename applies the entql string predicate on the filename field. +func (f *MediaAssetFilter) WhereFilename(p entql.StringP) { + f.Where(p.Field(mediaasset.FieldFilename)) } -// WhereRelativePath applies the entql string predicate on the relative_path field. -func (f *MediaAssetFilter) WhereRelativePath(p entql.StringP) { - f.Where(p.Field(mediaasset.FieldRelativePath)) +// WherePath applies the entql string predicate on the path field. +func (f *MediaAssetFilter) WherePath(p entql.StringP) { + f.Where(p.Field(mediaasset.FieldPath)) } // WhereMimeType applies the entql string predicate on the mime_type field. diff --git a/internal/models/ent/mediaasset.go b/internal/models/ent/mediaasset.go index 66c5b9e4..4a70c42f 100644 --- a/internal/models/ent/mediaasset.go +++ b/internal/models/ent/mediaasset.go @@ -20,16 +20,14 @@ type MediaAsset struct { CreatedAt string `json:"created_at"` // update time UpdatedAt string `json:"updated_at"` - // deleted at - DeletedAt *string `json:"deleted_at,omitempty"` // UUID for cross-device sync (UUIDv7) UUID string `json:"uuid"` // stable media asset id derived from content sha256 AssetID string `json:"asset_id"` - // original imported filename - OriginalFilename *string `json:"original_filename,omitempty"` + // imported filename + Filename *string `json:"filename,omitempty"` // media path relative to media root - RelativePath string `json:"relative_path"` + Path string `json:"path"` // image mime type MimeType string `json:"mime_type"` // image byte size @@ -48,7 +46,7 @@ func (*MediaAsset) scanValues(columns []string) ([]any, error) { switch columns[i] { case mediaasset.FieldID, mediaasset.FieldSize, mediaasset.FieldWidth, mediaasset.FieldHeight: values[i] = new(sql.NullInt64) - case mediaasset.FieldCreatedAt, mediaasset.FieldUpdatedAt, mediaasset.FieldDeletedAt, mediaasset.FieldUUID, mediaasset.FieldAssetID, mediaasset.FieldOriginalFilename, mediaasset.FieldRelativePath, mediaasset.FieldMimeType: + case mediaasset.FieldCreatedAt, mediaasset.FieldUpdatedAt, mediaasset.FieldUUID, mediaasset.FieldAssetID, mediaasset.FieldFilename, mediaasset.FieldPath, mediaasset.FieldMimeType: values[i] = new(sql.NullString) default: values[i] = new(sql.UnknownType) @@ -83,13 +81,6 @@ func (_m *MediaAsset) assignValues(columns []string, values []any) error { } else if value.Valid { _m.UpdatedAt = value.String } - case mediaasset.FieldDeletedAt: - if value, ok := values[i].(*sql.NullString); !ok { - return fmt.Errorf("unexpected type %T for field deleted_at", values[i]) - } else if value.Valid { - _m.DeletedAt = new(string) - *_m.DeletedAt = value.String - } case mediaasset.FieldUUID: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field uuid", values[i]) @@ -102,18 +93,18 @@ func (_m *MediaAsset) assignValues(columns []string, values []any) error { } else if value.Valid { _m.AssetID = value.String } - case mediaasset.FieldOriginalFilename: + case mediaasset.FieldFilename: if value, ok := values[i].(*sql.NullString); !ok { - return fmt.Errorf("unexpected type %T for field original_filename", values[i]) + return fmt.Errorf("unexpected type %T for field filename", values[i]) } else if value.Valid { - _m.OriginalFilename = new(string) - *_m.OriginalFilename = value.String + _m.Filename = new(string) + *_m.Filename = value.String } - case mediaasset.FieldRelativePath: + case mediaasset.FieldPath: if value, ok := values[i].(*sql.NullString); !ok { - return fmt.Errorf("unexpected type %T for field relative_path", values[i]) + return fmt.Errorf("unexpected type %T for field path", values[i]) } else if value.Valid { - _m.RelativePath = value.String + _m.Path = value.String } case mediaasset.FieldMimeType: if value, ok := values[i].(*sql.NullString); !ok { @@ -181,24 +172,19 @@ func (_m *MediaAsset) String() string { builder.WriteString("updated_at=") builder.WriteString(_m.UpdatedAt) builder.WriteString(", ") - if v := _m.DeletedAt; v != nil { - builder.WriteString("deleted_at=") - builder.WriteString(*v) - } - builder.WriteString(", ") builder.WriteString("uuid=") builder.WriteString(_m.UUID) builder.WriteString(", ") builder.WriteString("asset_id=") builder.WriteString(_m.AssetID) builder.WriteString(", ") - if v := _m.OriginalFilename; v != nil { - builder.WriteString("original_filename=") + if v := _m.Filename; v != nil { + builder.WriteString("filename=") builder.WriteString(*v) } builder.WriteString(", ") - builder.WriteString("relative_path=") - builder.WriteString(_m.RelativePath) + builder.WriteString("path=") + builder.WriteString(_m.Path) builder.WriteString(", ") builder.WriteString("mime_type=") builder.WriteString(_m.MimeType) diff --git a/internal/models/ent/mediaasset/mediaasset.go b/internal/models/ent/mediaasset/mediaasset.go index d041257a..eb0ade8d 100644 --- a/internal/models/ent/mediaasset/mediaasset.go +++ b/internal/models/ent/mediaasset/mediaasset.go @@ -16,16 +16,14 @@ const ( FieldCreatedAt = "created_at" // FieldUpdatedAt holds the string denoting the updated_at field in the database. FieldUpdatedAt = "updated_at" - // FieldDeletedAt holds the string denoting the deleted_at field in the database. - FieldDeletedAt = "deleted_at" // FieldUUID holds the string denoting the uuid field in the database. FieldUUID = "uuid" // FieldAssetID holds the string denoting the asset_id field in the database. FieldAssetID = "asset_id" - // FieldOriginalFilename holds the string denoting the original_filename field in the database. - FieldOriginalFilename = "original_filename" - // FieldRelativePath holds the string denoting the relative_path field in the database. - FieldRelativePath = "relative_path" + // FieldFilename holds the string denoting the filename field in the database. + FieldFilename = "filename" + // FieldPath holds the string denoting the path field in the database. + FieldPath = "path" // FieldMimeType holds the string denoting the mime_type field in the database. FieldMimeType = "mime_type" // FieldSize holds the string denoting the size field in the database. @@ -43,11 +41,10 @@ var Columns = []string{ FieldID, FieldCreatedAt, FieldUpdatedAt, - FieldDeletedAt, FieldUUID, FieldAssetID, - FieldOriginalFilename, - FieldRelativePath, + FieldFilename, + FieldPath, FieldMimeType, FieldSize, FieldWidth, @@ -70,8 +67,7 @@ func ValidColumn(column string) bool { // // import _ "voidraft/internal/models/ent/runtime" var ( - Hooks [2]ent.Hook - Interceptors [1]ent.Interceptor + Hooks [1]ent.Hook // DefaultCreatedAt holds the default value on creation for the "created_at" field. DefaultCreatedAt func() string // DefaultUpdatedAt holds the default value on creation for the "updated_at" field. @@ -80,10 +76,10 @@ var ( DefaultUUID func() string // AssetIDValidator is a validator for the "asset_id" field. It is called by the builders before save. AssetIDValidator func(string) error - // OriginalFilenameValidator is a validator for the "original_filename" field. It is called by the builders before save. - OriginalFilenameValidator func(string) error - // RelativePathValidator is a validator for the "relative_path" field. It is called by the builders before save. - RelativePathValidator func(string) error + // FilenameValidator is a validator for the "filename" field. It is called by the builders before save. + FilenameValidator func(string) error + // PathValidator is a validator for the "path" field. It is called by the builders before save. + PathValidator func(string) error // MimeTypeValidator is a validator for the "mime_type" field. It is called by the builders before save. MimeTypeValidator func(string) error // SizeValidator is a validator for the "size" field. It is called by the builders before save. @@ -112,11 +108,6 @@ func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc() } -// ByDeletedAt orders the results by the deleted_at field. -func ByDeletedAt(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldDeletedAt, opts...).ToFunc() -} - // ByUUID orders the results by the uuid field. func ByUUID(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldUUID, opts...).ToFunc() @@ -127,14 +118,14 @@ func ByAssetID(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldAssetID, opts...).ToFunc() } -// ByOriginalFilename orders the results by the original_filename field. -func ByOriginalFilename(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldOriginalFilename, opts...).ToFunc() +// ByFilename orders the results by the filename field. +func ByFilename(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldFilename, opts...).ToFunc() } -// ByRelativePath orders the results by the relative_path field. -func ByRelativePath(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldRelativePath, opts...).ToFunc() +// ByPath orders the results by the path field. +func ByPath(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldPath, opts...).ToFunc() } // ByMimeType orders the results by the mime_type field. diff --git a/internal/models/ent/mediaasset/where.go b/internal/models/ent/mediaasset/where.go index eea886dd..5ad81d21 100644 --- a/internal/models/ent/mediaasset/where.go +++ b/internal/models/ent/mediaasset/where.go @@ -63,11 +63,6 @@ func UpdatedAt(v string) predicate.MediaAsset { return predicate.MediaAsset(sql.FieldEQ(FieldUpdatedAt, v)) } -// DeletedAt applies equality check predicate on the "deleted_at" field. It's identical to DeletedAtEQ. -func DeletedAt(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldEQ(FieldDeletedAt, v)) -} - // UUID applies equality check predicate on the "uuid" field. It's identical to UUIDEQ. func UUID(v string) predicate.MediaAsset { return predicate.MediaAsset(sql.FieldEQ(FieldUUID, v)) @@ -78,14 +73,14 @@ func AssetID(v string) predicate.MediaAsset { return predicate.MediaAsset(sql.FieldEQ(FieldAssetID, v)) } -// OriginalFilename applies equality check predicate on the "original_filename" field. It's identical to OriginalFilenameEQ. -func OriginalFilename(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldEQ(FieldOriginalFilename, v)) +// Filename applies equality check predicate on the "filename" field. It's identical to FilenameEQ. +func Filename(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldEQ(FieldFilename, v)) } -// RelativePath applies equality check predicate on the "relative_path" field. It's identical to RelativePathEQ. -func RelativePath(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldEQ(FieldRelativePath, v)) +// Path applies equality check predicate on the "path" field. It's identical to PathEQ. +func Path(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldEQ(FieldPath, v)) } // MimeType applies equality check predicate on the "mime_type" field. It's identical to MimeTypeEQ. @@ -238,81 +233,6 @@ func UpdatedAtContainsFold(v string) predicate.MediaAsset { return predicate.MediaAsset(sql.FieldContainsFold(FieldUpdatedAt, v)) } -// DeletedAtEQ applies the EQ predicate on the "deleted_at" field. -func DeletedAtEQ(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldEQ(FieldDeletedAt, v)) -} - -// DeletedAtNEQ applies the NEQ predicate on the "deleted_at" field. -func DeletedAtNEQ(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldNEQ(FieldDeletedAt, v)) -} - -// DeletedAtIn applies the In predicate on the "deleted_at" field. -func DeletedAtIn(vs ...string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldIn(FieldDeletedAt, vs...)) -} - -// DeletedAtNotIn applies the NotIn predicate on the "deleted_at" field. -func DeletedAtNotIn(vs ...string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldNotIn(FieldDeletedAt, vs...)) -} - -// DeletedAtGT applies the GT predicate on the "deleted_at" field. -func DeletedAtGT(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldGT(FieldDeletedAt, v)) -} - -// DeletedAtGTE applies the GTE predicate on the "deleted_at" field. -func DeletedAtGTE(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldGTE(FieldDeletedAt, v)) -} - -// DeletedAtLT applies the LT predicate on the "deleted_at" field. -func DeletedAtLT(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldLT(FieldDeletedAt, v)) -} - -// DeletedAtLTE applies the LTE predicate on the "deleted_at" field. -func DeletedAtLTE(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldLTE(FieldDeletedAt, v)) -} - -// DeletedAtContains applies the Contains predicate on the "deleted_at" field. -func DeletedAtContains(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldContains(FieldDeletedAt, v)) -} - -// DeletedAtHasPrefix applies the HasPrefix predicate on the "deleted_at" field. -func DeletedAtHasPrefix(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldHasPrefix(FieldDeletedAt, v)) -} - -// DeletedAtHasSuffix applies the HasSuffix predicate on the "deleted_at" field. -func DeletedAtHasSuffix(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldHasSuffix(FieldDeletedAt, v)) -} - -// DeletedAtIsNil applies the IsNil predicate on the "deleted_at" field. -func DeletedAtIsNil() predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldIsNull(FieldDeletedAt)) -} - -// DeletedAtNotNil applies the NotNil predicate on the "deleted_at" field. -func DeletedAtNotNil() predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldNotNull(FieldDeletedAt)) -} - -// DeletedAtEqualFold applies the EqualFold predicate on the "deleted_at" field. -func DeletedAtEqualFold(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldEqualFold(FieldDeletedAt, v)) -} - -// DeletedAtContainsFold applies the ContainsFold predicate on the "deleted_at" field. -func DeletedAtContainsFold(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldContainsFold(FieldDeletedAt, v)) -} - // UUIDEQ applies the EQ predicate on the "uuid" field. func UUIDEQ(v string) predicate.MediaAsset { return predicate.MediaAsset(sql.FieldEQ(FieldUUID, v)) @@ -443,144 +363,144 @@ func AssetIDContainsFold(v string) predicate.MediaAsset { return predicate.MediaAsset(sql.FieldContainsFold(FieldAssetID, v)) } -// OriginalFilenameEQ applies the EQ predicate on the "original_filename" field. -func OriginalFilenameEQ(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldEQ(FieldOriginalFilename, v)) +// FilenameEQ applies the EQ predicate on the "filename" field. +func FilenameEQ(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldEQ(FieldFilename, v)) } -// OriginalFilenameNEQ applies the NEQ predicate on the "original_filename" field. -func OriginalFilenameNEQ(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldNEQ(FieldOriginalFilename, v)) +// FilenameNEQ applies the NEQ predicate on the "filename" field. +func FilenameNEQ(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldNEQ(FieldFilename, v)) } -// OriginalFilenameIn applies the In predicate on the "original_filename" field. -func OriginalFilenameIn(vs ...string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldIn(FieldOriginalFilename, vs...)) +// FilenameIn applies the In predicate on the "filename" field. +func FilenameIn(vs ...string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldIn(FieldFilename, vs...)) } -// OriginalFilenameNotIn applies the NotIn predicate on the "original_filename" field. -func OriginalFilenameNotIn(vs ...string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldNotIn(FieldOriginalFilename, vs...)) +// FilenameNotIn applies the NotIn predicate on the "filename" field. +func FilenameNotIn(vs ...string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldNotIn(FieldFilename, vs...)) } -// OriginalFilenameGT applies the GT predicate on the "original_filename" field. -func OriginalFilenameGT(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldGT(FieldOriginalFilename, v)) +// FilenameGT applies the GT predicate on the "filename" field. +func FilenameGT(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldGT(FieldFilename, v)) } -// OriginalFilenameGTE applies the GTE predicate on the "original_filename" field. -func OriginalFilenameGTE(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldGTE(FieldOriginalFilename, v)) +// FilenameGTE applies the GTE predicate on the "filename" field. +func FilenameGTE(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldGTE(FieldFilename, v)) } -// OriginalFilenameLT applies the LT predicate on the "original_filename" field. -func OriginalFilenameLT(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldLT(FieldOriginalFilename, v)) +// FilenameLT applies the LT predicate on the "filename" field. +func FilenameLT(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldLT(FieldFilename, v)) } -// OriginalFilenameLTE applies the LTE predicate on the "original_filename" field. -func OriginalFilenameLTE(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldLTE(FieldOriginalFilename, v)) +// FilenameLTE applies the LTE predicate on the "filename" field. +func FilenameLTE(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldLTE(FieldFilename, v)) } -// OriginalFilenameContains applies the Contains predicate on the "original_filename" field. -func OriginalFilenameContains(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldContains(FieldOriginalFilename, v)) +// FilenameContains applies the Contains predicate on the "filename" field. +func FilenameContains(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldContains(FieldFilename, v)) } -// OriginalFilenameHasPrefix applies the HasPrefix predicate on the "original_filename" field. -func OriginalFilenameHasPrefix(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldHasPrefix(FieldOriginalFilename, v)) +// FilenameHasPrefix applies the HasPrefix predicate on the "filename" field. +func FilenameHasPrefix(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldHasPrefix(FieldFilename, v)) } -// OriginalFilenameHasSuffix applies the HasSuffix predicate on the "original_filename" field. -func OriginalFilenameHasSuffix(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldHasSuffix(FieldOriginalFilename, v)) +// FilenameHasSuffix applies the HasSuffix predicate on the "filename" field. +func FilenameHasSuffix(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldHasSuffix(FieldFilename, v)) } -// OriginalFilenameIsNil applies the IsNil predicate on the "original_filename" field. -func OriginalFilenameIsNil() predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldIsNull(FieldOriginalFilename)) +// FilenameIsNil applies the IsNil predicate on the "filename" field. +func FilenameIsNil() predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldIsNull(FieldFilename)) } -// OriginalFilenameNotNil applies the NotNil predicate on the "original_filename" field. -func OriginalFilenameNotNil() predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldNotNull(FieldOriginalFilename)) +// FilenameNotNil applies the NotNil predicate on the "filename" field. +func FilenameNotNil() predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldNotNull(FieldFilename)) } -// OriginalFilenameEqualFold applies the EqualFold predicate on the "original_filename" field. -func OriginalFilenameEqualFold(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldEqualFold(FieldOriginalFilename, v)) +// FilenameEqualFold applies the EqualFold predicate on the "filename" field. +func FilenameEqualFold(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldEqualFold(FieldFilename, v)) } -// OriginalFilenameContainsFold applies the ContainsFold predicate on the "original_filename" field. -func OriginalFilenameContainsFold(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldContainsFold(FieldOriginalFilename, v)) +// FilenameContainsFold applies the ContainsFold predicate on the "filename" field. +func FilenameContainsFold(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldContainsFold(FieldFilename, v)) } -// RelativePathEQ applies the EQ predicate on the "relative_path" field. -func RelativePathEQ(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldEQ(FieldRelativePath, v)) +// PathEQ applies the EQ predicate on the "path" field. +func PathEQ(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldEQ(FieldPath, v)) } -// RelativePathNEQ applies the NEQ predicate on the "relative_path" field. -func RelativePathNEQ(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldNEQ(FieldRelativePath, v)) +// PathNEQ applies the NEQ predicate on the "path" field. +func PathNEQ(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldNEQ(FieldPath, v)) } -// RelativePathIn applies the In predicate on the "relative_path" field. -func RelativePathIn(vs ...string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldIn(FieldRelativePath, vs...)) +// PathIn applies the In predicate on the "path" field. +func PathIn(vs ...string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldIn(FieldPath, vs...)) } -// RelativePathNotIn applies the NotIn predicate on the "relative_path" field. -func RelativePathNotIn(vs ...string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldNotIn(FieldRelativePath, vs...)) +// PathNotIn applies the NotIn predicate on the "path" field. +func PathNotIn(vs ...string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldNotIn(FieldPath, vs...)) } -// RelativePathGT applies the GT predicate on the "relative_path" field. -func RelativePathGT(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldGT(FieldRelativePath, v)) +// PathGT applies the GT predicate on the "path" field. +func PathGT(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldGT(FieldPath, v)) } -// RelativePathGTE applies the GTE predicate on the "relative_path" field. -func RelativePathGTE(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldGTE(FieldRelativePath, v)) +// PathGTE applies the GTE predicate on the "path" field. +func PathGTE(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldGTE(FieldPath, v)) } -// RelativePathLT applies the LT predicate on the "relative_path" field. -func RelativePathLT(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldLT(FieldRelativePath, v)) +// PathLT applies the LT predicate on the "path" field. +func PathLT(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldLT(FieldPath, v)) } -// RelativePathLTE applies the LTE predicate on the "relative_path" field. -func RelativePathLTE(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldLTE(FieldRelativePath, v)) +// PathLTE applies the LTE predicate on the "path" field. +func PathLTE(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldLTE(FieldPath, v)) } -// RelativePathContains applies the Contains predicate on the "relative_path" field. -func RelativePathContains(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldContains(FieldRelativePath, v)) +// PathContains applies the Contains predicate on the "path" field. +func PathContains(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldContains(FieldPath, v)) } -// RelativePathHasPrefix applies the HasPrefix predicate on the "relative_path" field. -func RelativePathHasPrefix(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldHasPrefix(FieldRelativePath, v)) +// PathHasPrefix applies the HasPrefix predicate on the "path" field. +func PathHasPrefix(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldHasPrefix(FieldPath, v)) } -// RelativePathHasSuffix applies the HasSuffix predicate on the "relative_path" field. -func RelativePathHasSuffix(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldHasSuffix(FieldRelativePath, v)) +// PathHasSuffix applies the HasSuffix predicate on the "path" field. +func PathHasSuffix(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldHasSuffix(FieldPath, v)) } -// RelativePathEqualFold applies the EqualFold predicate on the "relative_path" field. -func RelativePathEqualFold(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldEqualFold(FieldRelativePath, v)) +// PathEqualFold applies the EqualFold predicate on the "path" field. +func PathEqualFold(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldEqualFold(FieldPath, v)) } -// RelativePathContainsFold applies the ContainsFold predicate on the "relative_path" field. -func RelativePathContainsFold(v string) predicate.MediaAsset { - return predicate.MediaAsset(sql.FieldContainsFold(FieldRelativePath, v)) +// PathContainsFold applies the ContainsFold predicate on the "path" field. +func PathContainsFold(v string) predicate.MediaAsset { + return predicate.MediaAsset(sql.FieldContainsFold(FieldPath, v)) } // MimeTypeEQ applies the EQ predicate on the "mime_type" field. diff --git a/internal/models/ent/mediaasset_create.go b/internal/models/ent/mediaasset_create.go index 1f1a18bf..0fd5b22b 100644 --- a/internal/models/ent/mediaasset_create.go +++ b/internal/models/ent/mediaasset_create.go @@ -47,20 +47,6 @@ func (_c *MediaAssetCreate) SetNillableUpdatedAt(v *string) *MediaAssetCreate { return _c } -// SetDeletedAt sets the "deleted_at" field. -func (_c *MediaAssetCreate) SetDeletedAt(v string) *MediaAssetCreate { - _c.mutation.SetDeletedAt(v) - return _c -} - -// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil. -func (_c *MediaAssetCreate) SetNillableDeletedAt(v *string) *MediaAssetCreate { - if v != nil { - _c.SetDeletedAt(*v) - } - return _c -} - // SetUUID sets the "uuid" field. func (_c *MediaAssetCreate) SetUUID(v string) *MediaAssetCreate { _c.mutation.SetUUID(v) @@ -81,23 +67,23 @@ func (_c *MediaAssetCreate) SetAssetID(v string) *MediaAssetCreate { return _c } -// SetOriginalFilename sets the "original_filename" field. -func (_c *MediaAssetCreate) SetOriginalFilename(v string) *MediaAssetCreate { - _c.mutation.SetOriginalFilename(v) +// SetFilename sets the "filename" field. +func (_c *MediaAssetCreate) SetFilename(v string) *MediaAssetCreate { + _c.mutation.SetFilename(v) return _c } -// SetNillableOriginalFilename sets the "original_filename" field if the given value is not nil. -func (_c *MediaAssetCreate) SetNillableOriginalFilename(v *string) *MediaAssetCreate { +// SetNillableFilename sets the "filename" field if the given value is not nil. +func (_c *MediaAssetCreate) SetNillableFilename(v *string) *MediaAssetCreate { if v != nil { - _c.SetOriginalFilename(*v) + _c.SetFilename(*v) } return _c } -// SetRelativePath sets the "relative_path" field. -func (_c *MediaAssetCreate) SetRelativePath(v string) *MediaAssetCreate { - _c.mutation.SetRelativePath(v) +// SetPath sets the "path" field. +func (_c *MediaAssetCreate) SetPath(v string) *MediaAssetCreate { + _c.mutation.SetPath(v) return _c } @@ -205,17 +191,17 @@ func (_c *MediaAssetCreate) check() error { return &ValidationError{Name: "asset_id", err: fmt.Errorf(`ent: validator failed for field "MediaAsset.asset_id": %w`, err)} } } - if v, ok := _c.mutation.OriginalFilename(); ok { - if err := mediaasset.OriginalFilenameValidator(v); err != nil { - return &ValidationError{Name: "original_filename", err: fmt.Errorf(`ent: validator failed for field "MediaAsset.original_filename": %w`, err)} + if v, ok := _c.mutation.Filename(); ok { + if err := mediaasset.FilenameValidator(v); err != nil { + return &ValidationError{Name: "filename", err: fmt.Errorf(`ent: validator failed for field "MediaAsset.filename": %w`, err)} } } - if _, ok := _c.mutation.RelativePath(); !ok { - return &ValidationError{Name: "relative_path", err: errors.New(`ent: missing required field "MediaAsset.relative_path"`)} + if _, ok := _c.mutation.Path(); !ok { + return &ValidationError{Name: "path", err: errors.New(`ent: missing required field "MediaAsset.path"`)} } - if v, ok := _c.mutation.RelativePath(); ok { - if err := mediaasset.RelativePathValidator(v); err != nil { - return &ValidationError{Name: "relative_path", err: fmt.Errorf(`ent: validator failed for field "MediaAsset.relative_path": %w`, err)} + if v, ok := _c.mutation.Path(); ok { + if err := mediaasset.PathValidator(v); err != nil { + return &ValidationError{Name: "path", err: fmt.Errorf(`ent: validator failed for field "MediaAsset.path": %w`, err)} } } if _, ok := _c.mutation.MimeType(); !ok { @@ -284,10 +270,6 @@ func (_c *MediaAssetCreate) createSpec() (*MediaAsset, *sqlgraph.CreateSpec) { _spec.SetField(mediaasset.FieldUpdatedAt, field.TypeString, value) _node.UpdatedAt = value } - if value, ok := _c.mutation.DeletedAt(); ok { - _spec.SetField(mediaasset.FieldDeletedAt, field.TypeString, value) - _node.DeletedAt = &value - } if value, ok := _c.mutation.UUID(); ok { _spec.SetField(mediaasset.FieldUUID, field.TypeString, value) _node.UUID = value @@ -296,13 +278,13 @@ func (_c *MediaAssetCreate) createSpec() (*MediaAsset, *sqlgraph.CreateSpec) { _spec.SetField(mediaasset.FieldAssetID, field.TypeString, value) _node.AssetID = value } - if value, ok := _c.mutation.OriginalFilename(); ok { - _spec.SetField(mediaasset.FieldOriginalFilename, field.TypeString, value) - _node.OriginalFilename = &value + if value, ok := _c.mutation.Filename(); ok { + _spec.SetField(mediaasset.FieldFilename, field.TypeString, value) + _node.Filename = &value } - if value, ok := _c.mutation.RelativePath(); ok { - _spec.SetField(mediaasset.FieldRelativePath, field.TypeString, value) - _node.RelativePath = value + if value, ok := _c.mutation.Path(); ok { + _spec.SetField(mediaasset.FieldPath, field.TypeString, value) + _node.Path = value } if value, ok := _c.mutation.MimeType(); ok { _spec.SetField(mediaasset.FieldMimeType, field.TypeString, value) diff --git a/internal/models/ent/mediaasset_update.go b/internal/models/ent/mediaasset_update.go index 4b980b60..5d6fb20b 100644 --- a/internal/models/ent/mediaasset_update.go +++ b/internal/models/ent/mediaasset_update.go @@ -42,56 +42,36 @@ func (_u *MediaAssetUpdate) SetNillableUpdatedAt(v *string) *MediaAssetUpdate { return _u } -// SetDeletedAt sets the "deleted_at" field. -func (_u *MediaAssetUpdate) SetDeletedAt(v string) *MediaAssetUpdate { - _u.mutation.SetDeletedAt(v) +// SetFilename sets the "filename" field. +func (_u *MediaAssetUpdate) SetFilename(v string) *MediaAssetUpdate { + _u.mutation.SetFilename(v) return _u } -// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil. -func (_u *MediaAssetUpdate) SetNillableDeletedAt(v *string) *MediaAssetUpdate { +// SetNillableFilename sets the "filename" field if the given value is not nil. +func (_u *MediaAssetUpdate) SetNillableFilename(v *string) *MediaAssetUpdate { if v != nil { - _u.SetDeletedAt(*v) + _u.SetFilename(*v) } return _u } -// ClearDeletedAt clears the value of the "deleted_at" field. -func (_u *MediaAssetUpdate) ClearDeletedAt() *MediaAssetUpdate { - _u.mutation.ClearDeletedAt() +// ClearFilename clears the value of the "filename" field. +func (_u *MediaAssetUpdate) ClearFilename() *MediaAssetUpdate { + _u.mutation.ClearFilename() return _u } -// SetOriginalFilename sets the "original_filename" field. -func (_u *MediaAssetUpdate) SetOriginalFilename(v string) *MediaAssetUpdate { - _u.mutation.SetOriginalFilename(v) +// SetPath sets the "path" field. +func (_u *MediaAssetUpdate) SetPath(v string) *MediaAssetUpdate { + _u.mutation.SetPath(v) return _u } -// SetNillableOriginalFilename sets the "original_filename" field if the given value is not nil. -func (_u *MediaAssetUpdate) SetNillableOriginalFilename(v *string) *MediaAssetUpdate { +// SetNillablePath sets the "path" field if the given value is not nil. +func (_u *MediaAssetUpdate) SetNillablePath(v *string) *MediaAssetUpdate { if v != nil { - _u.SetOriginalFilename(*v) - } - return _u -} - -// ClearOriginalFilename clears the value of the "original_filename" field. -func (_u *MediaAssetUpdate) ClearOriginalFilename() *MediaAssetUpdate { - _u.mutation.ClearOriginalFilename() - return _u -} - -// SetRelativePath sets the "relative_path" field. -func (_u *MediaAssetUpdate) SetRelativePath(v string) *MediaAssetUpdate { - _u.mutation.SetRelativePath(v) - return _u -} - -// SetNillableRelativePath sets the "relative_path" field if the given value is not nil. -func (_u *MediaAssetUpdate) SetNillableRelativePath(v *string) *MediaAssetUpdate { - if v != nil { - _u.SetRelativePath(*v) + _u.SetPath(*v) } return _u } @@ -207,14 +187,14 @@ func (_u *MediaAssetUpdate) ExecX(ctx context.Context) { // check runs all checks and user-defined validators on the builder. func (_u *MediaAssetUpdate) check() error { - if v, ok := _u.mutation.OriginalFilename(); ok { - if err := mediaasset.OriginalFilenameValidator(v); err != nil { - return &ValidationError{Name: "original_filename", err: fmt.Errorf(`ent: validator failed for field "MediaAsset.original_filename": %w`, err)} + if v, ok := _u.mutation.Filename(); ok { + if err := mediaasset.FilenameValidator(v); err != nil { + return &ValidationError{Name: "filename", err: fmt.Errorf(`ent: validator failed for field "MediaAsset.filename": %w`, err)} } } - if v, ok := _u.mutation.RelativePath(); ok { - if err := mediaasset.RelativePathValidator(v); err != nil { - return &ValidationError{Name: "relative_path", err: fmt.Errorf(`ent: validator failed for field "MediaAsset.relative_path": %w`, err)} + if v, ok := _u.mutation.Path(); ok { + if err := mediaasset.PathValidator(v); err != nil { + return &ValidationError{Name: "path", err: fmt.Errorf(`ent: validator failed for field "MediaAsset.path": %w`, err)} } } if v, ok := _u.mutation.MimeType(); ok { @@ -261,20 +241,14 @@ func (_u *MediaAssetUpdate) sqlSave(ctx context.Context) (_node int, err error) if value, ok := _u.mutation.UpdatedAt(); ok { _spec.SetField(mediaasset.FieldUpdatedAt, field.TypeString, value) } - if value, ok := _u.mutation.DeletedAt(); ok { - _spec.SetField(mediaasset.FieldDeletedAt, field.TypeString, value) - } - if _u.mutation.DeletedAtCleared() { - _spec.ClearField(mediaasset.FieldDeletedAt, field.TypeString) - } - if value, ok := _u.mutation.OriginalFilename(); ok { - _spec.SetField(mediaasset.FieldOriginalFilename, field.TypeString, value) + if value, ok := _u.mutation.Filename(); ok { + _spec.SetField(mediaasset.FieldFilename, field.TypeString, value) } - if _u.mutation.OriginalFilenameCleared() { - _spec.ClearField(mediaasset.FieldOriginalFilename, field.TypeString) + if _u.mutation.FilenameCleared() { + _spec.ClearField(mediaasset.FieldFilename, field.TypeString) } - if value, ok := _u.mutation.RelativePath(); ok { - _spec.SetField(mediaasset.FieldRelativePath, field.TypeString, value) + if value, ok := _u.mutation.Path(); ok { + _spec.SetField(mediaasset.FieldPath, field.TypeString, value) } if value, ok := _u.mutation.MimeType(); ok { _spec.SetField(mediaasset.FieldMimeType, field.TypeString, value) @@ -333,56 +307,36 @@ func (_u *MediaAssetUpdateOne) SetNillableUpdatedAt(v *string) *MediaAssetUpdate return _u } -// SetDeletedAt sets the "deleted_at" field. -func (_u *MediaAssetUpdateOne) SetDeletedAt(v string) *MediaAssetUpdateOne { - _u.mutation.SetDeletedAt(v) +// SetFilename sets the "filename" field. +func (_u *MediaAssetUpdateOne) SetFilename(v string) *MediaAssetUpdateOne { + _u.mutation.SetFilename(v) return _u } -// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil. -func (_u *MediaAssetUpdateOne) SetNillableDeletedAt(v *string) *MediaAssetUpdateOne { +// SetNillableFilename sets the "filename" field if the given value is not nil. +func (_u *MediaAssetUpdateOne) SetNillableFilename(v *string) *MediaAssetUpdateOne { if v != nil { - _u.SetDeletedAt(*v) + _u.SetFilename(*v) } return _u } -// ClearDeletedAt clears the value of the "deleted_at" field. -func (_u *MediaAssetUpdateOne) ClearDeletedAt() *MediaAssetUpdateOne { - _u.mutation.ClearDeletedAt() +// ClearFilename clears the value of the "filename" field. +func (_u *MediaAssetUpdateOne) ClearFilename() *MediaAssetUpdateOne { + _u.mutation.ClearFilename() return _u } -// SetOriginalFilename sets the "original_filename" field. -func (_u *MediaAssetUpdateOne) SetOriginalFilename(v string) *MediaAssetUpdateOne { - _u.mutation.SetOriginalFilename(v) +// SetPath sets the "path" field. +func (_u *MediaAssetUpdateOne) SetPath(v string) *MediaAssetUpdateOne { + _u.mutation.SetPath(v) return _u } -// SetNillableOriginalFilename sets the "original_filename" field if the given value is not nil. -func (_u *MediaAssetUpdateOne) SetNillableOriginalFilename(v *string) *MediaAssetUpdateOne { +// SetNillablePath sets the "path" field if the given value is not nil. +func (_u *MediaAssetUpdateOne) SetNillablePath(v *string) *MediaAssetUpdateOne { if v != nil { - _u.SetOriginalFilename(*v) - } - return _u -} - -// ClearOriginalFilename clears the value of the "original_filename" field. -func (_u *MediaAssetUpdateOne) ClearOriginalFilename() *MediaAssetUpdateOne { - _u.mutation.ClearOriginalFilename() - return _u -} - -// SetRelativePath sets the "relative_path" field. -func (_u *MediaAssetUpdateOne) SetRelativePath(v string) *MediaAssetUpdateOne { - _u.mutation.SetRelativePath(v) - return _u -} - -// SetNillableRelativePath sets the "relative_path" field if the given value is not nil. -func (_u *MediaAssetUpdateOne) SetNillableRelativePath(v *string) *MediaAssetUpdateOne { - if v != nil { - _u.SetRelativePath(*v) + _u.SetPath(*v) } return _u } @@ -511,14 +465,14 @@ func (_u *MediaAssetUpdateOne) ExecX(ctx context.Context) { // check runs all checks and user-defined validators on the builder. func (_u *MediaAssetUpdateOne) check() error { - if v, ok := _u.mutation.OriginalFilename(); ok { - if err := mediaasset.OriginalFilenameValidator(v); err != nil { - return &ValidationError{Name: "original_filename", err: fmt.Errorf(`ent: validator failed for field "MediaAsset.original_filename": %w`, err)} + if v, ok := _u.mutation.Filename(); ok { + if err := mediaasset.FilenameValidator(v); err != nil { + return &ValidationError{Name: "filename", err: fmt.Errorf(`ent: validator failed for field "MediaAsset.filename": %w`, err)} } } - if v, ok := _u.mutation.RelativePath(); ok { - if err := mediaasset.RelativePathValidator(v); err != nil { - return &ValidationError{Name: "relative_path", err: fmt.Errorf(`ent: validator failed for field "MediaAsset.relative_path": %w`, err)} + if v, ok := _u.mutation.Path(); ok { + if err := mediaasset.PathValidator(v); err != nil { + return &ValidationError{Name: "path", err: fmt.Errorf(`ent: validator failed for field "MediaAsset.path": %w`, err)} } } if v, ok := _u.mutation.MimeType(); ok { @@ -582,20 +536,14 @@ func (_u *MediaAssetUpdateOne) sqlSave(ctx context.Context) (_node *MediaAsset, if value, ok := _u.mutation.UpdatedAt(); ok { _spec.SetField(mediaasset.FieldUpdatedAt, field.TypeString, value) } - if value, ok := _u.mutation.DeletedAt(); ok { - _spec.SetField(mediaasset.FieldDeletedAt, field.TypeString, value) - } - if _u.mutation.DeletedAtCleared() { - _spec.ClearField(mediaasset.FieldDeletedAt, field.TypeString) - } - if value, ok := _u.mutation.OriginalFilename(); ok { - _spec.SetField(mediaasset.FieldOriginalFilename, field.TypeString, value) + if value, ok := _u.mutation.Filename(); ok { + _spec.SetField(mediaasset.FieldFilename, field.TypeString, value) } - if _u.mutation.OriginalFilenameCleared() { - _spec.ClearField(mediaasset.FieldOriginalFilename, field.TypeString) + if _u.mutation.FilenameCleared() { + _spec.ClearField(mediaasset.FieldFilename, field.TypeString) } - if value, ok := _u.mutation.RelativePath(); ok { - _spec.SetField(mediaasset.FieldRelativePath, field.TypeString, value) + if value, ok := _u.mutation.Path(); ok { + _spec.SetField(mediaasset.FieldPath, field.TypeString, value) } if value, ok := _u.mutation.MimeType(); ok { _spec.SetField(mediaasset.FieldMimeType, field.TypeString, value) diff --git a/internal/models/ent/migrate/schema.go b/internal/models/ent/migrate/schema.go index cede4be2..e0196fab 100644 --- a/internal/models/ent/migrate/schema.go +++ b/internal/models/ent/migrate/schema.go @@ -153,11 +153,10 @@ var ( {Name: "id", Type: field.TypeInt, Increment: true}, {Name: "created_at", Type: field.TypeString}, {Name: "updated_at", Type: field.TypeString}, - {Name: "deleted_at", Type: field.TypeString, Nullable: true}, {Name: "uuid", Type: field.TypeString, Unique: true}, {Name: "asset_id", Type: field.TypeString, Unique: true, Size: 64}, - {Name: "original_filename", Type: field.TypeString, Nullable: true, Size: 255}, - {Name: "relative_path", Type: field.TypeString, Unique: true, Size: 1024}, + {Name: "filename", Type: field.TypeString, Nullable: true, Size: 255}, + {Name: "path", Type: field.TypeString, Unique: true, Size: 1024}, {Name: "mime_type", Type: field.TypeString, Size: 128}, {Name: "size", Type: field.TypeInt64}, {Name: "width", Type: field.TypeInt}, @@ -169,25 +168,20 @@ var ( Columns: MediaAssetsColumns, PrimaryKey: []*schema.Column{MediaAssetsColumns[0]}, Indexes: []*schema.Index{ - { - Name: "mediaasset_deleted_at", - Unique: false, - Columns: []*schema.Column{MediaAssetsColumns[3]}, - }, { Name: "mediaasset_uuid", Unique: false, - Columns: []*schema.Column{MediaAssetsColumns[4]}, + Columns: []*schema.Column{MediaAssetsColumns[3]}, }, { Name: "mediaasset_asset_id", Unique: false, - Columns: []*schema.Column{MediaAssetsColumns[5]}, + Columns: []*schema.Column{MediaAssetsColumns[4]}, }, { - Name: "mediaasset_relative_path", + Name: "mediaasset_path", Unique: false, - Columns: []*schema.Column{MediaAssetsColumns[7]}, + Columns: []*schema.Column{MediaAssetsColumns[6]}, }, { Name: "mediaasset_created_at", @@ -199,11 +193,6 @@ var ( Unique: false, Columns: []*schema.Column{MediaAssetsColumns[2]}, }, - { - Name: "mediaasset_deleted_at_created_at_asset_id", - Unique: false, - Columns: []*schema.Column{MediaAssetsColumns[3], MediaAssetsColumns[1], MediaAssetsColumns[5]}, - }, }, } // ThemesColumns holds the columns for the "themes" table. diff --git a/internal/models/ent/mutation.go b/internal/models/ent/mutation.go index 1fc562bb..9214b4f4 100644 --- a/internal/models/ent/mutation.go +++ b/internal/models/ent/mutation.go @@ -2545,27 +2545,26 @@ func (m *KeyBindingMutation) ResetEdge(name string) error { // MediaAssetMutation represents an operation that mutates the MediaAsset nodes in the graph. type MediaAssetMutation struct { config - op Op - typ string - id *int - created_at *string - updated_at *string - deleted_at *string - uuid *string - asset_id *string - original_filename *string - relative_path *string - mime_type *string - size *int64 - addsize *int64 - width *int - addwidth *int - height *int - addheight *int - clearedFields map[string]struct{} - done bool - oldValue func(context.Context) (*MediaAsset, error) - predicates []predicate.MediaAsset + op Op + typ string + id *int + created_at *string + updated_at *string + uuid *string + asset_id *string + filename *string + _path *string + mime_type *string + size *int64 + addsize *int64 + width *int + addwidth *int + height *int + addheight *int + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*MediaAsset, error) + predicates []predicate.MediaAsset } var _ ent.Mutation = (*MediaAssetMutation)(nil) @@ -2738,55 +2737,6 @@ func (m *MediaAssetMutation) ResetUpdatedAt() { m.updated_at = nil } -// SetDeletedAt sets the "deleted_at" field. -func (m *MediaAssetMutation) SetDeletedAt(s string) { - m.deleted_at = &s -} - -// DeletedAt returns the value of the "deleted_at" field in the mutation. -func (m *MediaAssetMutation) DeletedAt() (r string, exists bool) { - v := m.deleted_at - if v == nil { - return - } - return *v, true -} - -// OldDeletedAt returns the old "deleted_at" field's value of the MediaAsset entity. -// If the MediaAsset object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *MediaAssetMutation) OldDeletedAt(ctx context.Context) (v *string, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldDeletedAt is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldDeletedAt requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldDeletedAt: %w", err) - } - return oldValue.DeletedAt, nil -} - -// ClearDeletedAt clears the value of the "deleted_at" field. -func (m *MediaAssetMutation) ClearDeletedAt() { - m.deleted_at = nil - m.clearedFields[mediaasset.FieldDeletedAt] = struct{}{} -} - -// DeletedAtCleared returns if the "deleted_at" field was cleared in this mutation. -func (m *MediaAssetMutation) DeletedAtCleared() bool { - _, ok := m.clearedFields[mediaasset.FieldDeletedAt] - return ok -} - -// ResetDeletedAt resets all changes to the "deleted_at" field. -func (m *MediaAssetMutation) ResetDeletedAt() { - m.deleted_at = nil - delete(m.clearedFields, mediaasset.FieldDeletedAt) -} - // SetUUID sets the "uuid" field. func (m *MediaAssetMutation) SetUUID(s string) { m.uuid = &s @@ -2859,89 +2809,89 @@ func (m *MediaAssetMutation) ResetAssetID() { m.asset_id = nil } -// SetOriginalFilename sets the "original_filename" field. -func (m *MediaAssetMutation) SetOriginalFilename(s string) { - m.original_filename = &s +// SetFilename sets the "filename" field. +func (m *MediaAssetMutation) SetFilename(s string) { + m.filename = &s } -// OriginalFilename returns the value of the "original_filename" field in the mutation. -func (m *MediaAssetMutation) OriginalFilename() (r string, exists bool) { - v := m.original_filename +// Filename returns the value of the "filename" field in the mutation. +func (m *MediaAssetMutation) Filename() (r string, exists bool) { + v := m.filename if v == nil { return } return *v, true } -// OldOriginalFilename returns the old "original_filename" field's value of the MediaAsset entity. +// OldFilename returns the old "filename" field's value of the MediaAsset entity. // If the MediaAsset object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *MediaAssetMutation) OldOriginalFilename(ctx context.Context) (v *string, err error) { +func (m *MediaAssetMutation) OldFilename(ctx context.Context) (v *string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldOriginalFilename is only allowed on UpdateOne operations") + return v, errors.New("OldFilename is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldOriginalFilename requires an ID field in the mutation") + return v, errors.New("OldFilename requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldOriginalFilename: %w", err) + return v, fmt.Errorf("querying old value for OldFilename: %w", err) } - return oldValue.OriginalFilename, nil + return oldValue.Filename, nil } -// ClearOriginalFilename clears the value of the "original_filename" field. -func (m *MediaAssetMutation) ClearOriginalFilename() { - m.original_filename = nil - m.clearedFields[mediaasset.FieldOriginalFilename] = struct{}{} +// ClearFilename clears the value of the "filename" field. +func (m *MediaAssetMutation) ClearFilename() { + m.filename = nil + m.clearedFields[mediaasset.FieldFilename] = struct{}{} } -// OriginalFilenameCleared returns if the "original_filename" field was cleared in this mutation. -func (m *MediaAssetMutation) OriginalFilenameCleared() bool { - _, ok := m.clearedFields[mediaasset.FieldOriginalFilename] +// FilenameCleared returns if the "filename" field was cleared in this mutation. +func (m *MediaAssetMutation) FilenameCleared() bool { + _, ok := m.clearedFields[mediaasset.FieldFilename] return ok } -// ResetOriginalFilename resets all changes to the "original_filename" field. -func (m *MediaAssetMutation) ResetOriginalFilename() { - m.original_filename = nil - delete(m.clearedFields, mediaasset.FieldOriginalFilename) +// ResetFilename resets all changes to the "filename" field. +func (m *MediaAssetMutation) ResetFilename() { + m.filename = nil + delete(m.clearedFields, mediaasset.FieldFilename) } -// SetRelativePath sets the "relative_path" field. -func (m *MediaAssetMutation) SetRelativePath(s string) { - m.relative_path = &s +// SetPath sets the "path" field. +func (m *MediaAssetMutation) SetPath(s string) { + m._path = &s } -// RelativePath returns the value of the "relative_path" field in the mutation. -func (m *MediaAssetMutation) RelativePath() (r string, exists bool) { - v := m.relative_path +// Path returns the value of the "path" field in the mutation. +func (m *MediaAssetMutation) Path() (r string, exists bool) { + v := m._path if v == nil { return } return *v, true } -// OldRelativePath returns the old "relative_path" field's value of the MediaAsset entity. +// OldPath returns the old "path" field's value of the MediaAsset entity. // If the MediaAsset object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *MediaAssetMutation) OldRelativePath(ctx context.Context) (v string, err error) { +func (m *MediaAssetMutation) OldPath(ctx context.Context) (v string, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldRelativePath is only allowed on UpdateOne operations") + return v, errors.New("OldPath is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldRelativePath requires an ID field in the mutation") + return v, errors.New("OldPath requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldRelativePath: %w", err) + return v, fmt.Errorf("querying old value for OldPath: %w", err) } - return oldValue.RelativePath, nil + return oldValue.Path, nil } -// ResetRelativePath resets all changes to the "relative_path" field. -func (m *MediaAssetMutation) ResetRelativePath() { - m.relative_path = nil +// ResetPath resets all changes to the "path" field. +func (m *MediaAssetMutation) ResetPath() { + m._path = nil } // SetMimeType sets the "mime_type" field. @@ -3182,27 +3132,24 @@ func (m *MediaAssetMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *MediaAssetMutation) Fields() []string { - fields := make([]string, 0, 11) + fields := make([]string, 0, 10) if m.created_at != nil { fields = append(fields, mediaasset.FieldCreatedAt) } if m.updated_at != nil { fields = append(fields, mediaasset.FieldUpdatedAt) } - if m.deleted_at != nil { - fields = append(fields, mediaasset.FieldDeletedAt) - } if m.uuid != nil { fields = append(fields, mediaasset.FieldUUID) } if m.asset_id != nil { fields = append(fields, mediaasset.FieldAssetID) } - if m.original_filename != nil { - fields = append(fields, mediaasset.FieldOriginalFilename) + if m.filename != nil { + fields = append(fields, mediaasset.FieldFilename) } - if m.relative_path != nil { - fields = append(fields, mediaasset.FieldRelativePath) + if m._path != nil { + fields = append(fields, mediaasset.FieldPath) } if m.mime_type != nil { fields = append(fields, mediaasset.FieldMimeType) @@ -3228,16 +3175,14 @@ func (m *MediaAssetMutation) Field(name string) (ent.Value, bool) { return m.CreatedAt() case mediaasset.FieldUpdatedAt: return m.UpdatedAt() - case mediaasset.FieldDeletedAt: - return m.DeletedAt() case mediaasset.FieldUUID: return m.UUID() case mediaasset.FieldAssetID: return m.AssetID() - case mediaasset.FieldOriginalFilename: - return m.OriginalFilename() - case mediaasset.FieldRelativePath: - return m.RelativePath() + case mediaasset.FieldFilename: + return m.Filename() + case mediaasset.FieldPath: + return m.Path() case mediaasset.FieldMimeType: return m.MimeType() case mediaasset.FieldSize: @@ -3259,16 +3204,14 @@ func (m *MediaAssetMutation) OldField(ctx context.Context, name string) (ent.Val return m.OldCreatedAt(ctx) case mediaasset.FieldUpdatedAt: return m.OldUpdatedAt(ctx) - case mediaasset.FieldDeletedAt: - return m.OldDeletedAt(ctx) case mediaasset.FieldUUID: return m.OldUUID(ctx) case mediaasset.FieldAssetID: return m.OldAssetID(ctx) - case mediaasset.FieldOriginalFilename: - return m.OldOriginalFilename(ctx) - case mediaasset.FieldRelativePath: - return m.OldRelativePath(ctx) + case mediaasset.FieldFilename: + return m.OldFilename(ctx) + case mediaasset.FieldPath: + return m.OldPath(ctx) case mediaasset.FieldMimeType: return m.OldMimeType(ctx) case mediaasset.FieldSize: @@ -3300,13 +3243,6 @@ func (m *MediaAssetMutation) SetField(name string, value ent.Value) error { } m.SetUpdatedAt(v) return nil - case mediaasset.FieldDeletedAt: - v, ok := value.(string) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetDeletedAt(v) - return nil case mediaasset.FieldUUID: v, ok := value.(string) if !ok { @@ -3321,19 +3257,19 @@ func (m *MediaAssetMutation) SetField(name string, value ent.Value) error { } m.SetAssetID(v) return nil - case mediaasset.FieldOriginalFilename: + case mediaasset.FieldFilename: v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetOriginalFilename(v) + m.SetFilename(v) return nil - case mediaasset.FieldRelativePath: + case mediaasset.FieldPath: v, ok := value.(string) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetRelativePath(v) + m.SetPath(v) return nil case mediaasset.FieldMimeType: v, ok := value.(string) @@ -3432,11 +3368,8 @@ func (m *MediaAssetMutation) AddField(name string, value ent.Value) error { // mutation. func (m *MediaAssetMutation) ClearedFields() []string { var fields []string - if m.FieldCleared(mediaasset.FieldDeletedAt) { - fields = append(fields, mediaasset.FieldDeletedAt) - } - if m.FieldCleared(mediaasset.FieldOriginalFilename) { - fields = append(fields, mediaasset.FieldOriginalFilename) + if m.FieldCleared(mediaasset.FieldFilename) { + fields = append(fields, mediaasset.FieldFilename) } return fields } @@ -3452,11 +3385,8 @@ func (m *MediaAssetMutation) FieldCleared(name string) bool { // error if the field is not defined in the schema. func (m *MediaAssetMutation) ClearField(name string) error { switch name { - case mediaasset.FieldDeletedAt: - m.ClearDeletedAt() - return nil - case mediaasset.FieldOriginalFilename: - m.ClearOriginalFilename() + case mediaasset.FieldFilename: + m.ClearFilename() return nil } return fmt.Errorf("unknown MediaAsset nullable field %s", name) @@ -3472,20 +3402,17 @@ func (m *MediaAssetMutation) ResetField(name string) error { case mediaasset.FieldUpdatedAt: m.ResetUpdatedAt() return nil - case mediaasset.FieldDeletedAt: - m.ResetDeletedAt() - return nil case mediaasset.FieldUUID: m.ResetUUID() return nil case mediaasset.FieldAssetID: m.ResetAssetID() return nil - case mediaasset.FieldOriginalFilename: - m.ResetOriginalFilename() + case mediaasset.FieldFilename: + m.ResetFilename() return nil - case mediaasset.FieldRelativePath: - m.ResetRelativePath() + case mediaasset.FieldPath: + m.ResetPath() return nil case mediaasset.FieldMimeType: m.ResetMimeType() diff --git a/internal/models/ent/runtime/runtime.go b/internal/models/ent/runtime/runtime.go index ab2c906f..8cef6681 100644 --- a/internal/models/ent/runtime/runtime.go +++ b/internal/models/ent/runtime/runtime.go @@ -212,15 +212,11 @@ func init() { keybinding.ScopeValidator = keybindingDescScope.Validators[0].(func(string) error) mediaassetMixin := schema.MediaAsset{}.Mixin() mediaassetMixinHooks0 := mediaassetMixin[0].Hooks() - mediaassetMixinHooks1 := mediaassetMixin[1].Hooks() mediaasset.Hooks[0] = mediaassetMixinHooks0[0] - mediaasset.Hooks[1] = mediaassetMixinHooks1[0] - mediaassetMixinInters1 := mediaassetMixin[1].Interceptors() - mediaasset.Interceptors[0] = mediaassetMixinInters1[0] mediaassetMixinFields0 := mediaassetMixin[0].Fields() _ = mediaassetMixinFields0 - mediaassetMixinFields2 := mediaassetMixin[2].Fields() - _ = mediaassetMixinFields2 + mediaassetMixinFields1 := mediaassetMixin[1].Fields() + _ = mediaassetMixinFields1 mediaassetFields := schema.MediaAsset{}.Fields() _ = mediaassetFields // mediaassetDescCreatedAt is the schema descriptor for created_at field. @@ -232,7 +228,7 @@ func init() { // mediaasset.DefaultUpdatedAt holds the default value on creation for the updated_at field. mediaasset.DefaultUpdatedAt = mediaassetDescUpdatedAt.Default.(func() string) // mediaassetDescUUID is the schema descriptor for uuid field. - mediaassetDescUUID := mediaassetMixinFields2[0].Descriptor() + mediaassetDescUUID := mediaassetMixinFields1[0].Descriptor() // mediaasset.DefaultUUID holds the default value on creation for the uuid field. mediaasset.DefaultUUID = mediaassetDescUUID.Default.(func() string) // mediaassetDescAssetID is the schema descriptor for asset_id field. @@ -253,22 +249,22 @@ func init() { return nil } }() - // mediaassetDescOriginalFilename is the schema descriptor for original_filename field. - mediaassetDescOriginalFilename := mediaassetFields[1].Descriptor() - // mediaasset.OriginalFilenameValidator is a validator for the "original_filename" field. It is called by the builders before save. - mediaasset.OriginalFilenameValidator = mediaassetDescOriginalFilename.Validators[0].(func(string) error) - // mediaassetDescRelativePath is the schema descriptor for relative_path field. - mediaassetDescRelativePath := mediaassetFields[2].Descriptor() - // mediaasset.RelativePathValidator is a validator for the "relative_path" field. It is called by the builders before save. - mediaasset.RelativePathValidator = func() func(string) error { - validators := mediaassetDescRelativePath.Validators + // mediaassetDescFilename is the schema descriptor for filename field. + mediaassetDescFilename := mediaassetFields[1].Descriptor() + // mediaasset.FilenameValidator is a validator for the "filename" field. It is called by the builders before save. + mediaasset.FilenameValidator = mediaassetDescFilename.Validators[0].(func(string) error) + // mediaassetDescPath is the schema descriptor for path field. + mediaassetDescPath := mediaassetFields[2].Descriptor() + // mediaasset.PathValidator is a validator for the "path" field. It is called by the builders before save. + mediaasset.PathValidator = func() func(string) error { + validators := mediaassetDescPath.Validators fns := [...]func(string) error{ validators[0].(func(string) error), validators[1].(func(string) error), } - return func(relative_path string) error { + return func(_path string) error { for _, fn := range fns { - if err := fn(relative_path); err != nil { + if err := fn(_path); err != nil { return err } } diff --git a/internal/models/extension.go b/internal/models/extension.go index 712e43a0..2c5716b0 100644 --- a/internal/models/extension.go +++ b/internal/models/extension.go @@ -27,6 +27,7 @@ const ( ContextMenu ExtensionName = "contextMenu" // 上下文菜单 Search ExtensionName = "search" // 搜索功能 HttpClient ExtensionName = "httpClient" // HTTP 客户端 + InlineImage ExtensionName = "inlineImage" // 编辑器内联图片 BlockImage ExtensionName = "blockImage" // 代码块导出图片 BlockReadonly ExtensionName = "blockReadonly" // 代码块局部只读 ) @@ -108,6 +109,11 @@ func NewDefaultExtensions() []Extension { Enabled: true, Config: ExtensionConfig{}, }, + { + Name: InlineImage, + Enabled: true, + Config: ExtensionConfig{}, + }, { Name: BlockImage, Enabled: true, diff --git a/internal/models/schema/media_asset.go b/internal/models/schema/media_asset.go index 5f1cff19..a4a5f012 100644 --- a/internal/models/schema/media_asset.go +++ b/internal/models/schema/media_asset.go @@ -26,7 +26,6 @@ func (MediaAsset) Annotations() []schema.Annotation { func (MediaAsset) Mixin() []ent.Mixin { return []ent.Mixin{ mixin.TimeMixin{}, - mixin.SoftDeleteMixin{}, mixin.UUIDMixin{}, } } @@ -41,17 +40,17 @@ func (MediaAsset) Fields() []ent.Field { Immutable(). StructTag(`json:"asset_id"`). Comment("stable media asset id derived from content sha256"), - field.String("original_filename"). + field.String("filename"). MaxLen(255). Optional(). Nillable(). - StructTag(`json:"original_filename,omitempty"`). - Comment("original imported filename"), - field.String("relative_path"). + StructTag(`json:"filename,omitempty"`). + Comment("imported filename"), + field.String("path"). MaxLen(1024). NotEmpty(). Unique(). - StructTag(`json:"relative_path"`). + StructTag(`json:"path"`). Comment("media path relative to media root"), field.String("mime_type"). MaxLen(128). @@ -82,9 +81,8 @@ func (MediaAsset) Edges() []ent.Edge { func (MediaAsset) Indexes() []ent.Index { return []ent.Index{ index.Fields("asset_id"), - index.Fields("relative_path"), + index.Fields("path"), index.Fields("created_at"), index.Fields("updated_at"), - index.Fields("deleted_at", "created_at", "asset_id"), } } diff --git a/internal/services/media_http_service.go b/internal/services/media_http_service.go index 7605e341..66931c35 100644 --- a/internal/services/media_http_service.go +++ b/internal/services/media_http_service.go @@ -13,7 +13,6 @@ import ( "voidraft/internal/common/helper" entmodel "voidraft/internal/models/ent" "voidraft/internal/models/ent/mediaasset" - schemamixin "voidraft/internal/models/schema/mixin" "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/services/log" @@ -56,24 +55,23 @@ type ImageImportRequest struct { // ImageAsset describes one imported image asset. type ImageAsset struct { - ID string `json:"id"` - Filename string `json:"filename"` - OriginalFilename string `json:"original_filename,omitempty"` - RelativePath string `json:"relative_path"` - URL string `json:"url"` - MimeType string `json:"mime_type"` - Size int64 `json:"size"` - Width int `json:"width"` - Height int `json:"height"` - SHA256 string `json:"sha256"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` + ID string `json:"id"` + Filename string `json:"filename,omitempty"` + Path string `json:"path"` + URL string `json:"url"` + MimeType string `json:"mime_type"` + Size int64 `json:"size"` + Width int `json:"width"` + Height int `json:"height"` + SHA256 string `json:"sha256"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` } // ImageDeleteResult describes the outcome of a delete operation. type ImageDeleteResult struct { - RelativePath string `json:"relative_path"` - Deleted bool `json:"deleted"` + Path string `json:"path"` + Deleted bool `json:"deleted"` } // NewMediaHTTPService creates a media HTTP service. @@ -198,7 +196,7 @@ func (s *MediaHTTPService) ImportImage(ctx context.Context, request *ImageImport existing, err := client.MediaAsset.Query(). Where(mediaasset.AssetIDEQ(payload.Digest)). - Only(schemamixin.SkipSoftDelete(ctx)) + Only(ctx) if err != nil && !entmodel.IsNotFound(err) { return nil, fmt.Errorf("lookup image asset: %w", err) } @@ -209,14 +207,14 @@ func (s *MediaHTTPService) ImportImage(ctx context.Context, request *ImageImport return s.createIndexedImage(ctx, client, rootPath, request, data, payload) } -// DeleteImage removes one logical image asset and deletes its local file. +// DeleteImage permanently removes one indexed image asset and its local file. func (s *MediaHTTPService) DeleteImage(ctx context.Context, imageRef string) (*ImageDeleteResult, error) { - client, rootPath, err := s.ensureDependencies() + _, rootPath, err := s.ensureDependencies() if err != nil { return nil, err } - asset, err := s.findAssetByReference(ctx, imageRef, true) + asset, err := s.findAssetByReference(ctx, imageRef) if err != nil { return nil, err } @@ -233,18 +231,11 @@ func (s *MediaHTTPService) DeleteImage(ctx context.Context, imageRef string) (*I s.mediaHelper.TrimEmptyMediaDirs(rootPath, absPath) } return &ImageDeleteResult{ - RelativePath: relativePath, - Deleted: deleted, - }, nil - } - if asset.DeletedAt != nil { - return &ImageDeleteResult{ - RelativePath: asset.RelativePath, - Deleted: false, + Path: relativePath, + Deleted: deleted, }, nil } - - _, absPath, err := s.mediaHelper.ResolveManagedImagePath(rootPath, asset.RelativePath, "") + _, absPath, err := s.mediaHelper.ResolveManagedImagePath(rootPath, asset.Path, "") if err != nil { return nil, err } @@ -253,23 +244,22 @@ func (s *MediaHTTPService) DeleteImage(ctx context.Context, imageRef string) (*I return nil, err } - deletedAt := s.now().UTC().Format(time.RFC3339) - if err := client.MediaAsset.UpdateOneID(asset.ID). - SetDeletedAt(deletedAt). - SetUpdatedAt(deletedAt). - Exec(schemamixin.SkipAutoUpdate(ctx)); err != nil { + if err := s.hardDeleteAsset(ctx, asset.ID); err != nil { rollbackErr := s.mediaHelper.RestoreStagedFile(absPath, stagedPath) - return nil, helper.WrapRollbackError("logical delete image asset", err, rollbackErr) + return nil, helper.WrapRollbackError("delete image asset", err, rollbackErr) } if err := s.mediaHelper.DiscardStagedFile(stagedPath); err != nil { s.logger.Warning("discard staged image after delete %s: %v", stagedPath, err) } + if err := s.mediaHelper.DiscardStagedFiles(absPath); err != nil { + s.logger.Warning("discard remaining staged images after delete %s: %v", absPath, err) + } s.mediaHelper.TrimEmptyMediaDirs(rootPath, absPath) return &ImageDeleteResult{ - RelativePath: asset.RelativePath, - Deleted: true, + Path: asset.Path, + Deleted: true, }, nil } @@ -281,47 +271,18 @@ func (s *MediaHTTPService) importExistingImage(ctx context.Context, client *entm return nil, fmt.Errorf("media index database is not configured") } - originalName := coalesceOriginalFilename(asset.OriginalFilename, request.Filename) - _, absPath, err := s.mediaHelper.ResolveManagedImagePath(rootPath, asset.RelativePath, "") + filename := coalesceFilename(asset.Filename, request.Filename) + _, absPath, err := s.mediaHelper.ResolveManagedImagePath(rootPath, asset.Path, "") if err != nil { return nil, err } switch { - case asset.DeletedAt != nil: - stagedPath, err := s.mediaHelper.StageFile(absPath, s.now().UTC()) - if err != nil { - return nil, err - } - if err := s.mediaHelper.WriteBinaryFile(absPath, data); err != nil { - rollbackErr := s.mediaHelper.RollbackFileChange(absPath, stagedPath) - return nil, helper.WrapRollbackError("write restored image asset", err, rollbackErr) - } - if err := client.MediaAsset.UpdateOneID(asset.ID). - ClearDeletedAt(). - SetMimeType(payload.MimeType). - SetSize(int64(len(data))). - SetWidth(payload.Width). - SetHeight(payload.Height). - SetNillableOriginalFilename(originalName). - Exec(ctx); err != nil { - rollbackErr := s.mediaHelper.RollbackFileChange(absPath, stagedPath) - return nil, helper.WrapRollbackError("restore deleted image asset", err, rollbackErr) - } - if err := s.mediaHelper.DiscardStagedFile(stagedPath); err != nil { - s.logger.Warning("discard staged image after restore %s: %v", stagedPath, err) - } - fresh, err := client.MediaAsset.Get(ctx, asset.ID) - if err != nil { - return nil, fmt.Errorf("reload restored image asset: %w", err) - } - return s.imageAssetFromEntity(fresh), nil - case s.mediaHelper.FileExists(absPath): - if asset.OriginalFilename == nil && originalName != nil { + if asset.Filename == nil && filename != nil { if err := client.MediaAsset.UpdateOneID(asset.ID). - SetOriginalFilename(*originalName). - Exec(schemamixin.SkipAutoUpdate(ctx)); err != nil { + SetFilename(*filename). + Exec(ctx); err != nil { return nil, fmt.Errorf("repair indexed image asset: %w", err) } fresh, err := client.MediaAsset.Get(ctx, asset.ID) @@ -336,10 +297,10 @@ func (s *MediaHTTPService) importExistingImage(ctx context.Context, client *entm if err := s.mediaHelper.WriteBinaryFile(absPath, data); err != nil { return nil, err } - if asset.OriginalFilename == nil && originalName != nil { + if asset.Filename == nil && filename != nil { if err := client.MediaAsset.UpdateOneID(asset.ID). - SetOriginalFilename(*originalName). - Exec(schemamixin.SkipAutoUpdate(ctx)); err != nil { + SetFilename(*filename). + Exec(ctx); err != nil { return nil, fmt.Errorf("repair image asset filename: %w", err) } fresh, err := client.MediaAsset.Get(ctx, asset.ID) @@ -369,8 +330,8 @@ func (s *MediaHTTPService) createIndexedImage(ctx context.Context, client *entmo asset, err := client.MediaAsset.Create(). SetAssetID(payload.Digest). - SetNillableOriginalFilename(optionalString(originalFilename(request.Filename))). - SetRelativePath(relativePath). + SetNillableFilename(optionalString(originalFilename(request.Filename))). + SetPath(relativePath). SetMimeType(payload.MimeType). SetSize(int64(len(data))). SetWidth(payload.Width). @@ -383,7 +344,7 @@ func (s *MediaHTTPService) createIndexedImage(ctx context.Context, client *entmo if entmodel.IsConstraintError(err) { existing, lookupErr := client.MediaAsset.Query(). Where(mediaasset.AssetIDEQ(payload.Digest)). - Only(schemamixin.SkipSoftDelete(ctx)) + Only(ctx) if lookupErr == nil && existing != nil { return s.importExistingImage(ctx, client, rootPath, existing, request, data, payload) } @@ -404,23 +365,10 @@ func (s *MediaHTTPService) reconcileMediaIndex(ctx context.Context) error { } if err := s.walkIndexedMediaAssets(ctx, client, func(row *entmodel.MediaAsset) error { - _, absPath, err := s.mediaHelper.ResolveManagedImagePath(rootPath, row.RelativePath, "") + _, absPath, err := s.mediaHelper.ResolveManagedImagePath(rootPath, row.Path, "") if err != nil { return err } - if row.DeletedAt != nil { - if removed, err := s.mediaHelper.RemoveImageArtifacts(absPath); err != nil { - return err - } else if removed { - s.mediaHelper.TrimEmptyMediaDirs(rootPath, absPath) - } - if err := s.mediaHelper.DiscardStagedFiles(absPath); err != nil { - return err - } - s.mediaHelper.TrimEmptyMediaDirs(rootPath, absPath) - return nil - } - if s.mediaHelper.FileExists(absPath) { if err := s.mediaHelper.DiscardStagedFiles(absPath); err != nil { return err @@ -432,7 +380,7 @@ func (s *MediaHTTPService) reconcileMediaIndex(ctx context.Context) error { } else if restored { return nil } - if err := s.hardDeleteAsset(schemamixin.SkipSoftDelete(ctx), row.ID); err != nil { + if err := s.hardDeleteAsset(ctx, row.ID); err != nil { return err } return nil @@ -509,7 +457,7 @@ func (s *MediaHTTPService) walkIndexedMediaAssets(ctx context.Context, client *e query = query.Where(mediaasset.AssetIDGT(lastAssetID)) } - rows, err := query.All(schemamixin.SkipSoftDelete(ctx)) + rows, err := query.All(ctx) if err != nil { return err } @@ -526,7 +474,7 @@ func (s *MediaHTTPService) walkIndexedMediaAssets(ctx context.Context, client *e } } -func (s *MediaHTTPService) findAssetByReference(ctx context.Context, value string, includeDeleted bool) (*entmodel.MediaAsset, error) { +func (s *MediaHTTPService) findAssetByReference(ctx context.Context, value string) (*entmodel.MediaAsset, error) { client, _, err := s.ensureDependencies() if err != nil { return nil, err @@ -539,15 +487,10 @@ func (s *MediaHTTPService) findAssetByReference(ctx context.Context, value strin clean = strings.SplitN(clean, "#", 2)[0] clean = strings.SplitN(clean, "?", 2)[0] - queryCtx := ctx - if includeDeleted { - queryCtx = schemamixin.SkipSoftDelete(ctx) - } - if relativePath, err := s.mediaHelper.NormalizeImageReference(clean, mediaServiceRoute); err == nil { asset, err := client.MediaAsset.Query(). - Where(mediaasset.RelativePathEQ(relativePath)). - Only(queryCtx) + Where(mediaasset.PathEQ(relativePath)). + Only(ctx) if entmodel.IsNotFound(err) { return nil, nil } @@ -563,7 +506,7 @@ func (s *MediaHTTPService) findAssetByReference(ctx context.Context, value strin asset, err := client.MediaAsset.Query(). Where(mediaasset.AssetIDEQ(clean)). - Only(queryCtx) + Only(ctx) if entmodel.IsNotFound(err) { return nil, nil } @@ -584,8 +527,8 @@ func (s *MediaHTTPService) nextAvailableRelativePath(ctx context.Context, digest relativePath := path.Join(relativeDir, filename) existsInIndex, err := client.MediaAsset.Query(). - Where(mediaasset.RelativePathEQ(relativePath)). - Exist(schemamixin.SkipSoftDelete(ctx)) + Where(mediaasset.PathEQ(relativePath)). + Exist(ctx) if err != nil { return "", fmt.Errorf("check media path collision: %w", err) } @@ -618,7 +561,7 @@ func (s *MediaHTTPService) nextAvailableRelativePath(ctx context.Context, digest } func (s *MediaHTTPService) hardDeleteAsset(ctx context.Context, id int) error { - if err := s.dbService.Client.MediaAsset.DeleteOneID(id).Exec(schemamixin.SkipSoftDelete(ctx)); err != nil && !entmodel.IsNotFound(err) { + if err := s.dbService.Client.MediaAsset.DeleteOneID(id).Exec(ctx); err != nil && !entmodel.IsNotFound(err) { return fmt.Errorf("delete media asset index row: %w", err) } return nil @@ -634,25 +577,25 @@ func (s *MediaHTTPService) imageAssetFromEntity(asset *entmodel.MediaAsset) *Ima } result := &ImageAsset{ - ID: asset.AssetID, - Filename: path.Base(asset.RelativePath), - RelativePath: asset.RelativePath, - URL: s.mediaHelper.URL(mediaServiceRoute, asset.RelativePath), - MimeType: asset.MimeType, - Size: asset.Size, - Width: asset.Width, - Height: asset.Height, - SHA256: asset.AssetID, - CreatedAt: asset.CreatedAt, - UpdatedAt: asset.UpdatedAt, - } - if asset.OriginalFilename != nil { - result.OriginalFilename = *asset.OriginalFilename + ID: asset.AssetID, + Filename: path.Base(asset.Path), + Path: asset.Path, + URL: s.mediaHelper.URL(mediaServiceRoute, asset.Path), + MimeType: asset.MimeType, + Size: asset.Size, + Width: asset.Width, + Height: asset.Height, + SHA256: asset.AssetID, + CreatedAt: asset.CreatedAt, + UpdatedAt: asset.UpdatedAt, + } + if asset.Filename != nil && strings.TrimSpace(*asset.Filename) != "" { + result.Filename = *asset.Filename } return result } -func coalesceOriginalFilename(existing *string, imported string) *string { +func coalesceFilename(existing *string, imported string) *string { if existing != nil && strings.TrimSpace(*existing) != "" { value := strings.TrimSpace(*existing) return &value diff --git a/internal/services/media_http_service_test.go b/internal/services/media_http_service_test.go index df515dc6..0b7d6c16 100644 --- a/internal/services/media_http_service_test.go +++ b/internal/services/media_http_service_test.go @@ -19,7 +19,6 @@ import ( "voidraft/internal/models/ent" "voidraft/internal/models/ent/enttest" "voidraft/internal/models/ent/mediaasset" - schemamixin "voidraft/internal/models/schema/mixin" _ "github.com/mattn/go-sqlite3" ) @@ -41,10 +40,10 @@ func newIndexedMediaHTTPService(t *testing.T) (*MediaHTTPService, string) { return service, rootPath } -func failMediaAssetUpdates(client *ent.Client, err error) { +func failMediaAssetDeletes(client *ent.Client, err error) { client.MediaAsset.Use(func(next ent.Mutator) ent.Mutator { return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) { - if m.Op().Is(ent.OpUpdate | ent.OpUpdateOne) { + if m.Op().Is(ent.OpDelete | ent.OpDeleteOne) { return nil, err } return next.Mutate(ctx, m) @@ -180,11 +179,11 @@ func TestMediaHTTPServiceImportImageStoresByDateFolders(t *testing.T) { } expectedRelativePrefix := "images/2026/03/30/" - if got := result.RelativePath; len(got) <= len(expectedRelativePrefix) || got[:len(expectedRelativePrefix)] != expectedRelativePrefix { + if got := result.Path; len(got) <= len(expectedRelativePrefix) || got[:len(expectedRelativePrefix)] != expectedRelativePrefix { t.Fatalf("expected relative path to start with %q, got %q", expectedRelativePrefix, got) } - if result.URL != "/media/"+result.RelativePath { - t.Fatalf("expected url %q, got %q", "/media/"+result.RelativePath, result.URL) + if result.URL != "/media/"+result.Path { + t.Fatalf("expected url %q, got %q", "/media/"+result.Path, result.URL) } if result.Width != 2 || result.Height != 3 { t.Fatalf("expected dimensions 2x3, got %dx%d", result.Width, result.Height) @@ -200,7 +199,7 @@ func TestMediaHTTPServiceImportImageStoresByDateFolders(t *testing.T) { t.Fatalf("expected managed filename %q, got %q", expectedFilename, result.Filename) } - absImagePath := filepath.Join(rootPath, filepath.FromSlash(result.RelativePath)) + absImagePath := filepath.Join(rootPath, filepath.FromSlash(result.Path)) if _, err := os.Stat(absImagePath); err != nil { t.Fatalf("stat imported image: %v", err) } @@ -211,8 +210,8 @@ func TestMediaHTTPServiceImportImageStoresByDateFolders(t *testing.T) { if err != nil { t.Fatalf("query indexed asset: %v", err) } - if indexed.RelativePath != result.RelativePath { - t.Fatalf("expected indexed path %q, got %q", result.RelativePath, indexed.RelativePath) + if indexed.Path != result.Path { + t.Fatalf("expected indexed path %q, got %q", result.Path, indexed.Path) } } @@ -254,8 +253,8 @@ func TestMediaHTTPServiceImportImageDeduplicatesByContentHash(t *testing.T) { if first.ID != second.ID { t.Fatalf("expected deduplicated asset ids to match, got %q and %q", first.ID, second.ID) } - if first.RelativePath != second.RelativePath { - t.Fatalf("expected deduplicated path to match, got %q and %q", first.RelativePath, second.RelativePath) + if first.Path != second.Path { + t.Fatalf("expected deduplicated path to match, got %q and %q", first.Path, second.Path) } rows, err := service.dbService.Client.MediaAsset.Query().All(context.Background()) @@ -266,7 +265,7 @@ func TestMediaHTTPServiceImportImageDeduplicatesByContentHash(t *testing.T) { t.Fatalf("expected 1 indexed row after duplicate import, got %d", len(rows)) } - if _, err := os.Stat(filepath.Join(rootPath, filepath.FromSlash(first.RelativePath))); err != nil { + if _, err := os.Stat(filepath.Join(rootPath, filepath.FromSlash(first.Path))); err != nil { t.Fatalf("expected deduplicated image file to exist: %v", err) } } @@ -282,7 +281,7 @@ func TestMediaHTTPServiceReconcileMediaIndexRemovesMissingFiles(t *testing.T) { t.Fatalf("import image: %v", err) } - absImagePath := filepath.Join(rootPath, filepath.FromSlash(result.RelativePath)) + absImagePath := filepath.Join(rootPath, filepath.FromSlash(result.Path)) if err := os.Remove(absImagePath); err != nil { t.Fatalf("remove local image file: %v", err) } @@ -292,7 +291,7 @@ func TestMediaHTTPServiceReconcileMediaIndexRemovesMissingFiles(t *testing.T) { } _, err = service.dbService.Client.MediaAsset.Query(). Where(mediaasset.AssetIDEQ(result.ID)). - Only(schemamixin.SkipSoftDelete(context.Background())) + Only(context.Background()) if !ent.IsNotFound(err) { t.Fatalf("expected manually deleted asset index row to be removed, err=%v", err) } @@ -309,7 +308,7 @@ func TestMediaHTTPServiceReconcileMediaIndexRestoresStagedFiles(t *testing.T) { t.Fatalf("import image: %v", err) } - absImagePath := filepath.Join(rootPath, filepath.FromSlash(result.RelativePath)) + absImagePath := filepath.Join(rootPath, filepath.FromSlash(result.Path)) stagedPath, err := service.mediaHelper.StageFile(absImagePath, service.now().UTC()) if err != nil { t.Fatalf("stage image file: %v", err) @@ -329,18 +328,15 @@ func TestMediaHTTPServiceReconcileMediaIndexRestoresStagedFiles(t *testing.T) { t.Fatalf("expected staged file to be consumed, stat err=%v", err) } - reloaded, err := service.dbService.Client.MediaAsset.Query(). + _, err = service.dbService.Client.MediaAsset.Query(). Where(mediaasset.AssetIDEQ(result.ID)). Only(context.Background()) if err != nil { t.Fatalf("reload asset after staged reconcile: %v", err) } - if reloaded.DeletedAt != nil { - t.Fatal("expected asset to remain active after staged reconcile") - } } -func TestMediaHTTPServiceDeleteImageRemovesFileAndMarksIndexedRowDeleted(t *testing.T) { +func TestMediaHTTPServiceDeleteImageRemovesFileAndHardDeletesIndexedRow(t *testing.T) { service, rootPath := newIndexedMediaHTTPService(t) result, err := service.ImportImage(context.Background(), &ImageImportRequest{ @@ -359,19 +355,16 @@ func TestMediaHTTPServiceDeleteImageRemovesFileAndMarksIndexedRowDeleted(t *test t.Fatal("expected deleted=true") } - absImagePath := filepath.Join(rootPath, filepath.FromSlash(result.RelativePath)) + absImagePath := filepath.Join(rootPath, filepath.FromSlash(result.Path)) if _, err := os.Stat(absImagePath); !os.IsNotExist(err) { t.Fatalf("expected image file to be removed, stat err=%v", err) } reloaded, err := service.dbService.Client.MediaAsset.Query(). Where(mediaasset.AssetIDEQ(result.ID)). - Only(schemamixin.SkipSoftDelete(context.Background())) - if err != nil { - t.Fatalf("reload deleted asset: %v", err) - } - if reloaded.DeletedAt == nil { - t.Fatal("expected deleted_at to be set after logical delete") + Only(context.Background()) + if !ent.IsNotFound(err) { + t.Fatalf("expected deleted asset row to be removed, err=%v row=%v", err, reloaded) } } @@ -387,13 +380,13 @@ func TestMediaHTTPServiceDeleteImageRestoresFileWhenIndexUpdateFails(t *testing. t.Fatalf("import image: %v", err) } - failMediaAssetUpdates(service.dbService.Client, fmt.Errorf("forced update failure")) + failMediaAssetDeletes(service.dbService.Client, fmt.Errorf("forced delete failure")) if _, err := service.DeleteImage(context.Background(), result.ID); err == nil { t.Fatal("expected delete image to fail") } - absImagePath := filepath.Join(rootPath, filepath.FromSlash(result.RelativePath)) + absImagePath := filepath.Join(rootPath, filepath.FromSlash(result.Path)) restoredData, err := os.ReadFile(absImagePath) if err != nil { t.Fatalf("expected image file to be restored: %v", err) @@ -404,16 +397,16 @@ func TestMediaHTTPServiceDeleteImageRestoresFileWhenIndexUpdateFails(t *testing. reloaded, err := service.dbService.Client.MediaAsset.Query(). Where(mediaasset.AssetIDEQ(result.ID)). - Only(schemamixin.SkipSoftDelete(context.Background())) + Only(context.Background()) if err != nil { t.Fatalf("reload asset after failed delete: %v", err) } - if reloaded.DeletedAt != nil { - t.Fatal("expected deleted_at to remain nil after failed delete") + if reloaded.Path != result.Path { + t.Fatalf("expected asset path %q after failed delete, got %q", result.Path, reloaded.Path) } } -func TestMediaHTTPServiceImportImageRestoreDeletedAssetRollsBackFileWhenIndexUpdateFails(t *testing.T) { +func TestMediaHTTPServiceDeleteImageAllowsCleanReimport(t *testing.T) { service, rootPath := newIndexedMediaHTTPService(t) imageData := mustEncodePNG(t, 2, 2) @@ -426,31 +419,30 @@ func TestMediaHTTPServiceImportImageRestoreDeletedAssetRollsBackFileWhenIndexUpd } if _, err := service.DeleteImage(context.Background(), result.ID); err != nil { - t.Fatalf("delete image before restore rollback test: %v", err) + t.Fatalf("delete image before reimport test: %v", err) } - failMediaAssetUpdates(service.dbService.Client, fmt.Errorf("forced restore failure")) - - if _, err := service.ImportImage(context.Background(), &ImageImportRequest{ + reimported, err := service.ImportImage(context.Background(), &ImageImportRequest{ Filename: "restore-rollback.png", Data: imageData, - }); err == nil { - t.Fatal("expected restore import to fail") + }) + if err != nil { + t.Fatalf("reimport image after delete: %v", err) } - absImagePath := filepath.Join(rootPath, filepath.FromSlash(result.RelativePath)) - if _, err := os.Stat(absImagePath); !os.IsNotExist(err) { - t.Fatalf("expected restored file rollback to remove image, stat err=%v", err) + absImagePath := filepath.Join(rootPath, filepath.FromSlash(result.Path)) + if _, err := os.Stat(absImagePath); err != nil { + t.Fatalf("expected reimported image file to exist, stat err=%v", err) } reloaded, err := service.dbService.Client.MediaAsset.Query(). - Where(mediaasset.AssetIDEQ(result.ID)). - Only(schemamixin.SkipSoftDelete(context.Background())) + Where(mediaasset.AssetIDEQ(reimported.ID)). + Only(context.Background()) if err != nil { - t.Fatalf("reload asset after failed restore: %v", err) + t.Fatalf("reload asset after reimport: %v", err) } - if reloaded.DeletedAt == nil { - t.Fatal("expected deleted_at to remain set after failed restore") + if reloaded.Path != result.Path { + t.Fatalf("expected reimported image path %q to match deleted path %q", reloaded.Path, result.Path) } } diff --git a/internal/services/sync_service.go b/internal/services/sync_service.go index db17c734..6c73b9f9 100644 --- a/internal/services/sync_service.go +++ b/internal/services/sync_service.go @@ -232,16 +232,6 @@ func (s *SyncService) ensureApp() error { s.app = syncer.NewApp(s.dbService.Client, syncer.Options{ Logger: s.logger, MaxSyncAttempts: 3, - MediaRootResolve: func() string { - if s.configService == nil { - return "" - } - config, err := s.configService.GetConfig() - if err != nil { - return "" - } - return helper.NewMediaHelper().RootPath(config.General.DataPath) - }, }) return nil } From 0c6f742a42e5451a50dc6e8471c61287bafcd14a Mon Sep 17 00:00:00 2001 From: landaiqing Date: Wed, 8 Apr 2026 23:57:50 +0800 Subject: [PATCH 23/26] :sparkles: Optimize inline images extension --- .../internal/services/documentservice.ts | 8 + .../voidraft/internal/services/index.ts | 2 + .../voidraft/internal/services/internal.ts | 8 + .../internal/services/mediahttpservice.ts | 20 +- .../internal/services/mediasyncservice.ts | 25 + .../voidraft/internal/services/models.ts | 20 + .../inlineImage/DrawImageDialog.vue | 28 +- .../extensions/inlineImage/inlineImage.ts | 19 +- .../inlineImage/inlineImageWidget.ts | 72 +- internal/services/document_service.go | 17 +- internal/services/media_http_service.go | 336 ++----- internal/services/media_http_service_test.go | 309 ++++++- internal/services/media_reference_service.go | 166 ++++ .../services/media_reference_service_test.go | 28 + internal/services/media_sync_service.go | 875 ++++++++++++++++++ internal/services/service_manager.go | 10 +- 16 files changed, 1646 insertions(+), 297 deletions(-) create mode 100644 frontend/bindings/voidraft/internal/services/internal.ts create mode 100644 frontend/bindings/voidraft/internal/services/mediasyncservice.ts create mode 100644 internal/services/media_reference_service.go create mode 100644 internal/services/media_reference_service_test.go create mode 100644 internal/services/media_sync_service.go diff --git a/frontend/bindings/voidraft/internal/services/documentservice.ts b/frontend/bindings/voidraft/internal/services/documentservice.ts index f306ff4e..fcb05dc5 100644 --- a/frontend/bindings/voidraft/internal/services/documentservice.ts +++ b/frontend/bindings/voidraft/internal/services/documentservice.ts @@ -20,6 +20,9 @@ import * as ent$0 from "../models/ent/models.js"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports import * as $models from "./models.js"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as $internal from "./internal.js"; /** * CreateDocument 创建文档 @@ -81,6 +84,11 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(1739858306, scheduler) as any; + return $resultPromise; +} + /** * UnlockDocument 解锁文档 */ diff --git a/frontend/bindings/voidraft/internal/services/index.ts b/frontend/bindings/voidraft/internal/services/index.ts index a3dac58b..5de85d45 100644 --- a/frontend/bindings/voidraft/internal/services/index.ts +++ b/frontend/bindings/voidraft/internal/services/index.ts @@ -10,6 +10,7 @@ import * as HotkeyService from "./hotkeyservice.js"; import * as HttpClientService from "./httpclientservice.js"; import * as KeyBindingService from "./keybindingservice.js"; import * as MediaHTTPService from "./mediahttpservice.js"; +import * as MediaSyncService from "./mediasyncservice.js"; import * as MigrationService from "./migrationservice.js"; import * as SelfUpdateService from "./selfupdateservice.js"; import * as StartupService from "./startupservice.js"; @@ -29,6 +30,7 @@ export { HttpClientService, KeyBindingService, MediaHTTPService, + MediaSyncService, MigrationService, SelfUpdateService, StartupService, diff --git a/frontend/bindings/voidraft/internal/services/internal.ts b/frontend/bindings/voidraft/internal/services/internal.ts new file mode 100644 index 00000000..d00df59d --- /dev/null +++ b/frontend/bindings/voidraft/internal/services/internal.ts @@ -0,0 +1,8 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import {Create as $Create} from "@wailsio/runtime"; + +export type mediaOrphanCleanupScheduler = any; diff --git a/frontend/bindings/voidraft/internal/services/mediahttpservice.ts b/frontend/bindings/voidraft/internal/services/mediahttpservice.ts index af3b795a..f3f71d2a 100644 --- a/frontend/bindings/voidraft/internal/services/mediahttpservice.ts +++ b/frontend/bindings/voidraft/internal/services/mediahttpservice.ts @@ -2,7 +2,7 @@ // This file is automatically generated. DO NOT EDIT /** - * MediaHTTPService serves media files and manages the indexed image store. + * MediaHTTPService 负责媒体上传、删除和 HTTP 文件访问。 * @module */ @@ -19,7 +19,7 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic import * as $models from "./models.js"; /** - * DeleteImage permanently removes one indexed image asset and its local file. + * DeleteImage permanently removes one indexed image asset and its local file when no active document still references it. */ export function DeleteImage(imageRef: string): Promise<$models.ImageDeleteResult | null> & { cancel(): void } { let $resultPromise = $Call.ByID(143616668, imageRef) as any; @@ -42,24 +42,32 @@ export function ImportImage(request: $models.ImageImportRequest | null): Promise return $typingPromise; } -/** - * ServiceShutdown stops config watchers. - */ export function ServiceShutdown(): Promise & { cancel(): void } { let $resultPromise = $Call.ByID(2519222661) as any; return $resultPromise; } /** - * ServiceStartup configures the service and starts config watchers. + * ServiceStartup keeps the HTTP service lightweight. Root state is managed by MediaSyncService. */ export function ServiceStartup(options: application$0.ServiceOptions): Promise & { cancel(): void } { let $resultPromise = $Call.ByID(2764039056, options) as any; return $resultPromise; } +export function SyncService(): Promise<$models.MediaSyncService | null> & { cancel(): void } { + let $resultPromise = $Call.ByID(2238680370) as any; + let $typingPromise = $resultPromise.then(($result: any) => { + return $$createType5($result); + }) as any; + $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); + return $typingPromise; +} + // Private type creation functions const $$createType0 = $models.ImageDeleteResult.createFrom; const $$createType1 = $Create.Nullable($$createType0); const $$createType2 = $models.ImageAsset.createFrom; const $$createType3 = $Create.Nullable($$createType2); +const $$createType4 = $models.MediaSyncService.createFrom; +const $$createType5 = $Create.Nullable($$createType4); diff --git a/frontend/bindings/voidraft/internal/services/mediasyncservice.ts b/frontend/bindings/voidraft/internal/services/mediasyncservice.ts new file mode 100644 index 00000000..b65a441e --- /dev/null +++ b/frontend/bindings/voidraft/internal/services/mediasyncservice.ts @@ -0,0 +1,25 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * MediaSyncService 管理媒体根目录状态、索引修复和孤儿资源清理。 + * @module + */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import {Call as $Call, Create as $Create} from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js"; + +export function ServiceShutdown(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(760095884) as any; + return $resultPromise; +} + +export function ServiceStartup(options: application$0.ServiceOptions): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(2265252271, options) as any; + return $resultPromise; +} diff --git a/frontend/bindings/voidraft/internal/services/models.ts b/frontend/bindings/voidraft/internal/services/models.ts index 89956ab2..0ca73a7b 100644 --- a/frontend/bindings/voidraft/internal/services/models.ts +++ b/frontend/bindings/voidraft/internal/services/models.ts @@ -275,6 +275,26 @@ export class ImageImportRequest { } } +/** + * MediaSyncService 管理媒体根目录状态、索引修复和孤儿资源清理。 + */ +export class MediaSyncService { + + /** Creates a new MediaSyncService instance. */ + constructor($$source: Partial = {}) { + + Object.assign(this, $$source); + } + + /** + * Creates a new MediaSyncService instance from a string or object. + */ + static createFrom($$source: any = {}): MediaSyncService { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new MediaSyncService($$parsedSource as Partial); + } +} + /** * MemoryStats 内存统计信息 */ diff --git a/frontend/src/components/inlineImage/DrawImageDialog.vue b/frontend/src/components/inlineImage/DrawImageDialog.vue index a6d0f126..31d5d667 100644 --- a/frontend/src/components/inlineImage/DrawImageDialog.vue +++ b/frontend/src/components/inlineImage/DrawImageDialog.vue @@ -1,6 +1,7 @@ @@ -156,7 +215,18 @@ onMounted(() => { :value="syncConfig.git.repo_url" :placeholder="t('settings.sync.repoUrlPlaceholder')" :disabled="!isSyncEnabled" - @input="(e) => configStore.setRepoUrl((e.target as HTMLInputElement).value)" + @change="(e) => configStore.setRepoUrl((e.target as HTMLInputElement).value)" + /> + + + + @@ -198,7 +268,7 @@ onMounted(() => { :value="syncConfig.git.username ?? ''" :placeholder="t('settings.sync.usernamePlaceholder')" :disabled="!isSyncEnabled" - @input="(e) => configStore.setUsername((e.target as HTMLInputElement).value)" + @change="(e) => configStore.setUsername((e.target as HTMLInputElement).value)" /> @@ -209,7 +279,7 @@ onMounted(() => { :value="syncConfig.git.password ?? ''" :placeholder="t('settings.sync.passwordPlaceholder')" :disabled="!isSyncEnabled" - @input="(e) => configStore.setPassword((e.target as HTMLInputElement).value)" + @change="(e) => configStore.setPassword((e.target as HTMLInputElement).value)" /> @@ -222,7 +292,7 @@ onMounted(() => { :value="syncConfig.git.token ?? ''" :placeholder="t('settings.sync.tokenPlaceholder')" :disabled="!isSyncEnabled" - @input="(e) => configStore.setToken((e.target as HTMLInputElement).value)" + @change="(e) => configStore.setToken((e.target as HTMLInputElement).value)" /> @@ -247,7 +317,7 @@ onMounted(() => { :value="syncConfig.git.ssh_key_passphrase ?? ''" :placeholder="t('settings.sync.sshKeyPassphrasePlaceholder')" :disabled="!isSyncEnabled" - @input="(e) => configStore.setSshKeyPassphrase((e.target as HTMLInputElement).value)" + @change="(e) => configStore.setSshKeyPassphrase((e.target as HTMLInputElement).value)" /> @@ -257,18 +327,9 @@ onMounted(() => {
- -
+
+
+ + + +
+ {{ t('settings.sync.historyLoading') }} +
+ +
+ {{ t('settings.sync.historyEmpty') }} +
+ +
+
+
+ {{ formatRunTime(run.started_at) }} + + {{ formatRunTarget(run.target_type) }} · {{ formatRunTrigger(run.trigger_type) }} · {{ formatRunDuration(run) }} + + + {{ formatRunStatus(run.status) }} + +
-
-
{{ t('settings.sync.lastSync') }}: {{ lastSyncText }}
-
{{ t('settings.sync.lastSuccess') }}: {{ lastSuccessText }}
-
- {{ t('settings.sync.lastError') }}: {{ syncStatus.last_error }} -
+
+ + {{ run.target_path || '-' }} + + + {{ formatRunSummary(run) }} +
+ +
+ + + {{ t('settings.sync.historyDetails') }} + +
+
{{ t('settings.sync.historyFields.attempt') }}: {{ run.details.attempt }}/{{ run.details.max_attempts }}
+
{{ t('settings.sync.historyFields.pulled') }}: {{ run.details.flow.pulled ? t('settings.sync.historyBoolean.yes') : t('settings.sync.historyBoolean.no') }}
+
{{ t('settings.sync.historyFields.pushed') }}: {{ run.details.flow.pushed ? t('settings.sync.historyBoolean.yes') : t('settings.sync.historyBoolean.no') }}
+
{{ t('settings.sync.historyFields.changes') }}: +{{ run.details.changes.added }} ~{{ run.details.changes.updated }} -{{ run.details.changes.deleted }}
+
{{ t('settings.sync.historyFields.errorStage') }}: {{ run.details.error.stage }}
+
{{ t('settings.sync.historyFields.errorMessage') }}: {{ run.details.error.message }}
+
{{ t('settings.sync.historyFields.dataPath') }}: {{ run.details.paths.data_path }}
+
{{ t('settings.sync.historyFields.repoPath') }}: {{ run.details.paths.repo_path }}
+
+
- +
+ +
+ + + {{ t('settings.sync.pagination.page', { page: syncStore.currentPage }) }} + + +