From a2a332e735bed03b819ab41e99b88dd5c7fee172 Mon Sep 17 00:00:00 2001 From: landaiqing Date: Fri, 4 Jul 2025 14:37:03 +0800 Subject: [PATCH] :sparkles: Added context menu --- .../voidraft/internal/services/index.ts | 2 + .../internal/services/translationservice.ts | 87 ++++ frontend/package-lock.json | 489 +++++++++--------- frontend/package.json | 46 +- frontend/src/i18n/locales/en-US.ts | 8 +- frontend/src/i18n/locales/zh-CN.ts | 8 +- frontend/src/views/editor/basic/basicSetup.ts | 5 + .../views/editor/contextMenu/contextMenu.css | 156 ++++++ .../editor/contextMenu/contextMenuView.ts | 426 +++++++++++++++ .../src/views/editor/contextMenu/index.ts | 229 ++++++++ .../editor/extensions/hyperlink/index.ts | 31 +- .../views/editor/extensions/minimap/index.ts | 20 + go.mod | 6 +- go.sum | 4 + internal/common/translator/bing_translator.go | 277 ++++++++++ .../common/translator/deepl_translator.go | 318 ++++++++++++ .../common/translator/google_translator.go | 311 +++++++++++ internal/common/translator/translator.go | 73 +++ .../common/translator/youdao_translator.go | 186 +++++++ internal/services/service_manager.go | 61 ++- internal/services/translation_service.go | 253 +++++++++ 21 files changed, 2696 insertions(+), 300 deletions(-) create mode 100644 frontend/bindings/voidraft/internal/services/translationservice.ts create mode 100644 frontend/src/views/editor/contextMenu/contextMenu.css create mode 100644 frontend/src/views/editor/contextMenu/contextMenuView.ts create mode 100644 frontend/src/views/editor/contextMenu/index.ts create mode 100644 internal/common/translator/bing_translator.go create mode 100644 internal/common/translator/deepl_translator.go create mode 100644 internal/common/translator/google_translator.go create mode 100644 internal/common/translator/translator.go create mode 100644 internal/common/translator/youdao_translator.go create mode 100644 internal/services/translation_service.go diff --git a/frontend/bindings/voidraft/internal/services/index.ts b/frontend/bindings/voidraft/internal/services/index.ts index 8ed4a2c..0bca57d 100644 --- a/frontend/bindings/voidraft/internal/services/index.ts +++ b/frontend/bindings/voidraft/internal/services/index.ts @@ -11,6 +11,7 @@ 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 TranslationService from "./translationservice.js"; import * as TrayService from "./trayservice.js"; export { ConfigService, @@ -23,6 +24,7 @@ export { SelfUpdateService, StartupService, SystemService, + TranslationService, TrayService }; diff --git a/frontend/bindings/voidraft/internal/services/translationservice.ts b/frontend/bindings/voidraft/internal/services/translationservice.ts new file mode 100644 index 0000000..24a91a8 --- /dev/null +++ b/frontend/bindings/voidraft/internal/services/translationservice.ts @@ -0,0 +1,87 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * TranslationService 翻译服务 + * @module + */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import {Call as $Call, Create as $Create} from "@wailsio/runtime"; + +/** + * GetAvailableTranslators 获取所有可用翻译器类型 + * @returns {[]string} 翻译器类型列表 + */ +export function GetAvailableTranslators(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(1186597995) as any; + let $typingPromise = $resultPromise.then(($result: any) => { + return $$createType0($result); + }) as any; + $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); + return $typingPromise; +} + +/** + * SetActiveTranslator 设置活跃翻译器 + * @param {string} translatorType - 翻译器类型 ("google", "bing", "youdao", "deepl") + * @returns {error} 可能的错误 + */ +export function SetActiveTranslator(translatorType: string): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(620567821, translatorType) as any; + return $resultPromise; +} + +/** + * SetTimeout 设置翻译超时时间 + * @param {int} seconds - 超时秒数 + */ +export function SetTimeout(seconds: number): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(3787687384, seconds) as any; + return $resultPromise; +} + +/** + * Translate 使用当前活跃翻译器进行翻译 + * @param {string} text - 待翻译文本 + * @param {string} from - 源语言代码 (如 "en", "zh", "auto") + * @param {string} to - 目标语言代码 (如 "en", "zh") + * @returns {string} 翻译后的文本 + * @returns {error} 可能的错误 + */ +export function Translate(text: string, $from: string, to: string): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(2536995103, text, $from, to) as any; + return $resultPromise; +} + +/** + * TranslateWith 使用指定翻译器进行翻译 + * @param {string} text - 待翻译文本 + * @param {string} from - 源语言代码 (如 "en", "zh", "auto") + * @param {string} to - 目标语言代码 (如 "en", "zh") + * @param {string} translatorType - 翻译器类型 ("google", "bing", "youdao", "deepl") + * @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; +} + +/** + * TranslateWithFallback 尝试使用当前活跃翻译器翻译,如果失败则尝试备用翻译器 + * @param {string} text - 待翻译文本 + * @param {string} from - 源语言代码 (如 "en", "zh", "auto") + * @param {string} to - 目标语言代码 (如 "en", "zh") + * @returns {string} 翻译后的文本 + * @returns {string} 使用的翻译器类型 + * @returns {error} 可能的错误 + */ +export function TranslateWithFallback(text: string, $from: string, to: string): Promise<[string, string]> & { cancel(): void } { + let $resultPromise = $Call.ByID(1705788405, text, $from, to) as any; + return $resultPromise; +} + +// Private type creation functions +const $$createType0 = $Create.Array($Create.Any); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 87156b1..31abff4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,36 +11,36 @@ "@codemirror/autocomplete": "^6.18.6", "@codemirror/commands": "^6.8.1", "@codemirror/lang-angular": "^0.1.4", - "@codemirror/lang-cpp": "^6.0.2", + "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-go": "^6.0.1", "@codemirror/lang-html": "^6.4.9", - "@codemirror/lang-java": "^6.0.1", + "@codemirror/lang-java": "^6.0.2", "@codemirror/lang-javascript": "^6.2.4", - "@codemirror/lang-json": "^6.0.1", + "@codemirror/lang-json": "^6.0.2", "@codemirror/lang-less": "^6.0.2", - "@codemirror/lang-lezer": "^6.0.1", + "@codemirror/lang-lezer": "^6.0.2", "@codemirror/lang-liquid": "^6.2.3", "@codemirror/lang-markdown": "^6.3.3", - "@codemirror/lang-php": "^6.0.1", + "@codemirror/lang-php": "^6.0.2", "@codemirror/lang-python": "^6.2.1", - "@codemirror/lang-rust": "^6.0.1", + "@codemirror/lang-rust": "^6.0.2", "@codemirror/lang-sass": "^6.0.2", "@codemirror/lang-sql": "^6.9.0", "@codemirror/lang-vue": "^0.1.3", "@codemirror/lang-wast": "^6.0.2", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "^6.11.1", + "@codemirror/language": "^6.11.2", "@codemirror/language-data": "^6.5.1", "@codemirror/legacy-modes": "^6.5.1", "@codemirror/lint": "^6.8.5", "@codemirror/search": "^6.5.11", "@codemirror/state": "^6.5.2", - "@codemirror/view": "^6.37.2", + "@codemirror/view": "^6.38.0", "@lezer/highlight": "^1.2.1", "@lezer/lr": "^1.4.2", - "codemirror": "^6.0.1", + "codemirror": "^6.0.2", "codemirror-lang-elixir": "^4.0.0", "colors-named": "^1.0.2", "colors-named-hex": "^1.0.2", @@ -48,29 +48,29 @@ "lezer": "^0.13.5", "pinia": "^3.0.3", "pinia-plugin-persistedstate": "^4.4.1", - "prettier": "^3.5.3", + "prettier": "^3.6.2", "remarkable": "^2.0.1", "sass": "^1.89.2", "vue": "^3.5.17", - "vue-i18n": "^11.1.6", + "vue-i18n": "^11.1.9", "vue-router": "^4.5.1" }, "devDependencies": { - "@eslint/js": "^9.29.0", - "@lezer/generator": "^1.7.3", - "@types/node": "^24.0.3", + "@eslint/js": "^9.30.1", + "@lezer/generator": "^1.8.0", + "@types/node": "^24.0.10", "@types/remarkable": "^2.0.8", - "@vitejs/plugin-vue": "^5.2.4", + "@vitejs/plugin-vue": "^6.0.0", "@wailsio/runtime": "latest", - "eslint": "^9.29.0", - "eslint-plugin-vue": "^10.2.0", - "globals": "^16.2.0", + "eslint": "^9.30.1", + "eslint-plugin-vue": "^10.3.0", + "globals": "^16.3.0", "typescript": "^5.8.3", - "typescript-eslint": "^8.34.1", - "unplugin-vue-components": "^28.7.0", - "vite": "^6.3.5", - "vue-eslint-parser": "^10.1.3", - "vue-tsc": "^2.2.10" + "typescript-eslint": "^8.35.1", + "unplugin-vue-components": "^28.8.0", + "vite": "^7.0.1", + "vue-eslint-parser": "^10.2.0", + "vue-tsc": "^3.0.1" } }, "node_modules/@babel/helper-string-parser": { @@ -158,9 +158,9 @@ } }, "node_modules/@codemirror/lang-cpp": { - "version": "6.0.2", - "resolved": "https://registry.npmmirror.com/@codemirror/lang-cpp/-/lang-cpp-6.0.2.tgz", - "integrity": "sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==", + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz", + "integrity": "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -211,9 +211,9 @@ } }, "node_modules/@codemirror/lang-java": { - "version": "6.0.1", - "resolved": "https://registry.npmmirror.com/@codemirror/lang-java/-/lang-java-6.0.1.tgz", - "integrity": "sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==", + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/@codemirror/lang-java/-/lang-java-6.0.2.tgz", + "integrity": "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -236,9 +236,9 @@ } }, "node_modules/@codemirror/lang-json": { - "version": "6.0.1", - "resolved": "https://registry.npmmirror.com/@codemirror/lang-json/-/lang-json-6.0.1.tgz", - "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -259,9 +259,9 @@ } }, "node_modules/@codemirror/lang-lezer": { - "version": "6.0.1", - "resolved": "https://registry.npmmirror.com/@codemirror/lang-lezer/-/lang-lezer-6.0.1.tgz", - "integrity": "sha512-WHwjI7OqKFBEfkunohweqA5B/jIlxaZso6Nl3weVckz8EafYbPZldQEKSDb4QQ9H9BUkle4PVELP4sftKoA0uQ==", + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/@codemirror/lang-lezer/-/lang-lezer-6.0.2.tgz", + "integrity": "sha512-mcVAf8lw+sCfSlr2ivMqV8JtNmOQjSXdA1vHKRtoW0OZsz1k6qhF+DX0K2TbWlAThqiGgRkRSZyYzIoEtKB2uQ==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -302,9 +302,9 @@ } }, "node_modules/@codemirror/lang-php": { - "version": "6.0.1", - "resolved": "https://registry.npmmirror.com/@codemirror/lang-php/-/lang-php-6.0.1.tgz", - "integrity": "sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==", + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/@codemirror/lang-php/-/lang-php-6.0.2.tgz", + "integrity": "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==", "license": "MIT", "dependencies": { "@codemirror/lang-html": "^6.0.0", @@ -328,9 +328,9 @@ } }, "node_modules/@codemirror/lang-rust": { - "version": "6.0.1", - "resolved": "https://registry.npmmirror.com/@codemirror/lang-rust/-/lang-rust-6.0.1.tgz", - "integrity": "sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==", + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz", + "integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -420,9 +420,9 @@ } }, "node_modules/@codemirror/language": { - "version": "6.11.1", - "resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.11.1.tgz", - "integrity": "sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==", + "version": "6.11.2", + "resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.11.2.tgz", + "integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -504,9 +504,9 @@ } }, "node_modules/@codemirror/view": { - "version": "6.37.2", - "resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.37.2.tgz", - "integrity": "sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw==", + "version": "6.38.0", + "resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.38.0.tgz", + "integrity": "sha512-yvSchUwHOdupXkd7xJ0ob36jdsSR/I+/C+VbY0ffBiL5NiSTEBDfB1ZGWbbIlDd5xgdUkody+lukAdOxYrOBeg==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.5.0", @@ -983,9 +983,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.1", - "resolved": "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.20.1.tgz", - "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", + "version": "0.21.0", + "resolved": "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -998,9 +998,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.1", - "resolved": "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", - "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1058,9 +1058,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.29.0", - "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.29.0.tgz", - "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==", + "version": "9.30.1", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.30.1.tgz", + "integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==", "dev": true, "license": "MIT", "engines": { @@ -1161,13 +1161,13 @@ } }, "node_modules/@intlify/core-base": { - "version": "11.1.6", - "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.1.6.tgz", - "integrity": "sha512-gfMLnoWGiQkA1BwK6Qbrog/e3I6Lnkhqk08XObJb0lMq6sLG1Ggl2MazVaMfGnv/E1Td8pCS5UwR54Ys+fOxmQ==", + "version": "11.1.9", + "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.1.9.tgz", + "integrity": "sha512-Lrdi4wp3XnGhWmB/mMD/XtfGUw1Jt+PGpZI/M63X1ZqhTDjNHRVCs/i8vv8U1cwaj1A9fb0bkCQHLSL0SK+pIQ==", "license": "MIT", "dependencies": { - "@intlify/message-compiler": "11.1.6", - "@intlify/shared": "11.1.6" + "@intlify/message-compiler": "11.1.9", + "@intlify/shared": "11.1.9" }, "engines": { "node": ">= 16" @@ -1177,12 +1177,12 @@ } }, "node_modules/@intlify/message-compiler": { - "version": "11.1.6", - "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.1.6.tgz", - "integrity": "sha512-w0LYo5sqgQZF3vEmjLlx+5PYk5EEiB+uigsBkka/DKoAIH2c5xlXcjAxhTgSw35Vrck+GOGriahFsfbHL+ZjPw==", + "version": "11.1.9", + "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.1.9.tgz", + "integrity": "sha512-84SNs3Ikjg0rD1bOuchzb3iK1vR2/8nxrkyccIl5DjFTeMzE/Fxv6X+A7RN5ZXjEWelc1p5D4kHA6HEOhlKL5Q==", "license": "MIT", "dependencies": { - "@intlify/shared": "11.1.6", + "@intlify/shared": "11.1.9", "source-map-js": "^1.0.2" }, "engines": { @@ -1193,9 +1193,9 @@ } }, "node_modules/@intlify/shared": { - "version": "11.1.6", - "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-11.1.6.tgz", - "integrity": "sha512-G1Pe4UILhiGOItuehRW+Pk9/NlnRaMFsdnhZ1fwBjiHvrzitmPNZdLx7Eo3GPfRrsk1mdkilZSfgH8SnM419vA==", + "version": "11.1.9", + "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-11.1.9.tgz", + "integrity": "sha512-H/83xgU1l8ox+qG305p6ucmoy93qyjIPnvxGWRA7YdOoHe1tIiW9IlEu4lTdsOR7cfP1ecrwyflQSqXdXBacXA==", "license": "MIT", "engines": { "node": ">= 16" @@ -1204,6 +1204,29 @@ "url": "https://github.com/sponsors/kazupon" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -1239,9 +1262,9 @@ } }, "node_modules/@lezer/generator": { - "version": "1.7.3", - "resolved": "https://registry.npmmirror.com/@lezer/generator/-/generator-1.7.3.tgz", - "integrity": "sha512-vAI2O1tPF8QMMgp+bdUeeJCneJNkOZvqsrtyb4ohnFVFdboSqPwBEacnt0HH4E+5h+qsIwTHUSAhffU4hzKl1A==", + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@lezer/generator/-/generator-1.8.0.tgz", + "integrity": "sha512-/SF4EDWowPqV1jOgoGSGTIFsE7Ezdr7ZYxyihl5eMKVO5tlnpIhFcDavgm1hHY5GEonoOAEnJ0CU0x+tvuAuUg==", "dev": true, "license": "MIT", "dependencies": { @@ -1797,6 +1820,13 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.19", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", + "integrity": "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.40.0", "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", @@ -2092,9 +2122,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.3", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.0.3.tgz", - "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", + "version": "24.0.10", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.0.10.tgz", + "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==", "dev": true, "license": "MIT", "dependencies": { @@ -2109,17 +2139,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.34.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", - "integrity": "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==", + "version": "8.35.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz", + "integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.34.1", - "@typescript-eslint/type-utils": "8.34.1", - "@typescript-eslint/utils": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1", + "@typescript-eslint/scope-manager": "8.35.1", + "@typescript-eslint/type-utils": "8.35.1", + "@typescript-eslint/utils": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2133,7 +2163,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.34.1", + "@typescript-eslint/parser": "^8.35.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -2149,16 +2179,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.34.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.34.1.tgz", - "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", + "version": "8.35.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.35.1.tgz", + "integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.34.1", - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/typescript-estree": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1", + "@typescript-eslint/scope-manager": "8.35.1", + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/typescript-estree": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1", "debug": "^4.3.4" }, "engines": { @@ -2174,14 +2204,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.34.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.34.1.tgz", - "integrity": "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==", + "version": "8.35.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.35.1.tgz", + "integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.34.1", - "@typescript-eslint/types": "^8.34.1", + "@typescript-eslint/tsconfig-utils": "^8.35.1", + "@typescript-eslint/types": "^8.35.1", "debug": "^4.3.4" }, "engines": { @@ -2196,14 +2226,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.34.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz", - "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==", + "version": "8.35.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz", + "integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1" + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2214,9 +2244,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.34.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz", - "integrity": "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==", + "version": "8.35.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz", + "integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==", "dev": true, "license": "MIT", "engines": { @@ -2231,14 +2261,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.34.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz", - "integrity": "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==", + "version": "8.35.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz", + "integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.34.1", - "@typescript-eslint/utils": "8.34.1", + "@typescript-eslint/typescript-estree": "8.35.1", + "@typescript-eslint/utils": "8.35.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2255,9 +2285,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.34.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.34.1.tgz", - "integrity": "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==", + "version": "8.35.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.35.1.tgz", + "integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==", "dev": true, "license": "MIT", "engines": { @@ -2269,16 +2299,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.34.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz", - "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==", + "version": "8.35.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz", + "integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.34.1", - "@typescript-eslint/tsconfig-utils": "8.34.1", - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1", + "@typescript-eslint/project-service": "8.35.1", + "@typescript-eslint/tsconfig-utils": "8.35.1", + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2324,16 +2354,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.34.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.34.1.tgz", - "integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==", + "version": "8.35.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.35.1.tgz", + "integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.34.1", - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/typescript-estree": "8.34.1" + "@typescript-eslint/scope-manager": "8.35.1", + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/typescript-estree": "8.35.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2348,13 +2378,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.34.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz", - "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==", + "version": "8.35.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz", + "integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/types": "8.35.1", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2366,44 +2396,47 @@ } }, "node_modules/@vitejs/plugin-vue": { - "version": "5.2.4", - "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", - "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.0.tgz", + "integrity": "sha512-iAliE72WsdhjzTOp2DtvKThq1VBC4REhwRcaA+zPAAph6I+OQhUXv+Xu2KS7ElxYtb7Zc/3R30Hwv1DxEo7NXQ==", "dev": true, "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.19" + }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", "vue": "^3.2.25" } }, "node_modules/@volar/language-core": { - "version": "2.4.12", - "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.12.tgz", - "integrity": "sha512-RLrFdXEaQBWfSnYGVxvR2WrO6Bub0unkdHYIdC31HzIEqATIuuhRRzYu76iGPZ6OtA4Au1SnW0ZwIqPP217YhA==", + "version": "2.4.17", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.17.tgz", + "integrity": "sha512-chmRZMbKmcGpKMoO7Reb70uiLrzo0KWC2CkFttKUuKvrE+VYgi+fL9vWMJ07Fv5ulX0V1TAyyacN9q3nc5/ecA==", "dev": true, "license": "MIT", "dependencies": { - "@volar/source-map": "2.4.12" + "@volar/source-map": "2.4.17" } }, "node_modules/@volar/source-map": { - "version": "2.4.12", - "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.12.tgz", - "integrity": "sha512-bUFIKvn2U0AWojOaqf63ER0N/iHIBYZPpNGogfLPQ68F5Eet6FnLlyho7BS0y2HJ1jFhSif7AcuTx1TqsCzRzw==", + "version": "2.4.17", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.17.tgz", + "integrity": "sha512-QDybtQyO3Ms/NjFqNHTC5tbDN2oK5VH7ZaKrcubtfHBDj63n2pizHC3wlMQ+iT55kQXZUUAbmBX5L1C8CHFeBw==", "dev": true, "license": "MIT" }, "node_modules/@volar/typescript": { - "version": "2.4.12", - "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.12.tgz", - "integrity": "sha512-HJB73OTJDgPc80K30wxi3if4fSsZZAOScbj2fcicMuOPoOkcf9NNAINb33o+DzhBdF9xTKC1gnPmIRDous5S0g==", + "version": "2.4.17", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.17.tgz", + "integrity": "sha512-3paEFNh4P5DkgNUB2YkTRrfUekN4brAXxd3Ow1syMqdIPtCZHbUy4AW99S5RO/7mzyTWPMdDSo3mqTpB/LPObQ==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.12", + "@volar/language-core": "2.4.17", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } @@ -2503,18 +2536,18 @@ } }, "node_modules/@vue/language-core": { - "version": "2.2.10", - "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.10.tgz", - "integrity": "sha512-+yNoYx6XIKuAO8Mqh1vGytu8jkFEOH5C8iOv3i8Z/65A7x9iAOXA97Q+PqZ3nlm2lxf5rOJuIGI/wDtx/riNYw==", + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.0.1.tgz", + "integrity": "sha512-sq+/Mc1IqIexWEQ+Q2XPiDb5SxSvY5JPqHnMOl/PlF5BekslzduX8dglSkpC17VeiAQB6dpS+4aiwNLJRduCNw==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "~2.4.11", + "@volar/language-core": "2.4.17", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", - "alien-signals": "^1.0.3", - "minimatch": "^9.0.3", + "alien-signals": "^2.0.5", + "minimatch": "^10.0.1", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" }, @@ -2527,27 +2560,17 @@ } } }, - "node_modules/@vue/language-core/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/@vue/language-core/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.0.3", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2651,9 +2674,9 @@ } }, "node_modules/alien-signals": { - "version": "1.0.13", - "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz", - "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-2.0.5.tgz", + "integrity": "sha512-PdJB6+06nUNAClInE3Dweq7/2xVAYM64vvvS1IHVHSJmgeOtEdrAGyp7Z2oJtYm0B342/Exd2NT0uMJaThcjLQ==", "dev": true, "license": "MIT" }, @@ -2847,9 +2870,9 @@ } }, "node_modules/codemirror": { - "version": "6.0.1", - "resolved": "https://registry.npmmirror.com/codemirror/-/codemirror-6.0.1.tgz", - "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -3146,19 +3169,19 @@ } }, "node_modules/eslint": { - "version": "9.29.0", - "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.29.0.tgz", - "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", + "version": "9.30.1", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.30.1.tgz", + "integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.1", - "@eslint/config-helpers": "^0.2.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.29.0", + "@eslint/js": "9.30.1", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -3207,9 +3230,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "10.2.0", - "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-10.2.0.tgz", - "integrity": "sha512-tl9s+KN3z0hN2b8fV2xSs5ytGl7Esk1oSCxULLwFcdaElhZ8btYYZFrWxvh4En+czrSDtuLCeCOGa8HhEZuBdQ==", + "version": "10.3.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-10.3.0.tgz", + "integrity": "sha512-A0u9snqjCfYaPnqqOaH6MBLVWDUIN4trXn8J3x67uDcXvR7X6Ut8p16N+nYhMCQ9Y7edg2BIRGzfyZsY0IdqoQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3224,8 +3247,14 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", "eslint": "^8.57.0 || ^9.0.0", "vue-eslint-parser": "^10.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/parser": { + "optional": true + } } }, "node_modules/eslint-scope": { @@ -3508,9 +3537,9 @@ } }, "node_modules/globals": { - "version": "16.2.0", - "resolved": "https://registry.npmmirror.com/globals/-/globals-16.2.0.tgz", - "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "version": "16.3.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "dev": true, "license": "MIT", "engines": { @@ -3829,13 +3858,6 @@ "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==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4268,9 +4290,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -4738,15 +4760,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.34.1", - "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.34.1.tgz", - "integrity": "sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==", + "version": "8.35.1", + "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.35.1.tgz", + "integrity": "sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.34.1", - "@typescript-eslint/parser": "8.34.1", - "@typescript-eslint/utils": "8.34.1" + "@typescript-eslint/eslint-plugin": "8.35.1", + "@typescript-eslint/parser": "8.35.1", + "@typescript-eslint/utils": "8.35.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4911,9 +4933,9 @@ } }, "node_modules/unplugin-vue-components": { - "version": "28.7.0", - "resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-28.7.0.tgz", - "integrity": "sha512-3SuWAHlTjOiZckqRBGXRdN/k6IMmKyt2Ch5/+DKwYaT321H0ItdZDvW4r8/YkEKQpN9TN3F/SZ0W342gQROC3Q==", + "version": "28.8.0", + "resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-28.8.0.tgz", + "integrity": "sha512-2Q6ZongpoQzuXDK0ZsVzMoshH0MWZQ1pzVL538G7oIDKRTVzHjppBDS8aB99SADGHN3lpGU7frraCG6yWNoL5Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4923,7 +4945,7 @@ "magic-string": "^0.30.17", "mlly": "^1.7.4", "tinyglobby": "^0.2.14", - "unplugin": "^2.3.4", + "unplugin": "^2.3.5", "unplugin-utils": "^0.2.4" }, "engines": { @@ -4934,7 +4956,7 @@ }, "peerDependencies": { "@babel/parser": "^7.15.8", - "@nuxt/kit": "^3.2.2", + "@nuxt/kit": "^3.2.2 || ^4.0.0", "vue": "2 || 3" }, "peerDependenciesMeta": { @@ -5046,24 +5068,24 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmmirror.com/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/vite/-/vite-7.0.1.tgz", + "integrity": "sha512-BiKOQoW5HGR30E6JDeNsati6HnSPMVEKbkIWbCiol+xKeu3g5owrjy7kbk/QEMuzCV87dSUTvycYKmlcfGKq3Q==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.4.4", + "fdir": "^6.4.6", "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "postcss": "^8.5.6", + "rollup": "^4.40.0", + "tinyglobby": "^0.2.14" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -5072,14 +5094,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", - "less": "*", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -5121,9 +5143,9 @@ } }, "node_modules/vite/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.4.6", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dev": true, "license": "MIT", "peerDependencies": { @@ -5177,9 +5199,9 @@ } }, "node_modules/vue-eslint-parser": { - "version": "10.1.3", - "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-10.1.3.tgz", - "integrity": "sha512-dbCBnd2e02dYWsXoqX5yKUZlOt+ExIpq7hmHKPb5ZqKcjf++Eo0hMseFTZMLKThrUk61m+Uv6A2YSBve6ZvuDQ==", + "version": "10.2.0", + "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-10.2.0.tgz", + "integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==", "dev": true, "license": "MIT", "dependencies": { @@ -5188,7 +5210,6 @@ "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.6.0", - "lodash": "^4.17.21", "semver": "^7.6.3" }, "engines": { @@ -5202,13 +5223,13 @@ } }, "node_modules/vue-i18n": { - "version": "11.1.6", - "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.1.6.tgz", - "integrity": "sha512-+IbsW/sTZHj7U1w0rPOYJbuSB0/7DeO1nvUo3BxvO20OQgHs+ukJ3QeLqvoUA6DiLk+8SA9+djRmKC9+FC6cAg==", + "version": "11.1.9", + "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.1.9.tgz", + "integrity": "sha512-N9ZTsXdRmX38AwS9F6Rh93RtPkvZTkSy/zNv63FTIwZCUbLwwrpqlKz9YQuzFLdlvRdZTnWAUE5jMxr8exdl7g==", "license": "MIT", "dependencies": { - "@intlify/core-base": "11.1.6", - "@intlify/shared": "11.1.6", + "@intlify/core-base": "11.1.9", + "@intlify/shared": "11.1.9", "@vue/devtools-api": "^6.5.0" }, "engines": { @@ -5249,14 +5270,14 @@ "license": "MIT" }, "node_modules/vue-tsc": { - "version": "2.2.10", - "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.10.tgz", - "integrity": "sha512-jWZ1xSaNbabEV3whpIDMbjVSVawjAyW+x1n3JeGQo7S0uv2n9F/JMgWW90tGWNFRKya4YwKMZgCtr0vRAM7DeQ==", + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.0.1.tgz", + "integrity": "sha512-UvMLQD0hAGL1g/NfEQelnSVB4H5gtf/gz2lJKjMMwWNOUmSNyWkejwJagAxEbSjtV5CPPJYslOtoSuqJ63mhdg==", "dev": true, "license": "MIT", "dependencies": { - "@volar/typescript": "~2.4.11", - "@vue/language-core": "2.2.10" + "@volar/typescript": "2.4.17", + "@vue/language-core": "3.0.1" }, "bin": { "vue-tsc": "bin/vue-tsc.js" diff --git a/frontend/package.json b/frontend/package.json index 4f63287..45f76bc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,36 +15,36 @@ "@codemirror/autocomplete": "^6.18.6", "@codemirror/commands": "^6.8.1", "@codemirror/lang-angular": "^0.1.4", - "@codemirror/lang-cpp": "^6.0.2", + "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-go": "^6.0.1", "@codemirror/lang-html": "^6.4.9", - "@codemirror/lang-java": "^6.0.1", + "@codemirror/lang-java": "^6.0.2", "@codemirror/lang-javascript": "^6.2.4", - "@codemirror/lang-json": "^6.0.1", + "@codemirror/lang-json": "^6.0.2", "@codemirror/lang-less": "^6.0.2", - "@codemirror/lang-lezer": "^6.0.1", + "@codemirror/lang-lezer": "^6.0.2", "@codemirror/lang-liquid": "^6.2.3", "@codemirror/lang-markdown": "^6.3.3", - "@codemirror/lang-php": "^6.0.1", + "@codemirror/lang-php": "^6.0.2", "@codemirror/lang-python": "^6.2.1", - "@codemirror/lang-rust": "^6.0.1", + "@codemirror/lang-rust": "^6.0.2", "@codemirror/lang-sass": "^6.0.2", "@codemirror/lang-sql": "^6.9.0", "@codemirror/lang-vue": "^0.1.3", "@codemirror/lang-wast": "^6.0.2", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "^6.11.1", + "@codemirror/language": "^6.11.2", "@codemirror/language-data": "^6.5.1", "@codemirror/legacy-modes": "^6.5.1", "@codemirror/lint": "^6.8.5", "@codemirror/search": "^6.5.11", "@codemirror/state": "^6.5.2", - "@codemirror/view": "^6.37.2", + "@codemirror/view": "^6.38.0", "@lezer/highlight": "^1.2.1", "@lezer/lr": "^1.4.2", - "codemirror": "^6.0.1", + "codemirror": "^6.0.2", "codemirror-lang-elixir": "^4.0.0", "colors-named": "^1.0.2", "colors-named-hex": "^1.0.2", @@ -52,28 +52,28 @@ "lezer": "^0.13.5", "pinia": "^3.0.3", "pinia-plugin-persistedstate": "^4.4.1", - "prettier": "^3.5.3", + "prettier": "^3.6.2", "remarkable": "^2.0.1", "sass": "^1.89.2", "vue": "^3.5.17", - "vue-i18n": "^11.1.6", + "vue-i18n": "^11.1.9", "vue-router": "^4.5.1" }, "devDependencies": { - "@eslint/js": "^9.29.0", - "@lezer/generator": "^1.7.3", - "@types/node": "^24.0.3", + "@eslint/js": "^9.30.1", + "@lezer/generator": "^1.8.0", + "@types/node": "^24.0.10", "@types/remarkable": "^2.0.8", - "@vitejs/plugin-vue": "^5.2.4", + "@vitejs/plugin-vue": "^6.0.0", "@wailsio/runtime": "latest", - "eslint": "^9.29.0", - "eslint-plugin-vue": "^10.2.0", - "globals": "^16.2.0", + "eslint": "^9.30.1", + "eslint-plugin-vue": "^10.3.0", + "globals": "^16.3.0", "typescript": "^5.8.3", - "typescript-eslint": "^8.34.1", - "unplugin-vue-components": "^28.7.0", - "vite": "^6.3.5", - "vue-eslint-parser": "^10.1.3", - "vue-tsc": "^2.2.10" + "typescript-eslint": "^8.35.1", + "unplugin-vue-components": "^28.8.0", + "vite": "^7.0.1", + "vue-eslint-parser": "^10.2.0", + "vue-tsc": "^3.0.1" } } diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 93d79bd..4335fcf 100644 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -18,7 +18,7 @@ export default { blockLanguage: 'Block Language', searchLanguage: 'Search language...', noLanguageFound: 'No language found', - formatHint: 'Current block supports formatting, use Ctrl+Shift+F shortcut for formatting', + formatHint: 'Click Format Block (Ctrl+Shift+F)', // Document selector selectDocument: 'Select Document', searchOrCreateDocument: 'Search or enter new document name...', @@ -201,12 +201,10 @@ export default { name: 'Minimap', description: 'Display minimap overview of the document' }, - search: { name: 'Search', description: 'Text search and replace functionality' }, - fold: { name: 'Code Folding', description: 'Collapse and expand code sections for better readability' @@ -220,6 +218,10 @@ export default { checkbox: { name: 'Checkbox', description: 'Render [x] and [ ] as interactive checkboxes' + }, + codeblock: { + name: 'Code Block', + description: 'Code block related functionality' } }, monitor: { diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 9d73cb7..8a60e14 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -18,7 +18,7 @@ export default { blockLanguage: '块语言', searchLanguage: '搜索语言...', noLanguageFound: '未找到匹配的语言', - formatHint: '当前区块支持格式化,使用 Ctrl+Shift+F 进行格式化', + formatHint: '点击格式化区块(Ctrl+Shift+F)', // 文档选择器 selectDocument: '选择文档', searchOrCreateDocument: '搜索或输入新文档名...', @@ -202,12 +202,10 @@ export default { name: '小地图', description: '显示小地图视图' }, - search: { name: '搜索功能', description: '文本搜索和替换功能' }, - fold: { name: '代码折叠', description: '折叠和展开代码段以提高代码可读性' @@ -221,6 +219,10 @@ export default { checkbox: { name: '选择框', description: '将 [x] 和 [ ] 渲染为可交互的选择框' + }, + codeblock: { + name: '代码块', + description: '代码块相关功能' } }, monitor: { diff --git a/frontend/src/views/editor/basic/basicSetup.ts b/frontend/src/views/editor/basic/basicSetup.ts index a8df30b..8bb1a69 100644 --- a/frontend/src/views/editor/basic/basicSetup.ts +++ b/frontend/src/views/editor/basic/basicSetup.ts @@ -21,6 +21,8 @@ import { import {history} from '@codemirror/commands'; import {highlightSelectionMatches} from '@codemirror/search'; import {autocompletion, closeBrackets, closeBracketsKeymap} from '@codemirror/autocomplete'; +import createEditorContextMenu from '../contextMenu'; + // 基本编辑器设置 export const createBasicSetup = (): Extension[] => { return [ @@ -53,6 +55,9 @@ export const createBasicSetup = (): Extension[] => { // 自动完成 autocompletion(), + // 上下文菜单 + createEditorContextMenu(), + // 键盘映射 keymap.of([ ...closeBracketsKeymap, diff --git a/frontend/src/views/editor/contextMenu/contextMenu.css b/frontend/src/views/editor/contextMenu/contextMenu.css new file mode 100644 index 0000000..d330162 --- /dev/null +++ b/frontend/src/views/editor/contextMenu/contextMenu.css @@ -0,0 +1,156 @@ +/** + * 编辑器上下文菜单样式 + * 支持系统主题自动适配 + */ + +.cm-context-menu { + position: fixed; + background-color: var(--settings-card-bg); + color: var(--settings-text); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 4px 0; + /* 优化阴影效果,只在右下角显示自然的阴影 */ + box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.12); + min-width: 200px; + max-width: 320px; + z-index: 9999; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + opacity: 0; + transform: scale(0.95); + transition: opacity 0.15s ease-out, transform 0.15s ease-out; + overflow: visible; /* 确保子菜单可以显示在外部 */ +} + +.cm-context-menu-item { + padding: 8px 12px; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 14px; + transition: all 0.1s ease; + position: relative; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.cm-context-menu-item:hover { + background-color: var(--toolbar-button-hover); + color: var(--toolbar-text); +} + +.cm-context-menu-item-label { + display: flex; + align-items: center; + gap: 8px; +} + +.cm-context-menu-item-shortcut { + opacity: 0.7; + font-size: 12px; + padding: 2px 4px; + border-radius: 4px; + background-color: var(--settings-input-bg); + color: var(--settings-text-secondary); + margin-left: 16px; +} + +.cm-context-menu-item-ripple { + position: absolute; + border-radius: 50%; + background-color: var(--selection-bg); + width: 100px; + height: 100px; + opacity: 0.5; + transform: scale(0); + transition: transform 0.3s ease-out, opacity 0.3s ease-out; +} + +/* 菜单分组标题样式 */ +.cm-context-menu-group-title { + padding: 6px 12px; + font-size: 12px; + color: var(--text-secondary); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + user-select: none; +} + +/* 菜单分隔线样式 */ +.cm-context-menu-divider { + height: 1px; + background-color: var(--border-color); + margin: 4px 0; +} + +/* 子菜单样式 */ +.cm-context-submenu-container { + position: relative; +} + +.cm-context-menu-item-with-submenu { + position: relative; +} + +.cm-context-menu-item-with-submenu::after { + content: "›"; + position: absolute; + right: 12px; + font-size: 16px; + opacity: 0.7; +} + +.cm-context-submenu { + position: fixed; /* 改为fixed定位,避免受父元素影响 */ + min-width: 180px; + opacity: 0; + pointer-events: none; + transform: translateX(10px); + transition: opacity 0.2s ease, transform 0.2s ease; + z-index: 10000; + border-radius: 6px; + background-color: var(--settings-card-bg); + color: var(--settings-text); + border: 1px solid var(--border-color); + padding: 4px 0; + /* 子菜单也使用相同的阴影效果 */ + box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.12); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); +} + +.cm-context-menu-item-with-submenu:hover .cm-context-submenu { + opacity: 1; + pointer-events: auto; + transform: translateX(0); +} + +/* 深色主题下的特殊样式 */ +:root[data-theme="dark"] .cm-context-menu { + /* 深色主题下阴影更深,但仍然只在右下角 */ + box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25); +} + +:root[data-theme="dark"] .cm-context-submenu { + /* 深色主题下子菜单阴影 */ + box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25); +} + +:root[data-theme="dark"] .cm-context-menu-divider { + background-color: var(--dark-border-color); + opacity: 0.6; +} + +/* 动画相关类 */ +.cm-context-menu.show { + opacity: 1; + transform: scale(1); +} + +.cm-context-menu.hide { + opacity: 0; +} \ No newline at end of file diff --git a/frontend/src/views/editor/contextMenu/contextMenuView.ts b/frontend/src/views/editor/contextMenu/contextMenuView.ts new file mode 100644 index 0000000..15385c9 --- /dev/null +++ b/frontend/src/views/editor/contextMenu/contextMenuView.ts @@ -0,0 +1,426 @@ +/** + * 上下文菜单视图实现 + * 处理菜单的创建、定位和事件绑定 + * 优化为单例模式,避免频繁创建和销毁DOM元素 + */ + +import { EditorView } from "@codemirror/view"; +import { MenuItem } from "../contextMenu"; +import "./contextMenu.css"; + +// 为Window对象添加cmSubmenus属性 +declare global { + interface Window { + cmSubmenus?: Map; + } +} + +// 菜单DOM元素缓存 +let menuElement: HTMLElement | null = null; +let clickOutsideHandler: ((e: MouseEvent) => void) | null = null; +// 子菜单缓存池 +let submenuPool: Map = new Map(); + +/** + * 获取或创建菜单DOM元素 + */ +function getOrCreateMenuElement(): HTMLElement { + if (!menuElement) { + menuElement = document.createElement("div"); + menuElement.className = "cm-context-menu"; + menuElement.style.display = "none"; + document.body.appendChild(menuElement); + + // 阻止菜单内右键点击冒泡 + menuElement.addEventListener('contextmenu', (e) => { + e.preventDefault(); + e.stopPropagation(); + return false; + }); + } + return menuElement; +} + +/** + * 创建或获取子菜单元素 + * @param id 子菜单唯一标识 + */ +function getOrCreateSubmenu(id: string): HTMLElement { + if (!submenuPool.has(id)) { + const submenu = document.createElement("div"); + submenu.className = "cm-context-menu cm-context-submenu"; + submenu.style.display = "none"; + document.body.appendChild(submenu); + submenuPool.set(id, submenu); + + // 阻止子菜单点击事件冒泡 + submenu.addEventListener('click', (e) => { + e.stopPropagation(); + }); + } + return submenuPool.get(id)!; +} + +/** + * 创建菜单项DOM元素 + */ +function createMenuItemElement(item: MenuItem, view: EditorView): HTMLElement { + // 创建菜单项容器 + const menuItem = document.createElement("div"); + menuItem.className = "cm-context-menu-item"; + + // 如果有子菜单,添加相应类 + if (item.submenu && item.submenu.length > 0) { + menuItem.classList.add("cm-context-menu-item-with-submenu"); + } + + // 创建内容容器 + const contentContainer = document.createElement("div"); + contentContainer.className = "cm-context-menu-item-label"; + + // 标签文本 + const label = document.createElement("span"); + label.textContent = item.label; + contentContainer.appendChild(label); + menuItem.appendChild(contentContainer); + + // 快捷键提示(如果有) + if (item.shortcut) { + const shortcut = document.createElement("span"); + shortcut.className = "cm-context-menu-item-shortcut"; + shortcut.textContent = item.shortcut; + menuItem.appendChild(shortcut); + } + + // 如果有子菜单,创建或获取子菜单 + if (item.submenu && item.submenu.length > 0) { + // 使用菜单项标签作为子菜单ID + const submenuId = `submenu-${item.label.replace(/\s+/g, '-').toLowerCase()}`; + const submenu = getOrCreateSubmenu(submenuId); + + // 清空现有子菜单内容 + while (submenu.firstChild) { + submenu.removeChild(submenu.firstChild); + } + + // 添加子菜单项 + item.submenu.forEach(subItem => { + const subMenuItemElement = createMenuItemElement(subItem, view); + submenu.appendChild(subMenuItemElement); + }); + + // 初始状态设置为隐藏 + submenu.style.opacity = '0'; + submenu.style.pointerEvents = 'none'; + submenu.style.visibility = 'hidden'; + submenu.style.display = 'block'; + + // 当鼠标悬停在菜单项上时,显示子菜单 + menuItem.addEventListener('mouseenter', () => { + const rect = menuItem.getBoundingClientRect(); + + // 计算子菜单位置 + submenu.style.left = `${rect.right}px`; + submenu.style.top = `${rect.top}px`; + + // 检查子菜单是否会超出屏幕右侧 + setTimeout(() => { + const submenuRect = submenu.getBoundingClientRect(); + if (submenuRect.right > window.innerWidth) { + // 如果会超出右侧,则显示在左侧 + submenu.style.left = `${rect.left - submenuRect.width}px`; + } + + // 检查子菜单是否会超出屏幕底部 + if (submenuRect.bottom > window.innerHeight) { + // 如果会超出底部,则向上调整 + const newTop = rect.top - (submenuRect.bottom - window.innerHeight); + submenu.style.top = `${Math.max(0, newTop)}px`; + } + }, 0); + + // 显示子菜单 + submenu.style.opacity = '1'; + submenu.style.pointerEvents = 'auto'; + submenu.style.visibility = 'visible'; + submenu.style.transform = 'translateX(0)'; + }); + + // 当鼠标离开菜单项时,隐藏子菜单 + menuItem.addEventListener('mouseleave', (e) => { + // 检查是否移动到子菜单上 + const toElement = e.relatedTarget as HTMLElement; + if (submenu.contains(toElement)) { + return; // 如果移动到子菜单上,不隐藏 + } + + // 隐藏子菜单 + submenu.style.opacity = '0'; + submenu.style.pointerEvents = 'none'; + submenu.style.transform = 'translateX(10px)'; + + // 延迟设置visibility,以便过渡动画能够完成 + setTimeout(() => { + if (submenu.style.opacity === '0') { + submenu.style.visibility = 'hidden'; + } + }, 200); + }); + + // 当鼠标离开子菜单时,隐藏它 + submenu.addEventListener('mouseleave', (e) => { + // 检查是否移动回父菜单项 + const toElement = e.relatedTarget as HTMLElement; + if (menuItem.contains(toElement)) { + return; // 如果移动回父菜单项,不隐藏 + } + + // 隐藏子菜单 + submenu.style.opacity = '0'; + submenu.style.pointerEvents = 'none'; + submenu.style.transform = 'translateX(10px)'; + + // 延迟设置visibility,以便过渡动画能够完成 + setTimeout(() => { + if (submenu.style.opacity === '0') { + submenu.style.visibility = 'hidden'; + } + }, 200); + }); + + // 记录子菜单 + if (!window.cmSubmenus) { + window.cmSubmenus = new Map(); + } + window.cmSubmenus.set(submenuId, submenu); + } + + // 点击事件(仅当有command时添加) + if (item.command) { + menuItem.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + + // 添加点击动画效果 + const ripple = document.createElement("div"); + ripple.className = "cm-context-menu-item-ripple"; + + // 计算相对位置 + const rect = menuItem.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + ripple.style.left = (x - 50) + "px"; + ripple.style.top = (y - 50) + "px"; + + menuItem.appendChild(ripple); + + // 执行点击动画 + setTimeout(() => { + ripple.style.transform = "scale(1)"; + ripple.style.opacity = "0"; + + // 动画完成后移除ripple元素 + setTimeout(() => { + if (ripple.parentNode === menuItem) { + menuItem.removeChild(ripple); + } + }, 300); + }, 10); + + // 执行命令 + item.command!(view); + + // 隐藏菜单 + hideContextMenu(); + }); + } + + return menuItem; +} + +/** + * 创建分隔线 + */ +function createDivider(): HTMLElement { + const divider = document.createElement("div"); + divider.className = "cm-context-menu-divider"; + return divider; +} + +/** + * 添加菜单组 + * @param menuElement 菜单元素 + * @param title 菜单组标题 + * @param items 菜单项 + * @param view 编辑器视图 + */ +function addMenuGroup(menuElement: HTMLElement, title: string | null, items: MenuItem[], view: EditorView): void { + // 如果有标题,添加组标题 + if (title) { + const groupTitle = document.createElement("div"); + groupTitle.className = "cm-context-menu-group-title"; + groupTitle.textContent = title; + menuElement.appendChild(groupTitle); + } + + // 添加菜单项 + items.forEach(item => { + const menuItemElement = createMenuItemElement(item, view); + menuElement.appendChild(menuItemElement); + }); +} + +/** + * 显示上下文菜单 + */ +export function showContextMenu(view: EditorView, clientX: number, clientY: number, items: MenuItem[]): void { + // 获取或创建菜单元素 + const menu = getOrCreateMenuElement(); + + // 如果已经有菜单显示,先隐藏所有子菜单 + hideAllSubmenus(); + + // 清空现有菜单项 + while (menu.firstChild) { + menu.removeChild(menu.firstChild); + } + + // 添加主菜单项 + items.forEach(item => { + const menuItemElement = createMenuItemElement(item, view); + menu.appendChild(menuItemElement); + }); + + // 显示菜单 + menu.style.display = "block"; + + // 定位菜单 + positionMenu(menu, clientX, clientY); + + + // 添加点击外部关闭事件 + if (clickOutsideHandler) { + document.removeEventListener("click", clickOutsideHandler, true); + } + + clickOutsideHandler = (e: MouseEvent) => { + // 检查点击是否在菜单外 + if (menu && !menu.contains(e.target as Node)) { + let isInSubmenu = false; + + // 检查是否点击在子菜单内 + if (window.cmSubmenus) { + window.cmSubmenus.forEach((submenu) => { + if (submenu.contains(e.target as Node)) { + isInSubmenu = true; + } + }); + } + + if (!isInSubmenu) { + hideContextMenu(); + } + } + }; + + // 使用捕获阶段确保事件被处理 + document.addEventListener("click", clickOutsideHandler, true); + + // ESC键关闭 + document.addEventListener("keydown", handleKeyDown); + + // 触发显示动画 + setTimeout(() => { + if (menu) { + menu.classList.add("show"); + } + }, 10); +} + +/** + * 隐藏所有子菜单 + */ +function hideAllSubmenus(): void { + if (window.cmSubmenus) { + window.cmSubmenus.forEach((submenu) => { + submenu.style.opacity = '0'; + submenu.style.pointerEvents = 'none'; + submenu.style.visibility = 'hidden'; + submenu.style.transform = 'translateX(10px)'; + }); + } +} + +/** + * 处理键盘事件 + */ +function handleKeyDown(e: KeyboardEvent): void { + if (e.key === "Escape") { + hideContextMenu(); + document.removeEventListener("keydown", handleKeyDown); + } +} + +/** + * 隐藏上下文菜单 + */ +export function hideContextMenu(): void { + // 隐藏所有子菜单 + hideAllSubmenus(); + + if (menuElement) { + // 添加淡出动画 + menuElement.classList.remove("show"); + menuElement.classList.add("hide"); + + // 等待动画完成后隐藏(不移除DOM元素) + setTimeout(() => { + if (menuElement) { + menuElement.style.display = "none"; + menuElement.classList.remove("hide"); + } + }, 150); + } + + if (clickOutsideHandler) { + document.removeEventListener("click", clickOutsideHandler, true); + clickOutsideHandler = null; + } + + document.removeEventListener("keydown", handleKeyDown); +} + +/** + * 定位菜单元素 + */ +function positionMenu(menu: HTMLElement, clientX: number, clientY: number): void { + // 获取窗口尺寸 + const windowWidth = window.innerWidth; + const windowHeight = window.innerHeight; + + // 初始位置设置 + let left = clientX; + let top = clientY; + + // 确保菜单在视窗内 + setTimeout(() => { + // 计算菜单尺寸 + const menuWidth = menu.offsetWidth; + const menuHeight = menu.offsetHeight; + + // 确保菜单不会超出右侧边界 + if (left + menuWidth > windowWidth) { + left = windowWidth - menuWidth - 5; + } + + // 确保菜单不会超出底部边界 + if (top + menuHeight > windowHeight) { + top = windowHeight - menuHeight - 5; + } + + // 应用位置 + menu.style.left = `${left}px`; + menu.style.top = `${top}px`; + }, 0); +} \ No newline at end of file diff --git a/frontend/src/views/editor/contextMenu/index.ts b/frontend/src/views/editor/contextMenu/index.ts new file mode 100644 index 0000000..2797135 --- /dev/null +++ b/frontend/src/views/editor/contextMenu/index.ts @@ -0,0 +1,229 @@ +/** + * 编辑器上下文菜单实现 + * 提供基本的复制、剪切、粘贴等操作,支持动态快捷键显示 + */ + +import { EditorView } from "@codemirror/view"; +import { Extension } from "@codemirror/state"; +import { copyCommand, cutCommand, pasteCommand } from "../extensions/codeblock/copyPaste"; +import { KeyBindingCommand } from "@/../bindings/voidraft/internal/models/models"; +import { useKeybindingStore } from "@/stores/keybindingStore"; +import { + undo, redo +} from "@codemirror/commands"; +import { + deleteBlock, formatCurrentBlock, + addNewBlockAfterCurrent, addNewBlockAfterLast, addNewBlockBeforeCurrent +} from "../extensions/codeblock/commands"; +import { commandRegistry } from "@/views/editor/keymap"; +import i18n from "@/i18n"; +import {useSystemStore} from "@/stores/systemStore"; + +/** + * 菜单项类型定义 + */ +export interface MenuItem { + /** 菜单项显示文本 */ + label: string; + + /** 点击时执行的命令 (如果有子菜单,可以为null) */ + command?: (view: EditorView) => boolean; + + /** 快捷键提示文本 (可选) */ + shortcut?: string; + + /** 子菜单项 (可选) */ + submenu?: MenuItem[]; +} + +// 导入相关功能 +import { showContextMenu } from "./contextMenuView"; + +/** + * 获取翻译文本 + * @param key 翻译键 + * @returns 翻译后的文本 + */ +function t(key: string): string { + return i18n.global.t(key); +} + +/** + * 获取快捷键显示文本 + * @param command 命令ID + * @returns 快捷键显示文本 + */ +function getShortcutText(command: KeyBindingCommand): string { + try { + const keybindingStore = useKeybindingStore(); + + // 如果找到该命令的快捷键配置 + const binding = keybindingStore.keyBindings.find(kb => + kb.command === command && kb.enabled + ); + + if (binding && binding.key) { + // 格式化快捷键显示 + return formatKeyBinding(binding.key); + } + } catch (error) { + console.warn("An error occurred while getting the shortcut:", error); + } + + return ""; +} + +/** + * 格式化快捷键显示 + * @param keyBinding 快捷键字符串 + * @returns 格式化后的显示文本 + */ +function formatKeyBinding(keyBinding: string): string { + // 获取系统信息 + const systemStore = useSystemStore(); + const isMac = systemStore.isMacOS; + + // 替换修饰键名称为更友好的显示 + return keyBinding + .replace("Mod", isMac ? "⌘" : "Ctrl") + .replace("Shift", isMac ? "⇧" : "Shift") + .replace("Alt", isMac ? "⌥" : "Alt") + .replace("Ctrl", isMac ? "⌃" : "Ctrl") + .replace(/-/g, " + "); +} + +/** + * 从命令注册表获取命令处理程序和翻译键 + * @param command 命令ID + * @returns 命令处理程序和翻译键 + */ +function getCommandInfo(command: KeyBindingCommand): { handler: (view: EditorView) => boolean, descriptionKey: string } | undefined { + return commandRegistry[command]; +} + +/** + * 创建编辑菜单项 + */ +function createEditItems(): MenuItem[] { + return [ + { + label: t("keybindings.commands.blockCopy"), + command: copyCommand, + shortcut: getShortcutText(KeyBindingCommand.BlockCopyCommand) + }, + { + label: t("keybindings.commands.blockCut"), + command: cutCommand, + shortcut: getShortcutText(KeyBindingCommand.BlockCutCommand) + }, + { + label: t("keybindings.commands.blockPaste"), + command: pasteCommand, + shortcut: getShortcutText(KeyBindingCommand.BlockPasteCommand) + } + ]; +} + +/** + * 创建历史操作菜单项 + */ +function createHistoryItems(): MenuItem[] { + return [ + { + label: t("keybindings.commands.historyUndo"), + command: undo, + shortcut: getShortcutText(KeyBindingCommand.HistoryUndoCommand) + }, + { + label: t("keybindings.commands.historyRedo"), + command: redo, + shortcut: getShortcutText(KeyBindingCommand.HistoryRedoCommand) + } + ]; +} + +/** + * 创建代码块相关菜单项 + */ +function createCodeBlockItems(): MenuItem[] { + const defaultOptions = { defaultBlockToken: 'text', defaultBlockAutoDetect: true }; + return [ + // 格式化 + { + label: t("keybindings.commands.blockFormat"), + command: formatCurrentBlock, + shortcut: getShortcutText(KeyBindingCommand.BlockFormatCommand) + }, + // 删除 + { + label: t("keybindings.commands.blockDelete"), + command: deleteBlock(defaultOptions), + shortcut: getShortcutText(KeyBindingCommand.BlockDeleteCommand) + }, + // 在当前块后添加新块 + { + label: t("keybindings.commands.blockAddAfterCurrent"), + command: addNewBlockAfterCurrent(defaultOptions), + shortcut: getShortcutText(KeyBindingCommand.BlockAddAfterCurrentCommand) + }, + // 在当前块前添加新块 + { + label: t("keybindings.commands.blockAddBeforeCurrent"), + command: addNewBlockBeforeCurrent(defaultOptions), + shortcut: getShortcutText(KeyBindingCommand.BlockAddBeforeCurrentCommand) + }, + // 在最后添加新块 + { + label: t("keybindings.commands.blockAddAfterLast"), + command: addNewBlockAfterLast(defaultOptions), + shortcut: getShortcutText(KeyBindingCommand.BlockAddAfterLastCommand) + } + ]; +} + +/** + * 创建主菜单项 + */ +function createMainMenuItems(): MenuItem[] { + // 基本编辑操作放在主菜单 + const basicItems = createEditItems(); + + // 历史操作放在主菜单 + const historyItems = createHistoryItems(); + + // 构建主菜单 + return [ + ...basicItems, + ...historyItems, + { + label: t("extensions.codeblock.name"), + submenu: createCodeBlockItems() + } + ]; +} + +/** + * 创建编辑器上下文菜单 + */ +export function createEditorContextMenu(): Extension { + // 为编辑器添加右键事件处理 + return EditorView.domEventHandlers({ + contextmenu: (event, view) => { + // 阻止默认右键菜单 + event.preventDefault(); + + // 获取菜单项 + const menuItems = createMainMenuItems(); + + // 显示上下文菜单 + showContextMenu(view, event.clientX, event.clientY, menuItems); + + return true; + } + }); +} + +/** + * 默认导出 + */ +export default createEditorContextMenu; \ No newline at end of file diff --git a/frontend/src/views/editor/extensions/hyperlink/index.ts b/frontend/src/views/editor/extensions/hyperlink/index.ts index 829edc3..f9fee49 100644 --- a/frontend/src/views/editor/extensions/hyperlink/index.ts +++ b/frontend/src/views/editor/extensions/hyperlink/index.ts @@ -30,11 +30,15 @@ class HyperLinkIcon extends WidgetType { toDOM() { const wrapper = document.createElement('a'); wrapper.href = this.state.url; - wrapper.target = '_blank'; wrapper.innerHTML = pathStr; - wrapper.className = 'cm-hyper-link-icon'; - wrapper.rel = 'nofollow'; + wrapper.className = 'cm-hyper-link-icon cm-hyper-link-underline'; wrapper.title = this.state.url; + wrapper.setAttribute('data-url', this.state.url); + wrapper.onclick = (e) => { + e.preventDefault(); + runtime.Browser.OpenURL(this.state.url); + return false; + }; const anchor = this.state.anchor && this.state.anchor(wrapper); return anchor || wrapper; } @@ -141,10 +145,12 @@ export const hyperLinkStyle = EditorView.baseTheme({ color: '#0969da', cursor: 'pointer', transition: 'color 0.2s ease', - textDecoration: 'none', + textDecoration: 'underline', + textDecorationColor: '#0969da', + textDecorationThickness: '1px', + textUnderlineOffset: '2px', '&:hover': { color: '#0550ae', - textDecoration: 'underline', } }, @@ -160,9 +166,9 @@ export const hyperLinkStyle = EditorView.baseTheme({ verticalAlign: 'middle', marginLeft: '0.2ch', color: '#656d76', - textDecoration: 'none', opacity: 0.7, transition: 'opacity 0.2s ease, color 0.2s ease', + cursor: 'pointer', '&:hover': { opacity: 1, color: '#0969da', @@ -197,12 +203,17 @@ export const hyperLinkStyle = EditorView.baseTheme({ export const hyperLinkClickHandler = EditorView.domEventHandlers({ click: (event, view) => { const target = event.target as HTMLElement; + let urlElement = target; - if (target.classList.contains('cm-hyper-link-text')) { - const url = target.getAttribute('data-url'); + while (urlElement && !urlElement.hasAttribute('data-url')) { + urlElement = urlElement.parentElement as HTMLElement; + if (!urlElement || urlElement === document.body) break; + } + + if (urlElement && urlElement.hasAttribute('data-url')) { + const url = urlElement.getAttribute('data-url'); if (url) { - // window.open(url, '_blank', 'noopener,noreferrer'); - runtime.Browser.OpenURL(url).then() + runtime.Browser.OpenURL(url) event.preventDefault(); return true; } diff --git a/frontend/src/views/editor/extensions/minimap/index.ts b/frontend/src/views/editor/extensions/minimap/index.ts index 8c1f510..be54eb0 100644 --- a/frontend/src/views/editor/extensions/minimap/index.ts +++ b/frontend/src/views/editor/extensions/minimap/index.ts @@ -97,6 +97,26 @@ const minimapClass = ViewPlugin.fromClass( } } + // 阻止小地图上的右键菜单 + this.dom.addEventListener('contextmenu', (e) => { + e.preventDefault(); + e.stopPropagation(); + return false; + }); + + // 阻止小地图内部元素和画布上的右键菜单 + this.inner.addEventListener('contextmenu', (e) => { + e.preventDefault(); + e.stopPropagation(); + return false; + }); + + this.canvas.addEventListener('contextmenu', (e) => { + e.preventDefault(); + e.stopPropagation(); + return false; + }); + if (config.autohide) { this.dom.classList.add('cm-minimap-autohide'); } diff --git a/go.mod b/go.mod index 7f01cde..e26b831 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,11 @@ require ( github.com/knadh/koanf/providers/file v1.2.0 github.com/knadh/koanf/providers/structs v1.0.0 github.com/knadh/koanf/v2 v2.2.1 + github.com/robertkrimen/otto v0.5.1 github.com/wailsapp/wails/v3 v3.0.0-alpha.9 + golang.org/x/net v0.41.0 golang.org/x/sys v0.33.0 + golang.org/x/text v0.26.0 modernc.org/sqlite v1.38.0 ) @@ -71,11 +74,10 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect golang.org/x/crypto v0.39.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/net v0.41.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/text v0.26.0 // indirect golang.org/x/time v0.12.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/sourcemap.v1 v1.0.5 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.66.2 // indirect diff --git a/go.sum b/go.sum index 0e0126d..c295e65 100644 --- a/go.sum +++ b/go.sum @@ -142,6 +142,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0= +github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8= 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.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= @@ -224,6 +226,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= +gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/common/translator/bing_translator.go b/internal/common/translator/bing_translator.go new file mode 100644 index 0000000..ce661bf --- /dev/null +++ b/internal/common/translator/bing_translator.go @@ -0,0 +1,277 @@ +// Package translator 提供文本翻译功能 +package translator + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "regexp" + "strings" + "time" + + "golang.org/x/text/language" +) + +// BingTranslator Bing翻译器结构体 +type BingTranslator struct { + BingHost string // Bing服务主机 + httpClient *http.Client // HTTP客户端 + Timeout time.Duration // 请求超时时间 +} + +// 常量定义 +const ( + bingDefaultTimeout = 30 * time.Second + defaultBingHost = "cn.bing.com" // 使用cn.bing.com作为默认域名 +) + +// 错误定义 +var ( + ErrBingNetworkError = errors.New("bing translator network error") + ErrBingParseError = errors.New("bing translator parse error") + ErrBingTokenError = errors.New("failed to get bing translator token") +) + +// BingTranslationParams Bing翻译所需的参数 +type BingTranslationParams struct { + Token string // token参数 + Key string // key参数 + IG string // IG参数 +} + +// NewBingTranslator 创建一个新的Bing翻译器实例 +func NewBingTranslator() *BingTranslator { + translator := &BingTranslator{ + BingHost: defaultBingHost, + Timeout: bingDefaultTimeout, + httpClient: &http.Client{Timeout: bingDefaultTimeout}, + } + + return translator +} + +// SetTimeout 设置请求超时时间 +func (t *BingTranslator) SetTimeout(timeout time.Duration) { + t.Timeout = timeout + t.httpClient.Timeout = timeout +} + +// SetBingHost 设置Bing主机 +func (t *BingTranslator) SetBingHost(host string) { + t.BingHost = host +} + +// Translate 使用标准语言标签进行文本翻译 +func (t *BingTranslator) Translate(text string, from language.Tag, to language.Tag) (string, error) { + return t.translate(text, from.String(), to.String()) +} + +// TranslateWithParams 使用简单字符串参数进行文本翻译 +func (t *BingTranslator) TranslateWithParams(text string, params TranslationParams) (string, error) { + return t.translate(text, params.From, params.To) +} + +// translate 执行实际翻译操作 +func (t *BingTranslator) translate(text, from, to string) (string, error) { + // 获取翻译所需的参数 + params, err := t.ExtractBingTranslationParams() + if err != nil { + return "", fmt.Errorf("failed to extract bing translation params: %w", err) + } + + // 执行翻译 + return t.GetBingTranslation(params.Token, params.Key, params.IG, text, from, to) +} + +// ExtractBingTranslationParams 提取Bing翻译所需的参数 +func (t *BingTranslator) ExtractBingTranslationParams() (*BingTranslationParams, error) { + // 发送GET请求获取网页内容 + url := fmt.Sprintf("https://%s/translator?mkt=zh-CN", t.BingHost) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36") + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") + req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") + + resp, err := t.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrBingNetworkError, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to access Bing translator page: status code %d", resp.StatusCode) + } + + // 读取响应内容 + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + pageContent := string(body) + + // 模式1: 标准的params_AbusePreventionHelper数组 + keyPattern := regexp.MustCompile(`params_AbusePreventionHelper\s*=\s*\[([^\]]+)\]`) + keyMatch := keyPattern.FindStringSubmatch(pageContent) + + var key, token string + + if len(keyMatch) >= 2 { + // 提取并解析数组 + paramsStr := keyMatch[1] + paramsList := strings.Split(paramsStr, ",") + + if len(paramsList) >= 2 { + // 清理引号 + key = strings.Trim(paramsList[0], `"' `) + token = strings.Trim(paramsList[1], `"' `) + } + } + + // 如果标准模式失败,尝试备用模式 + if key == "" || token == "" { + // 模式2: 查找_G.Token和_G.Key + tokenPattern := regexp.MustCompile(`_G\.Token\s*=\s*["']([^"']+)["']`) + tokenMatch := tokenPattern.FindStringSubmatch(pageContent) + + keyPattern := regexp.MustCompile(`_G\.Key\s*=\s*["']?([^"',]+)["']?`) + keyMatch := keyPattern.FindStringSubmatch(pageContent) + + if len(tokenMatch) >= 2 && len(keyMatch) >= 2 { + token = tokenMatch[1] + key = keyMatch[1] + } + } + + // 如果仍然失败,尝试JSON格式 + if key == "" || token == "" { + jsonPattern := regexp.MustCompile(`"token"\s*:\s*"([^"]+)"\s*,\s*"key"\s*:\s*"?([^",]+)"?`) + jsonMatch := jsonPattern.FindStringSubmatch(pageContent) + + if len(jsonMatch) >= 3 { + token = jsonMatch[1] + key = jsonMatch[2] + } + } + + // 如果所有模式都失败 + if key == "" || token == "" { + return nil, fmt.Errorf("%w: unable to extract token and key", ErrBingTokenError) + } + + // 查找并提取 IG 参数,尝试多种格式 + var ig string + + // 模式1: 标准IG格式 + igPattern := regexp.MustCompile(`IG["']?\s*:\s*["']([^"']+)["']`) + igMatch := igPattern.FindStringSubmatch(pageContent) + + if len(igMatch) >= 2 { + ig = igMatch[1] + } else { + // 模式2: 备用IG格式 + igPattern = regexp.MustCompile(`"IG"\s*:\s*"([^"]+)"`) + igMatch = igPattern.FindStringSubmatch(pageContent) + + if len(igMatch) >= 2 { + ig = igMatch[1] + } else { + // 模式3: _G.IG格式 + igPattern = regexp.MustCompile(`_G\.IG\s*=\s*["']([^"']+)["']`) + igMatch = igPattern.FindStringSubmatch(pageContent) + + if len(igMatch) >= 2 { + ig = igMatch[1] + } + } + } + + // 如果所有IG提取模式都失败 + if ig == "" { + return nil, fmt.Errorf("%w: unable to extract IG parameter", ErrBingTokenError) + } + + return &BingTranslationParams{ + Token: token, + Key: key, + IG: ig, + }, nil +} + +// GetBingTranslation 获取Bing翻译结果 +func (t *BingTranslator) GetBingTranslation(token, key, ig, text, fromLang, toLang string) (string, error) { + // URL编码文本 + encodedText := url.QueryEscape(text) + + // 构建POST请求的payload + payload := fmt.Sprintf("fromLang=%s&to=%s&text=%s&token=%s&key=%s", + fromLang, toLang, encodedText, token, key) + + // 构建URL + urlStr := fmt.Sprintf("https://%s/ttranslatev3?isVertical=1&IG=%s&IID=translator.5028", t.BingHost, ig) + + // 创建请求 + req, err := http.NewRequest("POST", urlStr, strings.NewReader(payload)) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + + // 设置请求头 + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36") + req.Header.Set("Accept", "*/*") + req.Header.Set("Host", t.BingHost) + req.Header.Set("Connection", "keep-alive") + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Origin", fmt.Sprintf("https://%s", t.BingHost)) + req.Header.Set("Referer", fmt.Sprintf("https://%s/translator", t.BingHost)) + + // 发送请求 + resp, err := t.httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("%w: %v", ErrBingNetworkError, err) + } + defer resp.Body.Close() + + // 判断请求是否成功 + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("API error: status code %d", resp.StatusCode) + } + + // 读取响应体 + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response: %w", err) + } + + // 检查响应内容 + if len(body) == 0 { + return "", fmt.Errorf("translation API returned empty response") + } + + // 使用最简单的结构体解析JSON + var response []struct { + Translations []struct { + Text string `json:"text"` + } `json:"translations"` + } + + if err := json.Unmarshal(body, &response); err != nil { + return "", fmt.Errorf("%w: JSON parsing error: %v", ErrBingParseError, err) + } + + // 检查解析结果 + if len(response) == 0 || len(response[0].Translations) == 0 { + return "", fmt.Errorf("%w: invalid response format", ErrBingParseError) + } + + // 返回翻译结果 + return response[0].Translations[0].Text, nil +} diff --git a/internal/common/translator/deepl_translator.go b/internal/common/translator/deepl_translator.go new file mode 100644 index 0000000..a591c82 --- /dev/null +++ b/internal/common/translator/deepl_translator.go @@ -0,0 +1,318 @@ +// Package translator 提供文本翻译功能 +package translator + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "math/rand" + "net/http" + "strings" + "time" + + "golang.org/x/text/language" +) + +// DeeplTranslator DeepL翻译器结构体 +type DeeplTranslator struct { + DeeplHost string // DeepL服务主机 + httpClient *http.Client // HTTP客户端 + Timeout time.Duration // 请求超时时间 +} + +// 常量定义 +const ( + deeplDefaultTimeout = 30 * time.Second + defaultDeeplHost = "www2.deepl.com" // 默认DeepL API主机 + deeplJsonRpcUrl = "https://www2.deepl.com/jsonrpc" // DeepL JSON-RPC API +) + +// 错误定义 +var ( + ErrDeeplNetworkError = errors.New("deepl translator network error") + ErrDeeplUnsupportedLang = errors.New("deepl translator unsupported language") + ErrDeeplResponseError = errors.New("deepl translator response error") +) + +// 语言映射 +var deeplLangMap = map[string]string{ + "auto": "auto", + "de": "DE", + "en": "EN", + "es": "ES", + "fr": "FR", + "it": "IT", + "ja": "JA", + "ko": "KO", + "nl": "NL", + "pl": "PL", + "pt": "PT", + "ru": "RU", + "zh": "ZH", + "bg": "BG", + "cs": "CS", + "da": "DA", + "el": "EL", + "et": "ET", + "fi": "FI", + "hu": "HU", + "lt": "LT", + "lv": "LV", + "ro": "RO", + "sk": "SK", + "sl": "SL", + "sv": "SV", +} + +// 反向语言映射 +var deeplLangMapReverse = map[string]string{ + "auto": "auto", + "DE": "de", + "EN": "en", + "ES": "es", + "FR": "fr", + "IT": "it", + "JA": "ja", + "KO": "ko", + "NL": "nl", + "PL": "pl", + "PT": "pt", + "RU": "ru", + "ZH": "zh", + "BG": "bg", + "CS": "cs", + "DA": "da", + "EL": "el", + "ET": "et", + "FI": "fi", + "HU": "hu", + "LT": "lt", + "LV": "lv", + "RO": "ro", + "SK": "sk", + "SL": "sl", + "SV": "sv", +} + +// DeeplRequest DeepL请求结构体 +type DeeplRequest struct { + Jsonrpc string `json:"jsonrpc"` + Method string `json:"method"` + ID int64 `json:"id"` + Params DeeplReqParams `json:"params"` +} + +// DeeplReqParams DeepL请求参数结构体 +type DeeplReqParams struct { + Texts []DeeplText `json:"texts"` + Splitting string `json:"splitting"` + Lang DeeplLang `json:"lang"` + Timestamp int64 `json:"timestamp"` +} + +// DeeplText DeepL文本结构体 +type DeeplText struct { + Text string `json:"text"` + RequestAlternatives int `json:"requestAlternatives"` +} + +// DeeplLang DeepL语言结构体 +type DeeplLang struct { + SourceLangUserSelected string `json:"source_lang_user_selected"` + TargetLang string `json:"target_lang"` +} + +// DeeplResponse DeepL响应结构体 +type DeeplResponse struct { + Jsonrpc string `json:"jsonrpc"` + ID int64 `json:"id"` + Result DeeplResult `json:"result"` +} + +// DeeplResult DeepL结果结构体 +type DeeplResult struct { + Texts []DeeplResultText `json:"texts"` + Lang string `json:"lang"` + LangIsConfident bool `json:"lang_is_confident"` + DetectedLanguages map[string]float64 `json:"detectedLanguages"` +} + +// DeeplResultText DeepL结果文本结构体 +type DeeplResultText struct { + Text string `json:"text"` + Alternatives []DeeplAlternative `json:"alternatives,omitempty"` +} + +// DeeplAlternative DeepL替代翻译结构体 +type DeeplAlternative struct { + Text string `json:"text"` +} + +// NewDeeplTranslator 创建一个新的DeepL翻译器实例 +func NewDeeplTranslator() *DeeplTranslator { + translator := &DeeplTranslator{ + DeeplHost: defaultDeeplHost, + Timeout: deeplDefaultTimeout, + httpClient: &http.Client{Timeout: deeplDefaultTimeout}, + } + + return translator +} + +// SetTimeout 设置请求超时时间 +func (t *DeeplTranslator) SetTimeout(timeout time.Duration) { + t.Timeout = timeout + t.httpClient.Timeout = timeout +} + +// SetDeeplHost 设置DeepL主机 +func (t *DeeplTranslator) SetDeeplHost(host string) { + t.DeeplHost = host +} + +// Translate 使用标准语言标签进行文本翻译 +func (t *DeeplTranslator) Translate(text string, from language.Tag, to language.Tag) (string, error) { + return t.translate(text, from.String(), to.String()) +} + +// TranslateWithParams 使用简单字符串参数进行文本翻译 +func (t *DeeplTranslator) TranslateWithParams(text string, params TranslationParams) (string, error) { + tries := params.Tries + if tries == 0 { + tries = defaultNumberOfRetries + } + + var result string + var lastError error + + for i := 0; i < tries; i++ { + if i > 0 && params.Delay > 0 { + time.Sleep(params.Delay) + } + + result, lastError = t.translate(text, params.From, params.To) + if lastError == nil { + return result, nil + } + } + + return "", lastError +} + +// translate 执行实际翻译操作 +func (t *DeeplTranslator) translate(text, from, to string) (string, error) { + // 转换语言代码为DeepL格式 + sourceLang, ok := deeplLangMap[strings.ToLower(from)] + if !ok && from != "auto" { + sourceLang = "auto" + } + + targetLang, ok := deeplLangMap[strings.ToLower(to)] + if !ok { + return "", fmt.Errorf("%w: language '%s' not supported by DeepL", ErrDeeplUnsupportedLang, to) + } + + // 准备请求数据 + id := getRandomNumber() + iCount := getICount(text) + timestamp := getTimeStamp(iCount) + + // 构建请求体 + reqParams := DeeplReqParams{ + Texts: []DeeplText{ + { + Text: text, + RequestAlternatives: 3, + }, + }, + Splitting: "newlines", + Lang: DeeplLang{ + SourceLangUserSelected: sourceLang, + TargetLang: targetLang, + }, + Timestamp: timestamp, + } + + request := DeeplRequest{ + Jsonrpc: "2.0", + Method: "LMT_handle_texts", + ID: id, + Params: reqParams, + } + + // 序列化请求 + jsonData, err := json.Marshal(request) + if err != nil { + return "", fmt.Errorf("failed to marshal request: %w", err) + } + + // 特殊处理method字段格式 + postStr := string(jsonData) + if (id+5)%29 == 0 || (id+3)%13 == 0 { + postStr = strings.Replace(postStr, `"method":"`, `"method" : "`, 1) + } else { + postStr = strings.Replace(postStr, `"method":"`, `"method": "`, 1) + } + + // 发送请求 + req, err := http.NewRequest("POST", deeplJsonRpcUrl, bytes.NewBuffer([]byte(postStr))) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36") + + resp, err := t.httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("%w: %v", ErrDeeplNetworkError, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("%w: status code %d", ErrDeeplNetworkError, resp.StatusCode) + } + + // 读取响应 + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response: %w", err) + } + + // 解析响应 + var deeplResp DeeplResponse + err = json.Unmarshal(body, &deeplResp) + if err != nil { + return "", fmt.Errorf("%w: %v", ErrDeeplResponseError, err) + } + + // 检查是否有有效的结果 + if len(deeplResp.Result.Texts) == 0 { + return "", fmt.Errorf("%w: no translation result", ErrDeeplResponseError) + } + + // 返回翻译结果 + return deeplResp.Result.Texts[0].Text, nil +} + +// getICount 获取文本中'i'字符的数量 +func getICount(text string) int { + return strings.Count(text, "i") +} + +// getRandomNumber 生成随机数 +func getRandomNumber() int64 { + return int64(rand.Intn(99999)+100000) * 1000 +} + +// getTimeStamp 获取时间戳 +func getTimeStamp(iCount int) int64 { + ts := time.Now().UnixMilli() + if iCount != 0 { + iCount++ + return ts - (ts % int64(iCount)) + int64(iCount) + } + return ts +} diff --git a/internal/common/translator/google_translator.go b/internal/common/translator/google_translator.go new file mode 100644 index 0000000..43e20da --- /dev/null +++ b/internal/common/translator/google_translator.go @@ -0,0 +1,311 @@ +// Package translator 提供文本翻译功能 +package translator + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "log" + "math" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" + "time" + + "github.com/robertkrimen/otto" + "golang.org/x/text/language" +) + +// 错误定义 +var ( + ErrBadNetwork = errors.New("bad network, please check your internet connection") +) + +// GoogleTranslator Google翻译器结构体,统一管理翻译功能 +type GoogleTranslator struct { + GoogleHost string // Google服务主机 + vm *otto.Otto // JavaScript虚拟机 + ttk otto.Value // 翻译token缓存 + httpClient *http.Client // HTTP客户端 + Timeout time.Duration // 请求超时时间 +} + +// NewGoogleTranslator 创建一个新的Google翻译器实例 +func NewGoogleTranslator() *GoogleTranslator { + translator := &GoogleTranslator{ + GoogleHost: "google.com", + vm: otto.New(), + Timeout: defaultTimeout, + httpClient: &http.Client{Timeout: defaultTimeout}, + } + + // 初始化ttk + translator.ttk, _ = otto.ToValue("0") + + return translator +} + +// SetTimeout 设置请求超时时间 +func (t *GoogleTranslator) SetTimeout(timeout time.Duration) { + t.Timeout = timeout + t.httpClient.Timeout = timeout +} + +// SetGoogleHost 设置Google主机 +func (t *GoogleTranslator) SetGoogleHost(host string) { + t.GoogleHost = host +} + +// Translate 使用Go语言提供的标准语言标签进行文本翻译 +func (t *GoogleTranslator) Translate(text string, from language.Tag, to language.Tag) (string, error) { + return t.translate(text, from.String(), to.String(), false, defaultNumberOfRetries, 0) +} + +// TranslateWithParams 使用简单字符串参数进行文本翻译 +func (t *GoogleTranslator) TranslateWithParams(text string, params TranslationParams) (string, error) { + tries := params.Tries + if tries == 0 { + tries = defaultNumberOfRetries + } + + return t.translate(text, params.From, params.To, true, tries, params.Delay) +} + +// translate 执行实际翻译操作 +func (t *GoogleTranslator) translate(text, from, to string, withVerification bool, tries int, delay time.Duration) (string, error) { + if tries == 0 { + tries = defaultNumberOfRetries + } + + if withVerification { + if _, err := language.Parse(from); err != nil && from != "auto" { + log.Println("[WARNING], '" + from + "' is a invalid language, switching to 'auto'") + from = "auto" + } + if _, err := language.Parse(to); err != nil { + log.Println("[WARNING], '" + to + "' is a invalid language, switching to 'en'") + to = "en" + } + } + + textValue, _ := otto.ToValue(text) + urlStr := fmt.Sprintf("https://translate.%s/translate_a/single", t.GoogleHost) + token := t.getToken(textValue) + + data := map[string]string{ + "client": "gtx", + "sl": from, + "tl": to, + "hl": to, + "ie": "UTF-8", + "oe": "UTF-8", + "otf": "1", + "ssel": "0", + "tsel": "0", + "kc": "7", + "q": text, + } + + u, err := url.Parse(urlStr) + if err != nil { + return "", err + } + + parameters := url.Values{} + for k, v := range data { + parameters.Add(k, v) + } + for _, v := range []string{"at", "bd", "ex", "ld", "md", "qca", "rw", "rm", "ss", "t"} { + parameters.Add("dt", v) + } + + parameters.Add("tk", token) + u.RawQuery = parameters.Encode() + + var r *http.Response + for tries > 0 { + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return "", err + } + + r, err = t.httpClient.Do(req) + if err != nil { + if errors.Is(err, http.ErrHandlerTimeout) { + return "", ErrBadNetwork + } + return "", err + } + + if r.StatusCode == http.StatusOK { + break + } + + if r.StatusCode == http.StatusForbidden { + tries-- + time.Sleep(delay) + } + } + + raw, err := io.ReadAll(r.Body) + if err != nil { + return "", err + } + defer r.Body.Close() + + var resp []interface{} + err = json.Unmarshal(raw, &resp) + if err != nil { + return "", err + } + + responseText := "" + for _, obj := range resp[0].([]interface{}) { + if len(obj.([]interface{})) == 0 { + break + } + + t, ok := obj.([]interface{})[0].(string) + if ok { + responseText += t + } + } + + return responseText, nil +} + +// getToken 获取翻译API所需的token +func (t *GoogleTranslator) getToken(text otto.Value) string { + ttk, err := t.updateTTK() + if err != nil { + return "" + } + + tk, err := t.generateToken(text, ttk) + if err != nil { + return "" + } + + return strings.Replace(tk.String(), "&tk=", "", -1) +} + +// updateTTK 更新TTK值 +func (t *GoogleTranslator) updateTTK() (otto.Value, error) { + timestamp := time.Now().UnixNano() / 3600000 + now := math.Floor(float64(timestamp)) + ttk, err := strconv.ParseFloat(t.ttk.String(), 64) + if err != nil { + return otto.UndefinedValue(), err + } + + if ttk == now { + return t.ttk, nil + } + + req, err := http.NewRequest("GET", fmt.Sprintf("https://translate.%s", t.GoogleHost), nil) + if err != nil { + return otto.UndefinedValue(), err + } + + resp, err := t.httpClient.Do(req) + if err != nil { + return otto.UndefinedValue(), err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return otto.UndefinedValue(), err + } + + matches := regexp.MustCompile(`tkk:\s?'(.+?)'`).FindStringSubmatch(string(body)) + if len(matches) > 0 { + v, err := otto.ToValue(matches[0]) + if err != nil { + return otto.UndefinedValue(), err + } + t.ttk = v + return v, nil + } + + return t.ttk, nil +} + +// generateToken 生成翻译API所需的token +func (t *GoogleTranslator) generateToken(a otto.Value, TTK otto.Value) (otto.Value, error) { + err := t.vm.Set("x", a) + if err != nil { + return otto.UndefinedValue(), err + } + + _ = t.vm.Set("internalTTK", TTK) + + result, err := t.vm.Run(` + function sM(a) { + var b; + if (null !== yr) + b = yr; + else { + b = wr(String.fromCharCode(84)); + var c = wr(String.fromCharCode(75)); + b = [b(), b()]; + b[1] = c(); + b = (yr = window[b.join(c())] || "") || "" + } + var d = wr(String.fromCharCode(116)) + , c = wr(String.fromCharCode(107)) + , d = [d(), d()]; + d[1] = c(); + c = "&" + d.join("") + "="; + d = b.split("."); + b = Number(d[0]) || 0; + for (var e = [], f = 0, g = 0; g < a.length; g++) { + var l = a.charCodeAt(g); + 128 > l ? e[f++] = l : (2048 > l ? e[f++] = l >> 6 | 192 : (55296 == (l & 64512) && g + 1 < a.length && 56320 == (a.charCodeAt(g + 1) & 64512) ? (l = 65536 + ((l & 1023) << 10) + (a.charCodeAt(++g) & 1023), + e[f++] = l >> 18 | 240, + e[f++] = l >> 12 & 63 | 128) : e[f++] = l >> 12 | 224, + e[f++] = l >> 6 & 63 | 128), + e[f++] = l & 63 | 128) + } + a = b; + for (f = 0; f < e.length; f++) + a += e[f], + a = xr(a, "+-a^+6"); + a = xr(a, "+-3^+b+-f"); + a ^= Number(d[1]) || 0; + 0 > a && (a = (a & 2147483647) + 2147483648); + a %= 1E6; + return c + (a.toString() + "." + (a ^ b)) + } + + var yr = null; + var wr = function(a) { + return function() { + return a + } + } + , xr = function(a, b) { + for (var c = 0; c < b.length - 2; c += 3) { + var d = b.charAt(c + 2) + , d = "a" <= d ? d.charCodeAt(0) - 87 : Number(d) + , d = "+" == b.charAt(c + 1) ? a >>> d : a << d; + a = "+" == b.charAt(c) ? a + d & 4294967295 : a ^ d + } + return a + }; + + var window = { + TKK: internalTTK + }; + + sM(x) + `) + if err != nil { + return otto.UndefinedValue(), err + } + + return result, nil +} diff --git a/internal/common/translator/translator.go b/internal/common/translator/translator.go new file mode 100644 index 0000000..2c2594b --- /dev/null +++ b/internal/common/translator/translator.go @@ -0,0 +1,73 @@ +// Package translator 提供文本翻译功能 +package translator + +import ( + "fmt" + "time" + + "golang.org/x/text/language" +) + +// TranslationParams 用于指定翻译参数 +type TranslationParams struct { + From string // 源语言 + To string // 目标语言 + Tries int // 重试次数 + Delay time.Duration // 重试延迟 +} + +// 常量定义 +const ( + defaultNumberOfRetries = 2 + defaultTimeout = 30 * time.Second +) + +// TranslatorType 翻译器类型 +type TranslatorType string + +const ( + // GoogleTranslatorType 谷歌翻译器 + GoogleTranslatorType TranslatorType = "google" + // BingTranslatorType 必应翻译器 + BingTranslatorType TranslatorType = "bing" + // YoudaoTranslatorType 有道翻译器 + YoudaoTranslatorType TranslatorType = "youdao" + // DeeplTranslatorType DeepL翻译器 + DeeplTranslatorType TranslatorType = "deepl" +) + +// Translator 翻译器接口,定义所有翻译器必须实现的方法 +type Translator interface { + // Translate 使用Go语言提供的标准语言标签进行文本翻译 + Translate(text string, from language.Tag, to language.Tag) (string, error) + + // TranslateWithParams 使用简单字符串参数进行文本翻译 + TranslateWithParams(text string, params TranslationParams) (string, error) + + // SetTimeout 设置请求超时时间 + SetTimeout(timeout time.Duration) +} + +// TranslatorFactory 翻译器工厂,用于创建不同类型的翻译器 +type TranslatorFactory struct{} + +// NewTranslatorFactory 创建一个新的翻译器工厂 +func NewTranslatorFactory() *TranslatorFactory { + return &TranslatorFactory{} +} + +// Create 根据类型创建翻译器 +func (f *TranslatorFactory) Create(translatorType TranslatorType) (Translator, error) { + switch translatorType { + case GoogleTranslatorType: + return NewGoogleTranslator(), nil + case BingTranslatorType: + return NewBingTranslator(), nil + case YoudaoTranslatorType: + return NewYoudaoTranslator(), nil + case DeeplTranslatorType: + return NewDeeplTranslator(), nil + default: + return nil, fmt.Errorf("unsupported translator type: %s", translatorType) + } +} diff --git a/internal/common/translator/youdao_translator.go b/internal/common/translator/youdao_translator.go new file mode 100644 index 0000000..a8a291a --- /dev/null +++ b/internal/common/translator/youdao_translator.go @@ -0,0 +1,186 @@ +// Package translator 提供文本翻译功能 +package translator + +import ( + "errors" + "fmt" + "io" + "net/http" + "net/url" + "regexp" + "strings" + "time" + + "golang.org/x/net/html" + "golang.org/x/text/language" +) + +// YoudaoTranslator 有道翻译器结构体 +type YoudaoTranslator struct { + httpClient *http.Client // HTTP客户端 + Timeout time.Duration // 请求超时时间 +} + +// 常量定义 +const ( + youdaoDefaultTimeout = 30 * time.Second + youdaoTranslateURL = "https://m.youdao.com/translate" +) + +// 错误定义 +var ( + ErrYoudaoNetworkError = errors.New("youdao translator network error") + ErrYoudaoParseError = errors.New("youdao translator parse error") +) + +// NewYoudaoTranslator 创建一个新的有道翻译器实例 +func NewYoudaoTranslator() *YoudaoTranslator { + translator := &YoudaoTranslator{ + Timeout: youdaoDefaultTimeout, + httpClient: &http.Client{Timeout: youdaoDefaultTimeout}, + } + + return translator +} + +// SetTimeout 设置请求超时时间 +func (t *YoudaoTranslator) SetTimeout(timeout time.Duration) { + t.Timeout = timeout + t.httpClient.Timeout = timeout +} + +// Translate 使用标准语言标签进行文本翻译 +func (t *YoudaoTranslator) Translate(text string, from language.Tag, to language.Tag) (string, error) { + // 有道翻译不需要指定源语言和目标语言,它会自动检测 + return t.translate(text) +} + +// TranslateWithParams 使用简单字符串参数进行文本翻译 +func (t *YoudaoTranslator) TranslateWithParams(text string, params TranslationParams) (string, error) { + // 有道翻译不需要指定源语言和目标语言,它会自动检测 + return t.translate(text) +} + +// translate 执行实际翻译操作 +func (t *YoudaoTranslator) translate(text string) (string, error) { + // 构建表单数据 + form := url.Values{} + form.Add("inputtext", text) + form.Add("type", "AUTO") + + // 创建请求 + req, err := http.NewRequest("POST", youdaoTranslateURL, strings.NewReader(form.Encode())) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + + // 设置请求头 + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36") + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9") + req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") + + // 发送请求 + resp, err := t.httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("%w: %v", ErrYoudaoNetworkError, err) + } + defer resp.Body.Close() + + // 判断请求是否成功 + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("API error: status code %d", resp.StatusCode) + } + + // 读取响应体 + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response: %w", err) + } + + // 解析HTML响应 + result, err := t.extractTranslationResult(string(body)) + if err != nil { + return "", err + } + + return result, nil +} + +// extractTranslationResult 从HTML响应中提取翻译结果 +func (t *YoudaoTranslator) extractTranslationResult(htmlContent string) (string, error) { + // 方法1:使用正则表达式提取翻译结果 + pattern := regexp.MustCompile(`
    ]*>.*?]*>(.*?)`) + matches := pattern.FindStringSubmatch(htmlContent) + + if len(matches) >= 2 { + // 清理HTML标签 + result := matches[1] + result = strings.ReplaceAll(result, "
    ", "\n") + result = t.stripHTMLTags(result) + return result, nil + } + + // 方法2:使用HTML解析器提取翻译结果 + doc, err := html.Parse(strings.NewReader(htmlContent)) + if err != nil { + return "", fmt.Errorf("%w: failed to parse HTML", ErrYoudaoParseError) + } + + // 查找翻译结果元素 + result := t.findTranslateResult(doc) + if result != "" { + return result, nil + } + + return "", fmt.Errorf("%w: could not find translation result", ErrYoudaoParseError) +} + +// stripHTMLTags 移除HTML标签 +func (t *YoudaoTranslator) stripHTMLTags(input string) string { + // 简单的HTML标签移除 + re := regexp.MustCompile("<[^>]*>") + return re.ReplaceAllString(input, "") +} + +// findTranslateResult 在HTML文档中查找翻译结果 +func (t *YoudaoTranslator) findTranslateResult(n *html.Node) string { + if n.Type == html.ElementNode && n.Data == "ul" { + for _, attr := range n.Attr { + if attr.Key == "id" && attr.Val == "translateResult" { + // 找到了translateResult元素,提取其中的文本 + var result string + for c := n.FirstChild; c != nil; c = c.NextSibling { + if c.Type == html.ElementNode && c.Data == "li" { + return t.extractText(c) + } + } + return result + } + } + } + + // 递归查找子节点 + for c := n.FirstChild; c != nil; c = c.NextSibling { + result := t.findTranslateResult(c) + if result != "" { + return result + } + } + + return "" +} + +// extractText 提取节点中的文本内容 +func (t *YoudaoTranslator) extractText(n *html.Node) string { + if n.Type == html.TextNode { + return n.Data + } + + var result string + for c := n.FirstChild; c != nil; c = c.NextSibling { + result += t.extractText(c) + } + + return result +} diff --git a/internal/services/service_manager.go b/internal/services/service_manager.go index 45fe7ba..1311293 100644 --- a/internal/services/service_manager.go +++ b/internal/services/service_manager.go @@ -9,19 +9,20 @@ import ( // ServiceManager 服务管理器,负责协调各个服务 type ServiceManager struct { - pathManager *PathManager - configService *ConfigService - documentService *DocumentService - migrationService *MigrationService - systemService *SystemService - hotkeyService *HotkeyService - dialogService *DialogService - trayService *TrayService - keyBindingService *KeyBindingService - extensionService *ExtensionService - startupService *StartupService - selfUpdateService *SelfUpdateService - logger *log.LoggerService + pathManager *PathManager + configService *ConfigService + documentService *DocumentService + migrationService *MigrationService + systemService *SystemService + hotkeyService *HotkeyService + dialogService *DialogService + trayService *TrayService + keyBindingService *KeyBindingService + extensionService *ExtensionService + startupService *StartupService + selfUpdateService *SelfUpdateService + translationService *TranslationService + logger *log.LoggerService } // NewServiceManager 创建新的服务管理器实例 @@ -68,6 +69,9 @@ func NewServiceManager() *ServiceManager { panic(err) } + // 初始化翻译服务 + translationService := NewTranslationService(logger) + // 使用新的配置通知系统设置热键配置变更监听 err = configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error { return hotkeyService.UpdateHotkey(enable, hotkey) @@ -85,18 +89,19 @@ func NewServiceManager() *ServiceManager { } return &ServiceManager{ - configService: configService, - documentService: documentService, - migrationService: migrationService, - systemService: systemService, - hotkeyService: hotkeyService, - dialogService: dialogService, - trayService: trayService, - keyBindingService: keyBindingService, - extensionService: extensionService, - startupService: startupService, - selfUpdateService: selfUpdateService, - logger: logger, + configService: configService, + documentService: documentService, + migrationService: migrationService, + systemService: systemService, + hotkeyService: hotkeyService, + dialogService: dialogService, + trayService: trayService, + keyBindingService: keyBindingService, + extensionService: extensionService, + startupService: startupService, + selfUpdateService: selfUpdateService, + translationService: translationService, + logger: logger, } } @@ -114,6 +119,7 @@ func (sm *ServiceManager) GetServices() []application.Service { application.NewService(sm.extensionService), application.NewService(sm.startupService), application.NewService(sm.selfUpdateService), + application.NewService(sm.translationService), } return services } @@ -162,3 +168,8 @@ func (sm *ServiceManager) GetExtensionService() *ExtensionService { func (sm *ServiceManager) GetSelfUpdateService() *SelfUpdateService { return sm.selfUpdateService } + +// GetTranslationService 获取翻译服务实例 +func (sm *ServiceManager) GetTranslationService() *TranslationService { + return sm.translationService +} diff --git a/internal/services/translation_service.go b/internal/services/translation_service.go new file mode 100644 index 0000000..6543fdd --- /dev/null +++ b/internal/services/translation_service.go @@ -0,0 +1,253 @@ +package services + +import ( + "sync" + "time" + "voidraft/internal/common/translator" + + "github.com/wailsapp/wails/v3/pkg/services/log" + "golang.org/x/text/language" +) + +// TranslationService 翻译服务 +type TranslationService struct { + logger *log.LoggerService + factory *translator.TranslatorFactory + defaultTimeout time.Duration + activeTranslator translator.TranslatorType + translators map[translator.TranslatorType]translator.Translator + mutex sync.RWMutex +} + +// NewTranslationService 创建翻译服务实例 +func NewTranslationService(logger *log.LoggerService) *TranslationService { + factory := translator.NewTranslatorFactory() + defaultTimeout := 10 * time.Second + + // 默认使用bin翻译 + activeType := translator.BingTranslatorType + + // 预初始化所有翻译器 + translators := make(map[translator.TranslatorType]translator.Translator) + + service := &TranslationService{ + logger: logger, + factory: factory, + defaultTimeout: defaultTimeout, + activeTranslator: activeType, + translators: translators, + } + + // 延迟初始化翻译器以提高启动速度 + go service.initTranslators() + + return service +} + +// initTranslators 初始化所有翻译器 +func (s *TranslationService) initTranslators() { + types := []translator.TranslatorType{ + translator.GoogleTranslatorType, + translator.BingTranslatorType, + translator.YoudaoTranslatorType, + translator.DeeplTranslatorType, + } + + for _, t := range types { + trans, err := s.factory.Create(t) + if err != nil { + s.logger.Error("Failed to create translator: %v", err) + continue + } + + trans.SetTimeout(s.defaultTimeout) + + s.mutex.Lock() + s.translators[t] = trans + s.mutex.Unlock() + } +} + +// getTranslator 获取指定类型的翻译器 +func (s *TranslationService) getTranslator(translatorType translator.TranslatorType) (translator.Translator, error) { + s.mutex.RLock() + trans, exists := s.translators[translatorType] + s.mutex.RUnlock() + + if exists { + return trans, nil + } + + // 如果翻译器尚未初始化,则立即创建 + trans, err := s.factory.Create(translatorType) + if err != nil { + return nil, err + } + + trans.SetTimeout(s.defaultTimeout) + + s.mutex.Lock() + s.translators[translatorType] = trans + s.mutex.Unlock() + + return trans, nil +} + +// Translate 使用当前活跃翻译器进行翻译 +// @param {string} text - 待翻译文本 +// @param {string} from - 源语言代码 (如 "en", "zh", "auto") +// @param {string} to - 目标语言代码 (如 "en", "zh") +// @returns {string} 翻译后的文本 +// @returns {error} 可能的错误 +func (s *TranslationService) Translate(text string, from string, to string) (string, error) { + // 解析语言标签 + var fromLang, toLang language.Tag + var err error + + if from == "auto" { + fromLang = language.Und // 未定义,表示自动检测 + } else { + fromLang, err = language.Parse(from) + if err != nil { + return "", err + } + } + + toLang, err = language.Parse(to) + if err != nil { + return "", err + } + + // 获取活跃翻译器 + s.mutex.RLock() + activeType := s.activeTranslator + s.mutex.RUnlock() + + trans, err := s.getTranslator(activeType) + if err != nil { + return "", err + } + + // 执行翻译 + return trans.Translate(text, fromLang, toLang) +} + +// TranslateWithFallback 尝试使用当前活跃翻译器翻译,如果失败则尝试备用翻译器 +// @param {string} text - 待翻译文本 +// @param {string} from - 源语言代码 (如 "en", "zh", "auto") +// @param {string} to - 目标语言代码 (如 "en", "zh") +// @returns {string} 翻译后的文本 +// @returns {string} 使用的翻译器类型 +// @returns {error} 可能的错误 +func (s *TranslationService) TranslateWithFallback(text string, from string, to string) (string, string, error) { + // 首先尝试活跃翻译器 + s.mutex.RLock() + primaryType := s.activeTranslator + s.mutex.RUnlock() + + result, err := s.TranslateWith(text, from, to, string(primaryType)) + if err == nil { + return result, string(primaryType), nil + } + + // 备用翻译器列表 + fallbacks := []translator.TranslatorType{ + translator.GoogleTranslatorType, + translator.BingTranslatorType, + translator.DeeplTranslatorType, + translator.YoudaoTranslatorType, + } + + // 尝试备用翻译器 + for _, fallbackType := range fallbacks { + if fallbackType == primaryType { + continue // 跳过已尝试的主要翻译器 + } + + result, err := s.TranslateWith(text, from, to, string(fallbackType)) + if err == nil { + return result, string(fallbackType), nil + } + } + + return "", "", err // 所有翻译器都失败时返回最后一个错误 +} + +// TranslateWith 使用指定翻译器进行翻译 +// @param {string} text - 待翻译文本 +// @param {string} from - 源语言代码 (如 "en", "zh", "auto") +// @param {string} to - 目标语言代码 (如 "en", "zh") +// @param {string} translatorType - 翻译器类型 ("google", "bing", "youdao", "deepl") +// @returns {string} 翻译后的文本 +// @returns {error} 可能的错误 +func (s *TranslationService) TranslateWith(text string, from string, to string, translatorType string) (string, error) { + // 参数验证 + if text == "" { + return "", nil // 空文本无需翻译 + } + + // 转换为翻译器类型 + transType := translator.TranslatorType(translatorType) + + // 获取指定翻译器 + trans, err := s.getTranslator(transType) + if err != nil { + return "", err + } + + // 创建翻译参数 + params := translator.TranslationParams{ + From: from, + To: to, + Tries: 2, + Delay: 500 * time.Millisecond, + } + + // 执行翻译 + return trans.TranslateWithParams(text, params) +} + +// SetActiveTranslator 设置活跃翻译器 +// @param {string} translatorType - 翻译器类型 ("google", "bing", "youdao", "deepl") +// @returns {error} 可能的错误 +func (s *TranslationService) SetActiveTranslator(translatorType string) error { + transType := translator.TranslatorType(translatorType) + + // 验证翻译器类型 + _, err := s.factory.Create(transType) + if err != nil { + return err + } + + s.mutex.Lock() + s.activeTranslator = transType + s.mutex.Unlock() + + return nil +} + +// GetAvailableTranslators 获取所有可用翻译器类型 +// @returns {[]string} 翻译器类型列表 +func (s *TranslationService) GetAvailableTranslators() []string { + return []string{ + string(translator.GoogleTranslatorType), + string(translator.BingTranslatorType), + string(translator.YoudaoTranslatorType), + string(translator.DeeplTranslatorType), + } +} + +// SetTimeout 设置翻译超时时间 +// @param {int} seconds - 超时秒数 +func (s *TranslationService) SetTimeout(seconds int) { + timeout := time.Duration(seconds) * time.Second + + s.mutex.Lock() + s.defaultTimeout = timeout + s.mutex.Unlock() + + // 更新所有现有翻译器的超时设置 + for _, t := range s.translators { + t.SetTimeout(timeout) + } +}