From ad168a417b2a8edd7ae29cf46ad60a49ae5d04b2 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 23 May 2026 17:46:43 +0900 Subject: [PATCH 01/29] fix 01 --- src/pkg/utils/monaco-editor/index.ts | 391 ++++++++++++++++++--------- src/pkg/utils/monaco-editor/langs.ts | 14 + 2 files changed, 279 insertions(+), 126 deletions(-) diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index 1d0886471..9974e52fd 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -1,6 +1,6 @@ import { systemConfig } from "@App/pages/store/global"; import EventEmitter from "eventemitter3"; -import { languages } from "monaco-editor"; +import { editor, languages, MarkerSeverity } from "monaco-editor"; import { findGlobalInsertionInfo, updateGlobalCommentLine } from "./utils"; import type { EditorLangCode, EditorPrompt } from "./langs"; import { asEditorLangEntry, editorLangs } from "./langs"; @@ -53,6 +53,98 @@ export class LinterWorkerController { let isRegisterEditorDone = false; +const scriptcatMarkerOwner = "ScriptCat"; + +const isSimpleValidHost = (hostName: string) => { + let ret = false; + try { + ret = hostName.length > 0 && new URL(`https://${hostName}.com/path`).origin === `https://${hostName}.com`; + } catch { + // ignored + } + return ret; +}; + +const getMetadataLineFixes = (line: string) => { + const match = /^(\s*\/\/[ \t]*@)(connect|match)([ \t]+)(\S+)(.*)$/i.exec(line); + if (!match) return []; + + const [, prefix, tag, spacing, value, suffix] = match; + if (tag === "connect" && value.length > 2 && value.startsWith("*.")) { + const hostName = value.slice(2); + if (/\.\w{2,}$/.test(hostName) && isSimpleValidHost(hostName)) { + return [ + { + title: multiLang.replaceConnectWildcard.replace("{0}", hostName), + text: `${prefix}${tag}${spacing}${hostName}${suffix}`, + }, + ]; + } + } + + if (tag === "match") { + const matchPattern = /^(\*|[-a-z]+|http\*):\/\/([^/]+)(\/.*)?$/i.exec(value); + const host = matchPattern?.[2]; + if (host && host.endsWith(".*")) { + const hostName = host.slice(0, -2); + if (isSimpleValidHost(hostName)) { + const lenDiff = "include".length - tag.length; + let s = spacing; + if (lenDiff > 0 && s.length > lenDiff) s = s.slice(0, -lenDiff); + const tldValue = `${matchPattern[1]}://${hostName}.tld${matchPattern[3] || ""}`; + return [ + { + title: multiLang.replaceMatchWildcard.replace("{0}", value), + text: `${prefix}include${s}${value}${suffix}`, + }, + { + title: multiLang.replaceMatchWildcard.replace("{0}", tldValue), + text: `${prefix}include${s}${tldValue}${suffix}`, + }, + ]; + } + } + } + + return []; +}; + +const updateScriptcatMetadataMarkers = (model: editor.ITextModel) => { + if (model.getLanguageId() !== "javascript") return; + + const markers: editor.IMarkerData[] = []; + const lineCount = model.getLineCount(); + for (let lineNumber = 1; lineNumber <= lineCount; lineNumber += 1) { + const line = model.getLineContent(lineNumber); + const metadataLineFixes = getMetadataLineFixes(line); + if (metadataLineFixes.length === 0) continue; + + markers.push({ + severity: MarkerSeverity.Warning, + message: metadataLineFixes[0].title, + source: scriptcatMarkerOwner, + startLineNumber: lineNumber, + startColumn: 1, + endLineNumber: lineNumber, + endColumn: line.length + 1, + }); + } + + editor.setModelMarkers(model, scriptcatMarkerOwner, markers); +}; + +const registerScriptcatMetadataMarkerProvider = () => { + const registerModel = (model: editor.ITextModel) => { + updateScriptcatMetadataMarkers(model); + model.onDidChangeContent(() => { + updateScriptcatMetadataMarkers(model); + }); + }; + + editor.getModels().forEach(registerModel); + editor.onDidCreateModel(registerModel); +}; + /** * 注册 monaco-editor 的全局环境与语言支援 * 应该在应用启动早期执行一次(例如在 App 根组件 mount 时) @@ -93,6 +185,8 @@ export function registerEditor() { // provider 注册始终执行,不受 worker 复用影响 const META_LINE = /\/\/[ \t]*@(\S+)[ \t]*(.*)$/; + registerScriptcatMetadataMarkerProvider(); + languages.registerHoverProvider("javascript", { provideHover: (model, position) => { return new Promise((resolve) => { @@ -120,22 +214,127 @@ export function registerEditor() { }, }); - languages.registerCodeActionProvider("javascript", { - provideCodeActions: (model /** ITextModel */, range /** Range */, context /** CodeActionContext */) => { - const actions: languages.CodeAction[] = []; - const eslintFixMap = >(window.MonacoEnvironment as any)?.eslintFixMap; + languages.registerCodeActionProvider( + "javascript", + { + provideCodeActions: (model /** ITextModel */, range /** Range */, context /** CodeActionContext */) => { + const actions: languages.CodeAction[] = []; + const eslintFixMap = >(window.MonacoEnvironment as any)?.eslintFixMap; + const metadataLineFixes = getMetadataLineFixes(model.getLineContent(range.startLineNumber)); + const scriptcatDiagnostics = context.markers.filter( + (marker) => marker.source === scriptcatMarkerOwner && marker.startLineNumber === range.startLineNumber + ); + + if (metadataLineFixes.length > 0) { + const line = model.getLineContent(range.startLineNumber); + metadataLineFixes.forEach((metadataLineFix, index) => + actions.push({ + title: metadataLineFix.title, + diagnostics: scriptcatDiagnostics, + kind: "quickfix", + edit: { + edits: [ + { + resource: model.uri, + textEdit: { + range: { + startLineNumber: range.startLineNumber, + startColumn: 1, + endLineNumber: range.startLineNumber, + endColumn: line.length + 1, + }, + text: metadataLineFix.text, + }, + versionId: undefined, + }, + ], + }, + isPreferred: index === 0, + } satisfies languages.CodeAction) + ); + } + + for (let i = 0; i < context.markers.length; i++) { + // 判断有没有修复方案 + const val = context.markers[i]; + if (!val.code) continue; + const code = typeof val.code === "string" ? val.code : val.code!.value; + + // 1. eslint-fix + const baseKey = `${code}|${val.startLineNumber}|${val.endLineNumber}|${val.startColumn}|${val.endColumn}`; + const fix = eslintFixMap?.get(baseKey); + if (fix) { + actions.push({ + title: multiLang.quickfix.replace("{0}", code), + diagnostics: [val], + kind: "quickfix", + edit: { + edits: [ + { + resource: model.uri, + textEdit: { + range: fix.range, + text: fix.text, + }, + versionId: undefined, + }, + ], + }, + isPreferred: true, + } satisfies languages.CodeAction); + } + + // 2. no-undef → /* global */ + if (code === "no-undef") { + const message = val.message || ""; + const match = message.match(/^[^']*'([^']+)'[^']*$/); + const globalName = match?.[1]; - for (let i = 0; i < context.markers.length; i++) { - // 判断有没有修复方案 - const val = context.markers[i]; - const code = typeof val.code === "string" ? val.code : val.code!.value; + if (globalName) { + const { insertLine, globalLine } = findGlobalInsertionInfo(model); + let textEdit: languages.IWorkspaceTextEdit["textEdit"]; - // 1. eslint-fix - const baseKey = `${code}|${val.startLineNumber}|${val.endLineNumber}|${val.startColumn}|${val.endColumn}`; - const fix = eslintFixMap?.get(baseKey); - if (fix) { + if (globalLine != null) { + // there is already a /* global ... */ line → update it + const oldLine = model.getLineContent(globalLine); + const newLine = updateGlobalCommentLine(oldLine, globalName); + textEdit = { + range: { + startLineNumber: globalLine, + startColumn: 1, + endLineNumber: globalLine, + endColumn: oldLine.length + 1, + }, + text: newLine, + }; + } else { + // no global line yet → insert a new one + textEdit = { + range: { + startLineNumber: insertLine, + startColumn: 1, + endLineNumber: insertLine, + endColumn: 1, + }, + text: `/* global ${globalName} */\n`, + }; + } + + actions.push({ + title: multiLang.declareGlobal.replace("{0}", globalName), + diagnostics: [val], + kind: "quickfix", + edit: { + edits: [{ resource: model.uri, textEdit, versionId: undefined }], + }, + isPreferred: false, + } satisfies languages.CodeAction); + } + } + + // 3. disable-next-line / disable actions.push({ - title: multiLang.quickfix.replace("{0}", code), + title: multiLang.addEslintDisableNextLine, diagnostics: [val], kind: "quickfix", edit: { @@ -143,8 +342,13 @@ export function registerEditor() { { resource: model.uri, textEdit: { - range: fix.range, - text: fix.text, + range: { + startLineNumber: val.startLineNumber, + endLineNumber: val.startLineNumber, + startColumn: 1, + endColumn: 1, + }, + text: `// eslint-disable-next-line ${code}\n`, }, versionId: undefined, }, @@ -152,122 +356,57 @@ export function registerEditor() { }, isPreferred: true, } satisfies languages.CodeAction); - } - // 2. no-undef → /* global */ - if (code === "no-undef") { - const message = val.message || ""; - const match = message.match(/^[^']*'([^']+)'[^']*$/); - const globalName = match?.[1]; - - if (globalName) { - const { insertLine, globalLine } = findGlobalInsertionInfo(model); - let textEdit: languages.IWorkspaceTextEdit["textEdit"]; - - if (globalLine != null) { - // there is already a /* global ... */ line → update it - const oldLine = model.getLineContent(globalLine); - const newLine = updateGlobalCommentLine(oldLine, globalName); - textEdit = { - range: { - startLineNumber: globalLine, - startColumn: 1, - endLineNumber: globalLine, - endColumn: oldLine.length + 1, - }, - text: newLine, - }; - } else { - // no global line yet → insert a new one - textEdit = { - range: { - startLineNumber: insertLine, - startColumn: 1, - endLineNumber: insertLine, - endColumn: 1, + actions.push({ + title: multiLang.addEslintDisable, + diagnostics: [val], + kind: "quickfix", + edit: { + edits: [ + { + resource: model.uri, + textEdit: { + range: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 1, + }, + text: `/* eslint-disable ${code} */\n`, + }, + versionId: undefined, }, - text: `/* global ${globalName} */\n`, - }; - } - - actions.push({ - title: multiLang.declareGlobal.replace("{0}", globalName), - diagnostics: [val], - kind: "quickfix", - edit: { edits: [{ resource: model.uri, textEdit, versionId: undefined }] }, - isPreferred: false, - } satisfies languages.CodeAction); - } + ], + }, + isPreferred: true, + } satisfies languages.CodeAction); } - // 3. disable-next-line / disable - actions.push({ - title: multiLang.addEslintDisableNextLine, - diagnostics: [val], - kind: "quickfix", - edit: { - edits: [ - { - resource: model.uri, - textEdit: { - range: { - startLineNumber: val.startLineNumber, - endLineNumber: val.startLineNumber, - startColumn: 1, - endColumn: 1, - }, - text: `// eslint-disable-next-line ${code}\n`, - }, - versionId: undefined, - }, - ], - }, - isPreferred: true, - } satisfies languages.CodeAction); - - actions.push({ - title: multiLang.addEslintDisable, - diagnostics: [val], - kind: "quickfix", - edit: { - edits: [ - { - resource: model.uri, - textEdit: { - range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, - text: `/* eslint-disable ${code} */\n`, - }, - versionId: undefined, - }, - ], - }, - isPreferred: true, - } satisfies languages.CodeAction); - } + // const actions = context.markers.map((error) => { + // const edit: languages.IWorkspaceTextEdit = { + // resource: model.uri, + // textEdit: { + // range, + // text: "console.log(1)", + // }, + // versionId: undefined, + // }; + // return { + // title: ``, + // diagnostics: [error], + // kind: "quickfix", + // edit: { + // edits: [edit], + // }, + // isPreferred: true, + // }; + // }); - // const actions = context.markers.map((error) => { - // const edit: languages.IWorkspaceTextEdit = { - // resource: model.uri, - // textEdit: { - // range, - // text: "console.log(1)", - // }, - // versionId: undefined, - // }; - // return { - // title: ``, - // diagnostics: [error], - // kind: "quickfix", - // edit: { - // edits: [edit], - // }, - // isPreferred: true, - // }; - // }); - - return { actions, dispose: () => {} }; + return { actions, dispose: () => {} }; + }, }, - }); + { providedCodeActionKinds: ["quickfix"] } + ); // 设定编译器选项与额外类型定义 Promise.all([systemConfig.getEditorConfig(), systemConfig.getEditorTypeDefinition()]).then( diff --git a/src/pkg/utils/monaco-editor/langs.ts b/src/pkg/utils/monaco-editor/langs.ts index 42507baee..4fb99b289 100644 --- a/src/pkg/utils/monaco-editor/langs.ts +++ b/src/pkg/utils/monaco-editor/langs.ts @@ -7,6 +7,8 @@ export const editorLangs = { addEslintDisableNextLine: "添加 eslint-disable-next-line 注释", addEslintDisable: "添加 eslint-disable 注释", declareGlobal: "将 '{0}' 声明为全局变量 (/* global */)", + replaceConnectWildcard: "替换为 @connect {0}", + replaceMatchWildcard: "替换为 @include {0}", prompt: { name: "脚本名称", namespace: "脚本命名空间", @@ -83,6 +85,8 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), addEslintDisableNextLine: "Add eslint-disable-next-line Comment", addEslintDisable: "Add eslint-disable Comment", declareGlobal: "Declare '{0}' as a global variable (/* global */)", + replaceConnectWildcard: "Replace with @connect {0}", + replaceMatchWildcard: "Replace with @include {0}", prompt: { name: "Script name", namespace: "Script namespace", @@ -152,6 +156,8 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), addEslintDisableNextLine: "新增 eslint-disable-next-line 註解", addEslintDisable: "新增 eslint-disable 註解", declareGlobal: "將 '{0}' 宣告為全域變數 (/* global */)", + replaceConnectWildcard: "替換為 @connect {0}", + replaceMatchWildcard: "替換為 @include {0}", prompt: { name: "腳本名稱", namespace: "腳本命名空間", @@ -221,6 +227,8 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), addEslintDisableNextLine: "eslint-disable-next-line コメントを追加", addEslintDisable: "eslint-disable コメントを追加", declareGlobal: "'{0}' をグローバル変数として宣言 (/* global */)", + replaceConnectWildcard: "@connect {0} に置換", + replaceMatchWildcard: "@include {0} に置換", prompt: { name: "スクリプト名", namespace: "スクリプトの名前空間", @@ -290,6 +298,8 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), addEslintDisableNextLine: "eslint-disable-next-line Kommentar hinzufügen", addEslintDisable: "eslint-disable Kommentar hinzufügen", declareGlobal: "'{0}' als globale Variable deklarieren (/* global */)", + replaceConnectWildcard: "Durch @connect {0} ersetzen", + replaceMatchWildcard: "Durch @include {0} ersetzen", prompt: { name: "Skriptname", namespace: "Skript-Namensraum", @@ -359,6 +369,8 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), addEslintDisableNextLine: "Thêm chú thích eslint-disable-next-line", addEslintDisable: "Thêm chú thích eslint-disable", declareGlobal: "Khai báo '{0}' là biến toàn cục (/* global */)", + replaceConnectWildcard: "Thay bằng @connect {0}", + replaceMatchWildcard: "Thay bằng @include {0}", prompt: { name: "Tên script", namespace: "Namespace của script", @@ -428,6 +440,8 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), addEslintDisableNextLine: "Добавить комментарий eslint-disable-next-line", addEslintDisable: "Добавить комментарий eslint-disable", declareGlobal: "Объявить '{0}' как глобальную переменную (/* global */)", + replaceConnectWildcard: "Заменить на @connect {0}", + replaceMatchWildcard: "Заменить на @include {0}", prompt: { name: "Имя скрипта", namespace: "Пространство имён скрипта", From 04e6ef0e53d29cfc81f21f12429d05d3ec7d1709 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 23 May 2026 18:07:44 +0900 Subject: [PATCH 02/29] fix 02 --- src/pkg/utils/monaco-editor/index.ts | 574 +++++++++++++++------------ 1 file changed, 322 insertions(+), 252 deletions(-) diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index 9974e52fd..1b3a2fb97 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -1,6 +1,6 @@ import { systemConfig } from "@App/pages/store/global"; import EventEmitter from "eventemitter3"; -import { editor, languages, MarkerSeverity } from "monaco-editor"; +import { editor, languages, MarkerSeverity, type IRange } from "monaco-editor"; import { findGlobalInsertionInfo, updateGlobalCommentLine } from "./utils"; import type { EditorLangCode, EditorPrompt } from "./langs"; import { asEditorLangEntry, editorLangs } from "./langs"; @@ -10,6 +10,34 @@ interface ILinterWorker extends Worker { myLinterHook: EventEmitter; } +type EslintFix = { + range: IRange; + text: string; +}; + +type MetadataLineParts = { + prefix: string; + tag: string; + normalizedTag: MetadataTag; + spacing: string; + value: string; + suffix: string; +}; + +type MetadataTag = "connect" | "match"; + +type MetadataLineFix = { + title: string; + text: string; +}; + +type TextEdit = languages.IWorkspaceTextEdit["textEdit"]; + +type ScriptcatMonacoEnvironment = typeof window.MonacoEnvironment & { + myLinterWorker?: ILinterWorker; + eslintFixMap?: Map; +}; + // 注册 eslint worker(全局单例) const linterWorkerDeferred = deferred(); const langPromise = systemConfig.getLanguage(); @@ -54,59 +82,279 @@ export class LinterWorkerController { let isRegisterEditorDone = false; const scriptcatMarkerOwner = "ScriptCat"; +const quickfixKind = "quickfix"; +const noop = () => {}; +const metaLinePattern = /\/\/[ \t]*@(\S+)[ \t]*(.*)$/; +const metadataFixPattern = /^(\s*\/\/[ \t]*@)(connect|match)([ \t]+)(\S+)(.*)$/i; +const matchMetadataPattern = /^(\*|[-a-z]+|http\*):\/\/([^/]+)(\/.*)?$/i; +const noUndefMessagePattern = /^[^']*'([^']+)'[^']*$/; + +const getMonacoEnvironment = () => window.MonacoEnvironment as ScriptcatMonacoEnvironment | undefined; + +const ensureEslintFixMap = (environment: ScriptcatMonacoEnvironment) => { + environment.eslintFixMap ??= new Map(); + return environment.eslintFixMap; +}; + +const getMarkerCode = (marker: editor.IMarkerData) => { + if (!marker.code) return ""; + return typeof marker.code === "string" ? marker.code : marker.code.value; +}; + +const getEslintFixKey = (marker: editor.IMarkerData, code: string) => { + return `${code}|${marker.startLineNumber}|${marker.endLineNumber}|${marker.startColumn}|${marker.endColumn}`; +}; + +const createTextEditAction = ( + model: editor.ITextModel, + title: string, + diagnostics: editor.IMarkerData[], + textEdit: TextEdit, + isPreferred: boolean +) => { + return { + title, + diagnostics, + kind: quickfixKind, + edit: { + edits: [{ resource: model.uri, textEdit, versionId: undefined }], + }, + isPreferred, + } satisfies languages.CodeAction; +}; + +const createLineReplacementAction = ( + model: editor.ITextModel, + title: string, + diagnostics: editor.IMarkerData[], + lineNumber: number, + line: string, + text: string, + isPreferred: boolean +) => { + return createTextEditAction( + model, + title, + diagnostics, + { + range: { + startLineNumber: lineNumber, + startColumn: 1, + endLineNumber: lineNumber, + endColumn: line.length + 1, + }, + text, + }, + isPreferred + ); +}; const isSimpleValidHost = (hostName: string) => { - let ret = false; + if (!hostName) return false; try { - ret = hostName.length > 0 && new URL(`https://${hostName}.com/path`).origin === `https://${hostName}.com`; + hostName = hostName.toLowerCase(); + return new URL(`https://${hostName}.com/path`).origin === `https://${hostName}.com`; } catch { - // ignored + return false; } - return ret; }; -const getMetadataLineFixes = (line: string) => { - const match = /^(\s*\/\/[ \t]*@)(connect|match)([ \t]+)(\S+)(.*)$/i.exec(line); - if (!match) return []; +const parseMetadataLine = (line: string): MetadataLineParts | null => { + const match = metadataFixPattern.exec(line); + if (!match) return null; const [, prefix, tag, spacing, value, suffix] = match; - if (tag === "connect" && value.length > 2 && value.startsWith("*.")) { - const hostName = value.slice(2); - if (/\.\w{2,}$/.test(hostName) && isSimpleValidHost(hostName)) { - return [ + return { + prefix, + tag, + normalizedTag: tag.toLowerCase() as MetadataTag, + spacing, + value, + suffix, + }; +}; + +const createMetadataFix = (titleTemplate: string, titleValue: string, text: string): MetadataLineFix => { + return { + title: titleTemplate.replace("{0}", titleValue), + text, + }; +}; + +const getIncludeSpacing = (spacing: string, tag: string) => { + const lenDiff = "include".length - tag.length; + return lenDiff > 0 && spacing.length > lenDiff ? spacing.slice(0, -lenDiff) : spacing; +}; + +const getConnectMetadataFixes = ({ prefix, tag, spacing, value, suffix }: MetadataLineParts): MetadataLineFix[] => { + if (!value.startsWith("*.") || value.includes("**")) return []; + + const hostName = value.slice(2); + if (!/\.\w{2,}$/.test(hostName) || !isSimpleValidHost(hostName)) return []; + + const titleTemplate = multiLang.replaceConnectWildcard; + return [createMetadataFix(titleTemplate, hostName, `${prefix}${tag}${spacing}${hostName}${suffix}`)]; +}; + +const getMatchMetadataFixes = ({ + prefix, + normalizedTag, + spacing, + value, + suffix, +}: MetadataLineParts): MetadataLineFix[] => { + const match = matchMetadataPattern.exec(value); + const host = match?.[2]; + if (!match || !host?.endsWith(".*") || host.includes("**")) return []; + + const hostName = host.slice(0, -2); + if (!isSimpleValidHost(hostName.replace(/\*/g, "x"))) return []; + + const includeSpacing = getIncludeSpacing(spacing, normalizedTag); + const tldValue = `${match[1]}://${hostName}.tld${match[3] || ""}`; + + const titleTemplate = multiLang.replaceMatchWildcard; + return [ + createMetadataFix(titleTemplate, tldValue, `${prefix}include${includeSpacing}${tldValue}${suffix}`), + createMetadataFix(titleTemplate, value, `${prefix}include${includeSpacing}${value}${suffix}`), + ]; +}; + +const getMetadataLineFixes = (line: string): MetadataLineFix[] => { + const parts = parseMetadataLine(line); + if (!parts) return []; + + switch (parts.normalizedTag) { + case "connect": + return getConnectMetadataFixes(parts); + case "match": + return getMatchMetadataFixes(parts); + default: + return []; + } +}; + +const getMetadataLineActions = ( + actions: languages.CodeAction[], + model: editor.ITextModel, + lineNumber: number, + line: string, + markers: editor.IMarkerData[] +) => { + const fixes = getMetadataLineFixes(line); + if (fixes.length === 0) return; + + const diagnostics = markers.filter( + (marker) => marker.source === scriptcatMarkerOwner && marker.startLineNumber === lineNumber + ); + + for (let index = 0; index < fixes.length; index += 1) { + const fix = fixes[index]; + actions.push(createLineReplacementAction(model, fix.title, diagnostics, lineNumber, line, fix.text, index === 0)); + } +}; + +const getNoUndefGlobalName = (marker: editor.IMarkerData) => { + return noUndefMessagePattern.exec(marker.message)?.[1] || null; +}; + +const getGlobalDeclarationTextEdit = (model: editor.ITextModel, globalName: string): TextEdit => { + const { insertLine, globalLine } = findGlobalInsertionInfo(model); + + if (globalLine == null) { + return { + range: { + startLineNumber: insertLine, + startColumn: 1, + endLineNumber: insertLine, + endColumn: 1, + }, + text: `/* global ${globalName} */\n`, + }; + } + + const oldLine = model.getLineContent(globalLine); + return { + range: { + startLineNumber: globalLine, + startColumn: 1, + endLineNumber: globalLine, + endColumn: oldLine.length + 1, + }, + text: updateGlobalCommentLine(oldLine, globalName), + }; +}; + +const appendMarkerCodeActions = ( + actions: languages.CodeAction[], + model: editor.ITextModel, + marker: editor.IMarkerData, + eslintFixMap?: Map +) => { + const code = getMarkerCode(marker); + if (!code) return; + + const fix = eslintFixMap?.get(getEslintFixKey(marker, code)); + if (fix) { + actions.push( + createTextEditAction( + model, + multiLang.quickfix.replace("{0}", code), + [marker], { - title: multiLang.replaceConnectWildcard.replace("{0}", hostName), - text: `${prefix}${tag}${spacing}${hostName}${suffix}`, + range: fix.range, + text: fix.text, }, - ]; - } + true + ) + ); } - if (tag === "match") { - const matchPattern = /^(\*|[-a-z]+|http\*):\/\/([^/]+)(\/.*)?$/i.exec(value); - const host = matchPattern?.[2]; - if (host && host.endsWith(".*")) { - const hostName = host.slice(0, -2); - if (isSimpleValidHost(hostName)) { - const lenDiff = "include".length - tag.length; - let s = spacing; - if (lenDiff > 0 && s.length > lenDiff) s = s.slice(0, -lenDiff); - const tldValue = `${matchPattern[1]}://${hostName}.tld${matchPattern[3] || ""}`; - return [ - { - title: multiLang.replaceMatchWildcard.replace("{0}", value), - text: `${prefix}include${s}${value}${suffix}`, - }, - { - title: multiLang.replaceMatchWildcard.replace("{0}", tldValue), - text: `${prefix}include${s}${tldValue}${suffix}`, - }, - ]; - } - } + const globalName = code === "no-undef" ? getNoUndefGlobalName(marker) : null; + if (globalName) { + actions.push( + createTextEditAction( + model, + multiLang.declareGlobal.replace("{0}", globalName), + [marker], + getGlobalDeclarationTextEdit(model, globalName), + false + ) + ); } - return []; + actions.push( + createTextEditAction( + model, + multiLang.addEslintDisableNextLine, + [marker], + { + range: { + startLineNumber: marker.startLineNumber, + endLineNumber: marker.startLineNumber, + startColumn: 1, + endColumn: 1, + }, + text: `// eslint-disable-next-line ${code}\n`, + }, + true + ), + createTextEditAction( + model, + multiLang.addEslintDisable, + [marker], + { + range: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 1, + }, + text: `/* eslint-disable ${code} */\n`, + }, + true + ) + ); }; const updateScriptcatMetadataMarkers = (model: editor.ITextModel) => { @@ -155,8 +403,10 @@ export function registerEditor() { isRegisterEditorDone = true; // worker 初始化:复用已有 worker 或创建新的 - if ((window.MonacoEnvironment as any)?.myLinterWorker) { - linterWorkerDeferred.resolve((window.MonacoEnvironment as any)?.myLinterWorker); + const existingEnvironment = getMonacoEnvironment(); + if (existingEnvironment?.myLinterWorker) { + ensureEslintFixMap(existingEnvironment); + linterWorkerDeferred.resolve(existingEnvironment.myLinterWorker); } else { const linterWorker = new Worker("/src/linter.worker.js") as ILinterWorker; linterWorker.myLinterHook = new EventEmitter(); @@ -166,51 +416,45 @@ export function registerEditor() { }; window.MonacoEnvironment = { - getWorkerUrl(moduleId: any, label: any) { + ...existingEnvironment, + getWorkerUrl(_moduleId: unknown, label: string) { if (label === "typescript" || label === "javascript") { return "/src/ts.worker.js"; } return "/src/editor.worker.js"; }, - }; - - Object.assign(window.MonacoEnvironment, { myLinterWorker: linterWorker, - eslintFixMap: new Map(), - }); + eslintFixMap: new Map(), + } as ScriptcatMonacoEnvironment; linterWorkerDeferred.resolve(linterWorker); } // provider 注册始终执行,不受 worker 复用影响 - const META_LINE = /\/\/[ \t]*@(\S+)[ \t]*(.*)$/; - registerScriptcatMetadataMarkerProvider(); languages.registerHoverProvider("javascript", { provideHover: (model, position) => { - return new Promise((resolve) => { - const line = model.getLineContent(position.lineNumber); - const m = META_LINE.exec(line); - if (m) { - const key = m[1] as keyof EditorPrompt; - const prompt = multiLang.prompt; - resolve({ - contents: [ - { - value: prompt[key] || multiLang.undefinedPrompt, - supportHtml: true, - }, - ], - }); - } else if (/==UserScript==/.test(line)) { - resolve({ - contents: [{ value: multiLang.thisIsAUserScript }], - }); - } else { - resolve(null); - } - }); + const line = model.getLineContent(position.lineNumber); + const match = metaLinePattern.exec(line); + + if (match) { + const key = match[1] as keyof EditorPrompt; + return { + contents: [ + { + value: multiLang.prompt[key] || multiLang.undefinedPrompt, + supportHtml: true, + }, + ], + }; + } + + if (/==UserScript==/.test(line)) { + return { contents: [{ value: multiLang.thisIsAUserScript }] }; + } + + return null; }, }); @@ -218,191 +462,17 @@ export function registerEditor() { "javascript", { provideCodeActions: (model /** ITextModel */, range /** Range */, context /** CodeActionContext */) => { + const eslintFixMap = getMonacoEnvironment()?.eslintFixMap; + const line = model.getLineContent(range.startLineNumber); const actions: languages.CodeAction[] = []; - const eslintFixMap = >(window.MonacoEnvironment as any)?.eslintFixMap; - const metadataLineFixes = getMetadataLineFixes(model.getLineContent(range.startLineNumber)); - const scriptcatDiagnostics = context.markers.filter( - (marker) => marker.source === scriptcatMarkerOwner && marker.startLineNumber === range.startLineNumber - ); - - if (metadataLineFixes.length > 0) { - const line = model.getLineContent(range.startLineNumber); - metadataLineFixes.forEach((metadataLineFix, index) => - actions.push({ - title: metadataLineFix.title, - diagnostics: scriptcatDiagnostics, - kind: "quickfix", - edit: { - edits: [ - { - resource: model.uri, - textEdit: { - range: { - startLineNumber: range.startLineNumber, - startColumn: 1, - endLineNumber: range.startLineNumber, - endColumn: line.length + 1, - }, - text: metadataLineFix.text, - }, - versionId: undefined, - }, - ], - }, - isPreferred: index === 0, - } satisfies languages.CodeAction) - ); - } - for (let i = 0; i < context.markers.length; i++) { - // 判断有没有修复方案 - const val = context.markers[i]; - if (!val.code) continue; - const code = typeof val.code === "string" ? val.code : val.code!.value; - - // 1. eslint-fix - const baseKey = `${code}|${val.startLineNumber}|${val.endLineNumber}|${val.startColumn}|${val.endColumn}`; - const fix = eslintFixMap?.get(baseKey); - if (fix) { - actions.push({ - title: multiLang.quickfix.replace("{0}", code), - diagnostics: [val], - kind: "quickfix", - edit: { - edits: [ - { - resource: model.uri, - textEdit: { - range: fix.range, - text: fix.text, - }, - versionId: undefined, - }, - ], - }, - isPreferred: true, - } satisfies languages.CodeAction); - } - - // 2. no-undef → /* global */ - if (code === "no-undef") { - const message = val.message || ""; - const match = message.match(/^[^']*'([^']+)'[^']*$/); - const globalName = match?.[1]; - - if (globalName) { - const { insertLine, globalLine } = findGlobalInsertionInfo(model); - let textEdit: languages.IWorkspaceTextEdit["textEdit"]; - - if (globalLine != null) { - // there is already a /* global ... */ line → update it - const oldLine = model.getLineContent(globalLine); - const newLine = updateGlobalCommentLine(oldLine, globalName); - textEdit = { - range: { - startLineNumber: globalLine, - startColumn: 1, - endLineNumber: globalLine, - endColumn: oldLine.length + 1, - }, - text: newLine, - }; - } else { - // no global line yet → insert a new one - textEdit = { - range: { - startLineNumber: insertLine, - startColumn: 1, - endLineNumber: insertLine, - endColumn: 1, - }, - text: `/* global ${globalName} */\n`, - }; - } - - actions.push({ - title: multiLang.declareGlobal.replace("{0}", globalName), - diagnostics: [val], - kind: "quickfix", - edit: { - edits: [{ resource: model.uri, textEdit, versionId: undefined }], - }, - isPreferred: false, - } satisfies languages.CodeAction); - } - } - - // 3. disable-next-line / disable - actions.push({ - title: multiLang.addEslintDisableNextLine, - diagnostics: [val], - kind: "quickfix", - edit: { - edits: [ - { - resource: model.uri, - textEdit: { - range: { - startLineNumber: val.startLineNumber, - endLineNumber: val.startLineNumber, - startColumn: 1, - endColumn: 1, - }, - text: `// eslint-disable-next-line ${code}\n`, - }, - versionId: undefined, - }, - ], - }, - isPreferred: true, - } satisfies languages.CodeAction); - - actions.push({ - title: multiLang.addEslintDisable, - diagnostics: [val], - kind: "quickfix", - edit: { - edits: [ - { - resource: model.uri, - textEdit: { - range: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 1, - }, - text: `/* eslint-disable ${code} */\n`, - }, - versionId: undefined, - }, - ], - }, - isPreferred: true, - } satisfies languages.CodeAction); + getMetadataLineActions(actions, model, range.startLineNumber, line, context.markers); + + for (const marker of context.markers) { + appendMarkerCodeActions(actions, model, marker, eslintFixMap); } - // const actions = context.markers.map((error) => { - // const edit: languages.IWorkspaceTextEdit = { - // resource: model.uri, - // textEdit: { - // range, - // text: "console.log(1)", - // }, - // versionId: undefined, - // }; - // return { - // title: ``, - // diagnostics: [error], - // kind: "quickfix", - // edit: { - // edits: [edit], - // }, - // isPreferred: true, - // }; - // }); - - return { actions, dispose: () => {} }; + return { actions, dispose: noop }; }, }, { providedCodeActionKinds: ["quickfix"] } From 6823bd457e709e956c53e5bbcb11f4cfa86dbc4f Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 23 May 2026 18:22:05 +0900 Subject: [PATCH 03/29] fix 03 --- src/pkg/utils/monaco-editor/index.ts | 34 +++++++++++++--------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index 1b3a2fb97..d952a04f7 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -235,23 +235,21 @@ const getMetadataLineFixes = (line: string): MetadataLineFix[] => { }; const getMetadataLineActions = ( - actions: languages.CodeAction[], model: editor.ITextModel, lineNumber: number, line: string, markers: editor.IMarkerData[] -) => { +): languages.CodeAction[] => { const fixes = getMetadataLineFixes(line); - if (fixes.length === 0) return; + if (fixes.length === 0) return []; const diagnostics = markers.filter( (marker) => marker.source === scriptcatMarkerOwner && marker.startLineNumber === lineNumber ); - for (let index = 0; index < fixes.length; index += 1) { - const fix = fixes[index]; - actions.push(createLineReplacementAction(model, fix.title, diagnostics, lineNumber, line, fix.text, index === 0)); - } + return fixes.map((fix, index) => + createLineReplacementAction(model, fix.title, diagnostics, lineNumber, line, fix.text, index === 0) + ); }; const getNoUndefGlobalName = (marker: editor.IMarkerData) => { @@ -285,14 +283,15 @@ const getGlobalDeclarationTextEdit = (model: editor.ITextModel, globalName: stri }; }; -const appendMarkerCodeActions = ( - actions: languages.CodeAction[], +const getMarkerCodeActions = ( model: editor.ITextModel, marker: editor.IMarkerData, eslintFixMap?: Map -) => { +): languages.CodeAction[] => { const code = getMarkerCode(marker); - if (!code) return; + if (!code) return []; + + const actions: languages.CodeAction[] = []; const fix = eslintFixMap?.get(getEslintFixKey(marker, code)); if (fix) { @@ -355,6 +354,8 @@ const appendMarkerCodeActions = ( true ) ); + + return actions; }; const updateScriptcatMetadataMarkers = (model: editor.ITextModel) => { @@ -464,13 +465,10 @@ export function registerEditor() { provideCodeActions: (model /** ITextModel */, range /** Range */, context /** CodeActionContext */) => { const eslintFixMap = getMonacoEnvironment()?.eslintFixMap; const line = model.getLineContent(range.startLineNumber); - const actions: languages.CodeAction[] = []; - - getMetadataLineActions(actions, model, range.startLineNumber, line, context.markers); - - for (const marker of context.markers) { - appendMarkerCodeActions(actions, model, marker, eslintFixMap); - } + const actions = [ + ...getMetadataLineActions(model, range.startLineNumber, line, context.markers), + ...context.markers.flatMap((marker) => getMarkerCodeActions(model, marker, eslintFixMap)), + ]; return { actions, dispose: noop }; }, From 2b7c6ad69a64eacdec9811b7154abc04dec195b6 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 23 May 2026 18:31:01 +0900 Subject: [PATCH 04/29] fix 04 --- src/pkg/utils/monaco-editor/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index d952a04f7..c93292519 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -82,6 +82,7 @@ export class LinterWorkerController { let isRegisterEditorDone = false; const scriptcatMarkerOwner = "ScriptCat"; +const eslintMarkerOwner = "ESLint"; const quickfixKind = "quickfix"; const noop = () => {}; const metaLinePattern = /\/\/[ \t]*@(\S+)[ \t]*(.*)$/; @@ -288,6 +289,7 @@ const getMarkerCodeActions = ( marker: editor.IMarkerData, eslintFixMap?: Map ): languages.CodeAction[] => { + if (marker.source !== eslintMarkerOwner) return []; const code = getMarkerCode(marker); if (!code) return []; From 512b435fa1b0bfd84d088a0f72204330e3d13f16 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 23 May 2026 19:08:55 +0900 Subject: [PATCH 05/29] fix 05 --- src/pkg/utils/monaco-editor/index.ts | 56 +++++++++++++++++----------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index c93292519..f7f279afd 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -311,35 +311,49 @@ const getMarkerCodeActions = ( ); } - const globalName = code === "no-undef" ? getNoUndefGlobalName(marker) : null; - if (globalName) { + let canApplyEslintSingleLineDisable = true; + + switch (code) { + case "no-undef": { + const globalName = getNoUndefGlobalName(marker); + if (globalName) { + actions.push( + createTextEditAction( + model, + multiLang.declareGlobal.replace("{0}", globalName), + [marker], + getGlobalDeclarationTextEdit(model, globalName), + false + ) + ); + } + break; + } + case "userscripts/better-use-match": + case "userscripts/no-invalid-headers": + canApplyEslintSingleLineDisable = false; + } + + if (canApplyEslintSingleLineDisable) { actions.push( createTextEditAction( model, - multiLang.declareGlobal.replace("{0}", globalName), + multiLang.addEslintDisableNextLine, [marker], - getGlobalDeclarationTextEdit(model, globalName), - false + { + range: { + startLineNumber: marker.startLineNumber, + endLineNumber: marker.startLineNumber, + startColumn: 1, + endColumn: 1, + }, + text: `// eslint-disable-next-line ${code}\n`, + }, + true ) ); } - actions.push( - createTextEditAction( - model, - multiLang.addEslintDisableNextLine, - [marker], - { - range: { - startLineNumber: marker.startLineNumber, - endLineNumber: marker.startLineNumber, - startColumn: 1, - endColumn: 1, - }, - text: `// eslint-disable-next-line ${code}\n`, - }, - true - ), createTextEditAction( model, multiLang.addEslintDisable, From 3ab1b5b9903008b5ab97c2cf87760db2f99565c2 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 23 May 2026 19:11:06 +0900 Subject: [PATCH 06/29] fix 06 --- src/pkg/utils/monaco-editor/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index f7f279afd..9292e4365 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -329,6 +329,7 @@ const getMarkerCodeActions = ( } break; } + case "userscripts/align-attributes": case "userscripts/better-use-match": case "userscripts/no-invalid-headers": canApplyEslintSingleLineDisable = false; From e5d3e05ecee7e106bc8cf85e77c66b471d0b8c63 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 23 May 2026 19:33:43 +0900 Subject: [PATCH 07/29] fix 07 --- src/linter.worker.ts | 17 ++++++++++++++++- src/pkg/utils/monaco-editor/index.ts | 24 ++++++++++++++++++++++-- src/pkg/utils/monaco-editor/langs.ts | 4 ++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/linter.worker.ts b/src/linter.worker.ts index 69495b0f9..d8a03a11c 100644 --- a/src/linter.worker.ts +++ b/src/linter.worker.ts @@ -6,8 +6,23 @@ const { rules } = require("eslint-plugin-userscripts"); const linter = new Linter({ configType: "eslintrc" }); +// ScriptCat 不适用 - 有必要存在的用法 +const omitKeys = new Set(["better-use-match"]); + // 额外定义 userscripts 规则 -const formatRules = Object.fromEntries(Object.entries(rules).map(([key, metas]) => ["userscripts/" + key, metas])); +const formatRules = Object.fromEntries( + Object.entries(rules).map(([key, metas]) => [ + "userscripts/" + key, + omitKeys.has(key) + ? { + meta: {}, + create() { + return { CallExpression() {} }; + }, + } + : metas, + ]) +); linter.defineRules(formatRules as any); const getRules = linter.getRules(); diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index 9292e4365..20a9a2361 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -24,7 +24,7 @@ type MetadataLineParts = { suffix: string; }; -type MetadataTag = "connect" | "match"; +type MetadataTag = "connect" | "match" | "include"; type MetadataLineFix = { title: string; @@ -86,7 +86,7 @@ const eslintMarkerOwner = "ESLint"; const quickfixKind = "quickfix"; const noop = () => {}; const metaLinePattern = /\/\/[ \t]*@(\S+)[ \t]*(.*)$/; -const metadataFixPattern = /^(\s*\/\/[ \t]*@)(connect|match)([ \t]+)(\S+)(.*)$/i; +const metadataFixPattern = /^(\s*\/\/[ \t]*@)(connect|match|include)([ \t]+)(\S+)(.*)$/i; const matchMetadataPattern = /^(\*|[-a-z]+|http\*):\/\/([^/]+)(\/.*)?$/i; const noUndefMessagePattern = /^[^']*'([^']+)'[^']*$/; @@ -221,6 +221,24 @@ const getMatchMetadataFixes = ({ ]; }; +const getIncludeMetadataFixes = ({ + prefix, + normalizedTag, + spacing, + value, + suffix, +}: MetadataLineParts): MetadataLineFix[] => { + const match = matchMetadataPattern.exec(value); + const host = match?.[2]; + if (!match || !host || host.endsWith(".*") || host.includes("**")) return []; + if (host.split(".").every((e) => e === "*" || /^[\w-]+$/.test(e))) { + const includeSpacing = getIncludeSpacing(spacing, normalizedTag); + const titleTemplate = multiLang.replaceToMatch; + return [createMetadataFix(titleTemplate, value, `${prefix}match ${includeSpacing}${value}${suffix}`)]; + } + return []; +}; + const getMetadataLineFixes = (line: string): MetadataLineFix[] => { const parts = parseMetadataLine(line); if (!parts) return []; @@ -230,6 +248,8 @@ const getMetadataLineFixes = (line: string): MetadataLineFix[] => { return getConnectMetadataFixes(parts); case "match": return getMatchMetadataFixes(parts); + case "include": + return getIncludeMetadataFixes(parts); default: return []; } diff --git a/src/pkg/utils/monaco-editor/langs.ts b/src/pkg/utils/monaco-editor/langs.ts index 4fb99b289..319c3e194 100644 --- a/src/pkg/utils/monaco-editor/langs.ts +++ b/src/pkg/utils/monaco-editor/langs.ts @@ -9,6 +9,7 @@ export const editorLangs = { declareGlobal: "将 '{0}' 声明为全局变量 (/* global */)", replaceConnectWildcard: "替换为 @connect {0}", replaceMatchWildcard: "替换为 @include {0}", + replaceToMatch: "替换为 @match {0}", prompt: { name: "脚本名称", namespace: "脚本命名空间", @@ -87,6 +88,7 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), declareGlobal: "Declare '{0}' as a global variable (/* global */)", replaceConnectWildcard: "Replace with @connect {0}", replaceMatchWildcard: "Replace with @include {0}", + replaceToMatch: "Replace with @match {0}", prompt: { name: "Script name", namespace: "Script namespace", @@ -158,6 +160,7 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), declareGlobal: "將 '{0}' 宣告為全域變數 (/* global */)", replaceConnectWildcard: "替換為 @connect {0}", replaceMatchWildcard: "替換為 @include {0}", + replaceToMatch: "替換為 @match {0}", prompt: { name: "腳本名稱", namespace: "腳本命名空間", @@ -229,6 +232,7 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), declareGlobal: "'{0}' をグローバル変数として宣言 (/* global */)", replaceConnectWildcard: "@connect {0} に置換", replaceMatchWildcard: "@include {0} に置換", + replaceToMatch: "@match {0} に置換", prompt: { name: "スクリプト名", namespace: "スクリプトの名前空間", From 3fa2d69fdea466e2d4e89ccabf10efec65f3e868 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 23 May 2026 19:36:12 +0900 Subject: [PATCH 08/29] fix 08 --- src/pkg/utils/monaco-editor/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index 20a9a2361..a525d0619 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -204,9 +204,10 @@ const getMatchMetadataFixes = ({ value, suffix, }: MetadataLineParts): MetadataLineFix[] => { + if (!value || value.startsWith("/")) return []; const match = matchMetadataPattern.exec(value); const host = match?.[2]; - if (!match || !host?.endsWith(".*") || host.includes("**")) return []; + if (!match || !host?.endsWith(".*") || host.includes("**") || host.includes("\\")) return []; const hostName = host.slice(0, -2); if (!isSimpleValidHost(hostName.replace(/\*/g, "x"))) return []; @@ -230,7 +231,7 @@ const getIncludeMetadataFixes = ({ }: MetadataLineParts): MetadataLineFix[] => { const match = matchMetadataPattern.exec(value); const host = match?.[2]; - if (!match || !host || host.endsWith(".*") || host.includes("**")) return []; + if (!match || !host || host.endsWith(".*") || host.includes("**") || host.endsWith(".tld")) return []; if (host.split(".").every((e) => e === "*" || /^[\w-]+$/.test(e))) { const includeSpacing = getIncludeSpacing(spacing, normalizedTag); const titleTemplate = multiLang.replaceToMatch; From 528de9b04bb345ea6cad18b8fa385c06838e9e19 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 23 May 2026 19:59:00 +0900 Subject: [PATCH 09/29] add "allFrames" --- packages/eslint/compat-headers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint/compat-headers.js b/packages/eslint/compat-headers.js index 08eb3ce19..36ccbe436 100644 --- a/packages/eslint/compat-headers.js +++ b/packages/eslint/compat-headers.js @@ -17,6 +17,7 @@ const compatMap = { storageName: [], "early-start": [], "require-css": [], + "allFrames": [], }, }; From e9a3c695c730b50095505fc833edc9127f204e74 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 23 May 2026 19:59:16 +0900 Subject: [PATCH 10/29] Update compat-headers.js --- packages/eslint/compat-headers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint/compat-headers.js b/packages/eslint/compat-headers.js index 36ccbe436..2a674d4e7 100644 --- a/packages/eslint/compat-headers.js +++ b/packages/eslint/compat-headers.js @@ -17,7 +17,7 @@ const compatMap = { storageName: [], "early-start": [], "require-css": [], - "allFrames": [], + allFrames: [], }, }; From 0481a6853e027495c0c8fdd805e0a4a2427f1512 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 23 May 2026 20:13:28 +0900 Subject: [PATCH 11/29] fix langs --- src/pkg/utils/monaco-editor/langs.ts | 198 +++++++++++++++++++++++++-- 1 file changed, 185 insertions(+), 13 deletions(-) diff --git a/src/pkg/utils/monaco-editor/langs.ts b/src/pkg/utils/monaco-editor/langs.ts index 319c3e194..da25810e1 100644 --- a/src/pkg/utils/monaco-editor/langs.ts +++ b/src/pkg/utils/monaco-editor/langs.ts @@ -8,7 +8,7 @@ export const editorLangs = { addEslintDisable: "添加 eslint-disable 注释", declareGlobal: "将 '{0}' 声明为全局变量 (/* global */)", replaceConnectWildcard: "替换为 @connect {0}", - replaceMatchWildcard: "替换为 @include {0}", + replaceMatchWildcard: "将通配符 @match 替换为 @include {0}", replaceToMatch: "替换为 @match {0}", prompt: { name: "脚本名称", @@ -56,9 +56,28 @@ miner:该脚本存在利用用户资源但不为用户产生收益或收益极 membership:该脚本需要注册会员/关注公众号才能正常使用 tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), updateURL: "脚本检查更新的 url", + updateurl: "脚本检查更新的 url", downloadURL: "脚本更新的下载地址", + downloadurl: "脚本更新的下载地址", supportURL: "支持站点、bug 反馈页面", + supporturl: "支持站点、bug 反馈页面", source: "脚本源码页", + homepageurl: "脚本主页", + iconurl: "脚本图标", + icon64url: "64x64 大小的脚本图标", + scriptUrl: "订阅脚本中引用的用户脚本地址", + scripturl: "订阅脚本中引用的用户脚本地址", + storageName: "脚本值存储空间名称,用于让多个脚本共享同一个存储空间", + storagename: "脚本值存储空间名称,用于让多个脚本共享同一个存储空间", + tag: "脚本标签,多个标签可用逗号或空格分隔", + cloudCat: "标记脚本支持导出为 CloudCat 云端脚本包", + cloudcat: "标记脚本支持导出为 CloudCat 云端脚本包", + cloudServer: "脚本使用的 CloudCat 云端服务", + cloudserver: "脚本使用的 CloudCat 云端服务", + exportValue: "导出为云端脚本时需要导出的脚本存储值", + exportvalue: "导出为云端脚本时需要导出的脚本存储值", + exportCookie: "导出为云端脚本时需要导出的 Cookie", + exportcookie: "导出为云端脚本时需要导出的 Cookie", crontab: `定时脚本 crontab 参考(不适用于云端脚本) * * * * * * 每秒运行一次 * * * * * 每分钟运行一次 @@ -87,7 +106,7 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), addEslintDisable: "Add eslint-disable Comment", declareGlobal: "Declare '{0}' as a global variable (/* global */)", replaceConnectWildcard: "Replace with @connect {0}", - replaceMatchWildcard: "Replace with @include {0}", + replaceMatchWildcard: "Replace wildcard @match with @include {0}", replaceToMatch: "Replace with @match {0}", prompt: { name: "Script name", @@ -126,11 +145,36 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), unwrap: "Makes the user script bypass sandbox wrapping and be injected and executed directly in the page’s native global scope.
The script can directly access and modify the page’s real global variables, but will not be able to use user script privileged APIs such as GM.*.
Commonly used in scenarios that require deep interaction with native page scripts or when migrating from regular page scripts.", definition: "ScriptCat-only: URL of a `.d.ts` file used for editor auto-completion", - antifeature: "For script markets: describe any unwanted or controversial features", + antifeature: `Related to script markets: unwanted features should include this description value +referral-link: This script modifies or redirects to the author's referral link +ads: This script inserts ads on the pages you visit +payment: This script requires payment to be used properly +miner: This script engages in mining activities +membership: This script requires registration as a member to be used properly +tracking: This script tracks your user information`.replace(/\n/g, "
"), updateURL: "URL used to check for script updates", + updateurl: "URL used to check for script updates", downloadURL: "URL used to download script updates", + downloadurl: "URL used to download script updates", supportURL: "Support site / bug report page", + supporturl: "Support site / bug report page", source: "Script source code page", + homepageurl: "Script homepage", + iconurl: "Script icon", + icon64url: "64x64 script icon", + scriptUrl: "User script URL referenced by a subscription script", + scripturl: "User script URL referenced by a subscription script", + storageName: "Script value storage name, used to share one storage area across multiple scripts", + storagename: "Script value storage name, used to share one storage area across multiple scripts", + tag: "Script tags, separated by commas or spaces", + cloudCat: "Marks the script as exportable to a CloudCat cloud script package", + cloudcat: "Marks the script as exportable to a CloudCat cloud script package", + cloudServer: "CloudCat cloud service used by the script", + cloudserver: "CloudCat cloud service used by the script", + exportValue: "Script storage values to export when exporting as a cloud script", + exportvalue: "Script storage values to export when exporting as a cloud script", + exportCookie: "Cookies to export when exporting as a cloud script", + exportcookie: "Cookies to export when exporting as a cloud script", crontab: `Scheduled script crontab examples (not for cloud scripts) * * * * * * Run every second * * * * * Run every minute @@ -159,7 +203,7 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), addEslintDisable: "新增 eslint-disable 註解", declareGlobal: "將 '{0}' 宣告為全域變數 (/* global */)", replaceConnectWildcard: "替換為 @connect {0}", - replaceMatchWildcard: "替換為 @include {0}", + replaceMatchWildcard: "將萬用字元 @match 替換為 @include {0}", replaceToMatch: "替換為 @match {0}", prompt: { name: "腳本名稱", @@ -198,11 +242,36 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), unwrap: "讓使用者腳本不經過沙箱封裝,直接注入並執行於頁面的原生全域作用域中。
腳本可直接存取並修改頁面真實的全域變數,但將無法使用 GM.* 等使用者腳本的特權 API。
常用於需要與頁面原生腳本深度互動,或從一般頁面腳本遷移的場景。", definition: "ScriptCat 特有功能:一個 `.d.ts` 檔案的引用網址,可啟用編輯器自動提示", - antifeature: "與腳本市場相關,不受歡迎的功能需要在此描述", + antifeature: `與腳本市場相關,不受歡迎的功能需要加上此描述值 +referral-link:此腳本會修改或重新導向至作者的返傭連結 +ads:此腳本會在您存取的頁面上插入廣告 +payment:此腳本需要您付費才能正常使用 +miner:此腳本存在挖礦行為 +membership:此腳本需要註冊會員才能正常使用 +tracking:此腳本會追蹤您的使用者資訊`.replace(/\n/g, "
"), updateURL: "腳本檢查更新的 url", + updateurl: "腳本檢查更新的 url", downloadURL: "腳本更新的下載網址", + downloadurl: "腳本更新的下載網址", supportURL: "支援站點、錯誤回報頁面", + supporturl: "支援站點、錯誤回報頁面", source: "腳本原始碼頁面", + homepageurl: "腳本首頁", + iconurl: "腳本圖示", + icon64url: "64x64 大小的腳本圖示", + scriptUrl: "訂閱腳本中引用的使用者腳本網址", + scripturl: "訂閱腳本中引用的使用者腳本網址", + storageName: "腳本值儲存空間名稱,用於讓多個腳本共享同一個儲存空間", + storagename: "腳本值儲存空間名稱,用於讓多個腳本共享同一個儲存空間", + tag: "腳本標籤,多個標籤可用逗號或空格分隔", + cloudCat: "標記腳本支援匯出為 CloudCat 雲端腳本套件", + cloudcat: "標記腳本支援匯出為 CloudCat 雲端腳本套件", + cloudServer: "腳本使用的 CloudCat 雲端服務", + cloudserver: "腳本使用的 CloudCat 雲端服務", + exportValue: "匯出為雲端腳本時需要匯出的腳本儲存值", + exportvalue: "匯出為雲端腳本時需要匯出的腳本儲存值", + exportCookie: "匯出為雲端腳本時需要匯出的 Cookie", + exportcookie: "匯出為雲端腳本時需要匯出的 Cookie", crontab: `排程腳本 crontab 參考(不適用於雲端腳本) * * * * * * 每秒執行一次 * * * * * 每分鐘執行一次 @@ -231,7 +300,7 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), addEslintDisable: "eslint-disable コメントを追加", declareGlobal: "'{0}' をグローバル変数として宣言 (/* global */)", replaceConnectWildcard: "@connect {0} に置換", - replaceMatchWildcard: "@include {0} に置換", + replaceMatchWildcard: "ワイルドカード @match を @include {0} に置換", replaceToMatch: "@match {0} に置換", prompt: { name: "スクリプト名", @@ -270,11 +339,36 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), unwrap: "ユーザースクリプトをサンドボックスでラップせず、ページのネイティブなグローバルスコープに直接注入して実行します。
スクリプトはページの実際のグローバル変数に直接アクセスおよび変更できますが、GM.* などのユーザースクリプトの特権 API は使用できなくなります。
ページのネイティブスクリプトとの深い連携が必要な場合や、通常のページスクリプトから移行する際によく使用されます。", definition: "ScriptCat 専用機能:`.d.ts` ファイルの URL。エディタの補完を有効にします。", - antifeature: "スクリプトマーケット向け:好まれない機能がある場合、ここに説明を記載します。", + antifeature: `スクリプトマーケットに関連します。好まれない機能にはこの説明値を追加する必要があります +referral-link:このスクリプトは作者のアフィリエイトリンクに変更またはリダイレクトします +ads:このスクリプトはアクセスしたページに広告を挿入します +payment:このスクリプトは正常に使用するために支払いが必要です +miner:このスクリプトにはマイニング動作があります +membership:このスクリプトは正常に使用するためにメンバー登録が必要です +tracking:このスクリプトはユーザー情報を追跡します`.replace(/\n/g, "
"), updateURL: "スクリプト更新を確認する URL", + updateurl: "スクリプト更新を確認する URL", downloadURL: "スクリプト更新をダウンロードする URL", + downloadurl: "スクリプト更新をダウンロードする URL", supportURL: "サポートサイト・バグ報告ページ", + supporturl: "サポートサイト・バグ報告ページ", source: "スクリプトのソースコードページ", + homepageurl: "スクリプトのホームページ", + iconurl: "スクリプトのアイコン", + icon64url: "64x64 サイズのスクリプトアイコン", + scriptUrl: "サブスクリプションスクリプトで参照するユーザースクリプト URL", + scripturl: "サブスクリプションスクリプトで参照するユーザースクリプト URL", + storageName: "複数のスクリプトで同じ保存領域を共有するためのスクリプト値ストレージ名", + storagename: "複数のスクリプトで同じ保存領域を共有するためのスクリプト値ストレージ名", + tag: "スクリプトタグ。複数のタグはカンマまたはスペースで区切ります", + cloudCat: "スクリプトを CloudCat クラウドスクリプトパッケージとしてエクスポート可能にする印", + cloudcat: "スクリプトを CloudCat クラウドスクリプトパッケージとしてエクスポート可能にする印", + cloudServer: "スクリプトが使用する CloudCat クラウドサービス", + cloudserver: "スクリプトが使用する CloudCat クラウドサービス", + exportValue: "クラウドスクリプトとしてエクスポートする際に出力するスクリプト保存値", + exportvalue: "クラウドスクリプトとしてエクスポートする際に出力するスクリプト保存値", + exportCookie: "クラウドスクリプトとしてエクスポートする際に出力する Cookie", + exportcookie: "クラウドスクリプトとしてエクスポートする際に出力する Cookie", crontab: `スケジュールスクリプトの crontab 例(クラウドスクリプトには非対応) * * * * * * 毎秒実行 * * * * * 毎分実行 @@ -303,7 +397,8 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), addEslintDisable: "eslint-disable Kommentar hinzufügen", declareGlobal: "'{0}' als globale Variable deklarieren (/* global */)", replaceConnectWildcard: "Durch @connect {0} ersetzen", - replaceMatchWildcard: "Durch @include {0} ersetzen", + replaceMatchWildcard: "Wildcard-@match durch @include {0} ersetzen", + replaceToMatch: "Durch @match {0} ersetzen", prompt: { name: "Skriptname", namespace: "Skript-Namensraum", @@ -341,11 +436,36 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), unwrap: "Ermöglicht es, das Benutzerskript ohne Sandbox-Kapselung direkt in den nativen globalen Gültigkeitsbereich der Seite zu injizieren und auszuführen.
Das Skript kann direkt auf die tatsächlichen globalen Variablen der Seite zugreifen und diese verändern, kann jedoch keine privilegierten Benutzerskript-APIs wie GM.* verwenden.
Wird häufig in Szenarien eingesetzt, die eine tiefe Interaktion mit nativen Seitenskripten erfordern oder bei der Migration von normalen Seitenskripten.", definition: "Nur für ScriptCat: URL zu einer `.d.ts`-Datei für Editor-Autovervollständigung", - antifeature: "Für Script-Marktplätze: hier unerwünschte oder kontroverse Funktionen beschreiben", + antifeature: `Bezieht sich auf Script-Marktplätze: unerwünschte Funktionen sollten diesen Beschreibungswert enthalten +referral-link: Dieses Skript modifiziert oder leitet zu den Affiliate-Links des Autors um +ads: Dieses Skript fügt Werbung auf den von Ihnen besuchten Seiten ein +payment: Dieses Skript erfordert eine Zahlung für die normale Nutzung +miner: Dieses Skript hat Mining-Verhalten +membership: Dieses Skript erfordert eine Mitgliedschaftsregistrierung für die normale Nutzung +tracking: Dieses Skript verfolgt Ihre Benutzerinformationen`.replace(/\n/g, "
"), updateURL: "URL zur Aktualisierungsprüfung des Skripts", + updateurl: "URL zur Aktualisierungsprüfung des Skripts", downloadURL: "URL zum Herunterladen von Skriptaktualisierungen", + downloadurl: "URL zum Herunterladen von Skriptaktualisierungen", supportURL: "Support-Seite / Bugtracker", + supporturl: "Support-Seite / Bugtracker", source: "Quellcode-Seite des Skripts", + homepageurl: "Skript-Homepage", + iconurl: "Skript-Symbol", + icon64url: "64x64 Skript-Symbol", + scriptUrl: "Benutzerskript-URL, die von einem Abonnement-Skript referenziert wird", + scripturl: "Benutzerskript-URL, die von einem Abonnement-Skript referenziert wird", + storageName: "Speichername für Skriptwerte, um einen Speicherbereich mit mehreren Skripten zu teilen", + storagename: "Speichername für Skriptwerte, um einen Speicherbereich mit mehreren Skripten zu teilen", + tag: "Skript-Tags, getrennt durch Kommas oder Leerzeichen", + cloudCat: "Markiert das Skript als exportierbar in ein CloudCat-Cloud-Skriptpaket", + cloudcat: "Markiert das Skript als exportierbar in ein CloudCat-Cloud-Skriptpaket", + cloudServer: "Vom Skript verwendeter CloudCat-Clouddienst", + cloudserver: "Vom Skript verwendeter CloudCat-Clouddienst", + exportValue: "Skript-Speicherwerte, die beim Export als Cloud-Skript exportiert werden", + exportvalue: "Skript-Speicherwerte, die beim Export als Cloud-Skript exportiert werden", + exportCookie: "Cookies, die beim Export als Cloud-Skript exportiert werden", + exportcookie: "Cookies, die beim Export als Cloud-Skript exportiert werden", crontab: `Beispiele für geplante Skripte (crontab, nicht für Cloud-Skripte) * * * * * * Jede Sekunde ausführen * * * * * Jede Minute ausführen @@ -374,7 +494,8 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), addEslintDisable: "Thêm chú thích eslint-disable", declareGlobal: "Khai báo '{0}' là biến toàn cục (/* global */)", replaceConnectWildcard: "Thay bằng @connect {0}", - replaceMatchWildcard: "Thay bằng @include {0}", + replaceMatchWildcard: "Thay @match có ký tự đại diện bằng @include {0}", + replaceToMatch: "Thay bằng @match {0}", prompt: { name: "Tên script", namespace: "Namespace của script", @@ -412,11 +533,36 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), unwrap: "Cho phép script người dùng bỏ qua sandbox và được chèn, thực thi trực tiếp trong phạm vi toàn cục gốc của trang.
Script có thể trực tiếp truy cập và chỉnh sửa các biến toàn cục thực sự của trang, nhưng sẽ không thể sử dụng các API đặc quyền của user script như GM.*.
Thường được dùng trong các trường hợp cần tương tác sâu với script gốc của trang hoặc khi chuyển đổi từ script trang thông thường.", definition: "Tính năng riêng của ScriptCat: URL tới tệp `.d.ts` giúp bật gợi ý tự động trong trình soạn thảo", - antifeature: "Dùng cho chợ script: mô tả các tính năng không được người dùng ưa thích", + antifeature: `Liên quan đến chợ script: các tính năng không được ưa thích cần thêm giá trị mô tả này +referral-link: Script này sửa đổi hoặc chuyển hướng đến liên kết giới thiệu của tác giả +ads: Script này chèn quảng cáo vào các trang bạn truy cập +payment: Script này yêu cầu thanh toán để sử dụng đúng cách +miner: Script này tham gia vào các hoạt động đào coin +membership: Script này yêu cầu đăng ký làm thành viên để sử dụng đúng cách +tracking: Script này theo dõi thông tin người dùng của bạn`.replace(/\n/g, "
"), updateURL: "URL dùng để kiểm tra cập nhật script", + updateurl: "URL dùng để kiểm tra cập nhật script", downloadURL: "URL tải về bản cập nhật script", + downloadurl: "URL tải về bản cập nhật script", supportURL: "Trang hỗ trợ / báo lỗi", + supporturl: "Trang hỗ trợ / báo lỗi", source: "Trang mã nguồn script", + homepageurl: "Trang chủ script", + iconurl: "Biểu tượng script", + icon64url: "Biểu tượng script kích thước 64x64", + scriptUrl: "URL user script được tham chiếu bởi script đăng ký", + scripturl: "URL user script được tham chiếu bởi script đăng ký", + storageName: "Tên vùng lưu trữ giá trị script, dùng để chia sẻ cùng một vùng lưu trữ giữa nhiều script", + storagename: "Tên vùng lưu trữ giá trị script, dùng để chia sẻ cùng một vùng lưu trữ giữa nhiều script", + tag: "Thẻ script, phân tách bằng dấu phẩy hoặc khoảng trắng", + cloudCat: "Đánh dấu script có thể xuất thành gói cloud script CloudCat", + cloudcat: "Đánh dấu script có thể xuất thành gói cloud script CloudCat", + cloudServer: "Dịch vụ CloudCat cloud mà script sử dụng", + cloudserver: "Dịch vụ CloudCat cloud mà script sử dụng", + exportValue: "Giá trị lưu trữ script cần xuất khi xuất thành cloud script", + exportvalue: "Giá trị lưu trữ script cần xuất khi xuất thành cloud script", + exportCookie: "Cookie cần xuất khi xuất thành cloud script", + exportcookie: "Cookie cần xuất khi xuất thành cloud script", crontab: `Ví dụ crontab cho script chạy định kỳ (không áp dụng cho script trên cloud) * * * * * * Chạy mỗi giây * * * * * Chạy mỗi phút @@ -445,7 +591,8 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), addEslintDisable: "Добавить комментарий eslint-disable", declareGlobal: "Объявить '{0}' как глобальную переменную (/* global */)", replaceConnectWildcard: "Заменить на @connect {0}", - replaceMatchWildcard: "Заменить на @include {0}", + replaceMatchWildcard: "Заменить wildcard @match на @include {0}", + replaceToMatch: "Заменить на @match {0}", prompt: { name: "Имя скрипта", namespace: "Пространство имён скрипта", @@ -483,11 +630,36 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), unwrap: "Позволяет пользовательскому скрипту обходить песочницу и напрямую внедряться и выполняться в нативной глобальной области видимости страницы.
Скрипт может напрямую получать доступ к реальным глобальным переменным страницы и изменять их, однако не сможет использовать привилегированные API пользовательских скриптов, такие как GM.*.
Обычно используется в сценариях, требующих глубокой интеграции с нативными скриптами страницы или при миграции с обычных скриптов страницы.", definition: "Особенность ScriptCat: URL файла `.d.ts`, используемого для автодополнения в редакторе", - antifeature: "Для маркетплейсов скриптов: опишите здесь нежелательные / спорные функции", + antifeature: `Связано с маркетплейсами скриптов: для нежелательных функций следует добавить это значение описания +referral-link: Этот скрипт изменяет или перенаправляет на реферальную ссылку автора +ads: Этот скрипт вставляет рекламу на посещаемые вами страницы +payment: Этот скрипт требует оплаты для нормального использования +miner: Этот скрипт содержит функции майнинга +membership: Этот скрипт требует регистрации членства для нормального использования +tracking: Этот скрипт отслеживает информацию о пользователе`.replace(/\n/g, "
"), updateURL: "URL для проверки обновлений скрипта", + updateurl: "URL для проверки обновлений скрипта", downloadURL: "URL для загрузки обновлений скрипта", + downloadurl: "URL для загрузки обновлений скрипта", supportURL: "Страница поддержки / отчёта об ошибках", + supporturl: "Страница поддержки / отчёта об ошибках", source: "Страница с исходным кодом скрипта", + homepageurl: "Домашняя страница скрипта", + iconurl: "Иконка скрипта", + icon64url: "Иконка скрипта 64x64", + scriptUrl: "URL пользовательского скрипта, на который ссылается скрипт подписки", + scripturl: "URL пользовательского скрипта, на который ссылается скрипт подписки", + storageName: "Имя хранилища значений скрипта для совместного использования одного хранилища несколькими скриптами", + storagename: "Имя хранилища значений скрипта для совместного использования одного хранилища несколькими скриптами", + tag: "Теги скрипта, разделённые запятыми или пробелами", + cloudCat: "Отмечает, что скрипт можно экспортировать в пакет облачного скрипта CloudCat", + cloudcat: "Отмечает, что скрипт можно экспортировать в пакет облачного скрипта CloudCat", + cloudServer: "Облачный сервис CloudCat, используемый скриптом", + cloudserver: "Облачный сервис CloudCat, используемый скриптом", + exportValue: "Значения хранилища скрипта для экспорта при экспорте как облачного скрипта", + exportvalue: "Значения хранилища скрипта для экспорта при экспорте как облачного скрипта", + exportCookie: "Cookie для экспорта при экспорте как облачного скрипта", + exportcookie: "Cookie для экспорта при экспорте как облачного скрипта", crontab: `Примеры crontab для планового запуска скриптов (не для облачных скриптов) * * * * * * Запуск каждую секунду * * * * * Запуск каждую минуту From 607bd477b28b8e765deba71ca1e6edfa09d6b7cb Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 23 May 2026 23:27:33 +0900 Subject: [PATCH 12/29] fix langs --- src/pkg/utils/monaco-editor/index.ts | 27 ++++-- src/pkg/utils/monaco-editor/langs.ts | 133 +++++---------------------- 2 files changed, 44 insertions(+), 116 deletions(-) diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index a525d0619..37a0b08bd 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -2,7 +2,7 @@ import { systemConfig } from "@App/pages/store/global"; import EventEmitter from "eventemitter3"; import { editor, languages, MarkerSeverity, type IRange } from "monaco-editor"; import { findGlobalInsertionInfo, updateGlobalCommentLine } from "./utils"; -import type { EditorLangCode, EditorPrompt } from "./langs"; +import type { EditorLangCode, EditorLangEntry } from "./langs"; import { asEditorLangEntry, editorLangs } from "./langs"; import { deferred } from "../utils"; @@ -42,12 +42,23 @@ type ScriptcatMonacoEnvironment = typeof window.MonacoEnvironment & { const linterWorkerDeferred = deferred(); const langPromise = systemConfig.getLanguage(); -let multiLang = asEditorLangEntry("en-US"); +let multiLang: EditorLangEntry; +type EditorLangEntryPrompt = typeof multiLang.prompt; +let promptByLowerCase: EditorLangEntryPrompt; + +const loadEditorLangEntry = (key: EditorLangCode) => { + multiLang = asEditorLangEntry(key); + promptByLowerCase = Object.fromEntries( + Object.entries(multiLang.prompt).map(([key, value]) => [key.toLowerCase(), value]) + ) as typeof multiLang.prompt; +}; + +loadEditorLangEntry("en-US"); const updateLang = (lang: string) => { lang = `${lang || ""}` as EditorLangCode | ""; const key = ((Object.hasOwn(editorLangs, lang) && lang) || "en-US") as EditorLangCode; - multiLang = asEditorLangEntry(key); + loadEditorLangEntry(key); }; langPromise.then((res) => updateLang(res)); @@ -193,7 +204,7 @@ const getConnectMetadataFixes = ({ prefix, tag, spacing, value, suffix }: Metada const hostName = value.slice(2); if (!/\.\w{2,}$/.test(hostName) || !isSimpleValidHost(hostName)) return []; - const titleTemplate = multiLang.replaceConnectWildcard; + const titleTemplate = multiLang.removeConnectWildcard; return [createMetadataFix(titleTemplate, hostName, `${prefix}${tag}${spacing}${hostName}${suffix}`)]; }; @@ -215,7 +226,7 @@ const getMatchMetadataFixes = ({ const includeSpacing = getIncludeSpacing(spacing, normalizedTag); const tldValue = `${match[1]}://${hostName}.tld${match[3] || ""}`; - const titleTemplate = multiLang.replaceMatchWildcard; + const titleTemplate = multiLang.replaceMatchTldWildcardWithInclude; return [ createMetadataFix(titleTemplate, tldValue, `${prefix}include${includeSpacing}${tldValue}${suffix}`), createMetadataFix(titleTemplate, value, `${prefix}include${includeSpacing}${value}${suffix}`), @@ -234,7 +245,7 @@ const getIncludeMetadataFixes = ({ if (!match || !host || host.endsWith(".*") || host.includes("**") || host.endsWith(".tld")) return []; if (host.split(".").every((e) => e === "*" || /^[\w-]+$/.test(e))) { const includeSpacing = getIncludeSpacing(spacing, normalizedTag); - const titleTemplate = multiLang.replaceToMatch; + const titleTemplate = multiLang.replaceIncludeWithMatch; return [createMetadataFix(titleTemplate, value, `${prefix}match ${includeSpacing}${value}${suffix}`)]; } return []; @@ -478,11 +489,11 @@ export function registerEditor() { const match = metaLinePattern.exec(line); if (match) { - const key = match[1] as keyof EditorPrompt; + const key = match[1].toLowerCase() as keyof EditorLangEntryPrompt; return { contents: [ { - value: multiLang.prompt[key] || multiLang.undefinedPrompt, + value: promptByLowerCase[key] || multiLang.undefinedPrompt, supportHtml: true, }, ], diff --git a/src/pkg/utils/monaco-editor/langs.ts b/src/pkg/utils/monaco-editor/langs.ts index da25810e1..ed2147e7e 100644 --- a/src/pkg/utils/monaco-editor/langs.ts +++ b/src/pkg/utils/monaco-editor/langs.ts @@ -7,9 +7,9 @@ export const editorLangs = { addEslintDisableNextLine: "添加 eslint-disable-next-line 注释", addEslintDisable: "添加 eslint-disable 注释", declareGlobal: "将 '{0}' 声明为全局变量 (/* global */)", - replaceConnectWildcard: "替换为 @connect {0}", - replaceMatchWildcard: "将通配符 @match 替换为 @include {0}", - replaceToMatch: "替换为 @match {0}", + removeConnectWildcard: "移除 @connect 通配符,改为 {0}", + replaceMatchTldWildcardWithInclude: "将 @match 顶级域名通配符改为 @include {0}", + replaceIncludeWithMatch: "将 @include 改为 @match {0}", prompt: { name: "脚本名称", namespace: "脚本命名空间", @@ -56,28 +56,16 @@ miner:该脚本存在利用用户资源但不为用户产生收益或收益极 membership:该脚本需要注册会员/关注公众号才能正常使用 tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), updateURL: "脚本检查更新的 url", - updateurl: "脚本检查更新的 url", downloadURL: "脚本更新的下载地址", - downloadurl: "脚本更新的下载地址", supportURL: "支持站点、bug 反馈页面", - supporturl: "支持站点、bug 反馈页面", source: "脚本源码页", - homepageurl: "脚本主页", - iconurl: "脚本图标", - icon64url: "64x64 大小的脚本图标", scriptUrl: "订阅脚本中引用的用户脚本地址", - scripturl: "订阅脚本中引用的用户脚本地址", storageName: "脚本值存储空间名称,用于让多个脚本共享同一个存储空间", - storagename: "脚本值存储空间名称,用于让多个脚本共享同一个存储空间", tag: "脚本标签,多个标签可用逗号或空格分隔", cloudCat: "标记脚本支持导出为 CloudCat 云端脚本包", - cloudcat: "标记脚本支持导出为 CloudCat 云端脚本包", cloudServer: "脚本使用的 CloudCat 云端服务", - cloudserver: "脚本使用的 CloudCat 云端服务", exportValue: "导出为云端脚本时需要导出的脚本存储值", - exportvalue: "导出为云端脚本时需要导出的脚本存储值", exportCookie: "导出为云端脚本时需要导出的 Cookie", - exportcookie: "导出为云端脚本时需要导出的 Cookie", crontab: `定时脚本 crontab 参考(不适用于云端脚本) * * * * * * 每秒运行一次 * * * * * 每分钟运行一次 @@ -105,9 +93,9 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), addEslintDisableNextLine: "Add eslint-disable-next-line Comment", addEslintDisable: "Add eslint-disable Comment", declareGlobal: "Declare '{0}' as a global variable (/* global */)", - replaceConnectWildcard: "Replace with @connect {0}", - replaceMatchWildcard: "Replace wildcard @match with @include {0}", - replaceToMatch: "Replace with @match {0}", + removeConnectWildcard: "Remove @connect wildcard: {0}", + replaceMatchTldWildcardWithInclude: "Replace @match TLD wildcard with @include {0}", + replaceIncludeWithMatch: "Replace @include with @match {0}", prompt: { name: "Script name", namespace: "Script namespace", @@ -153,28 +141,16 @@ miner: This script engages in mining activities membership: This script requires registration as a member to be used properly tracking: This script tracks your user information`.replace(/\n/g, "
"), updateURL: "URL used to check for script updates", - updateurl: "URL used to check for script updates", downloadURL: "URL used to download script updates", - downloadurl: "URL used to download script updates", supportURL: "Support site / bug report page", - supporturl: "Support site / bug report page", source: "Script source code page", - homepageurl: "Script homepage", - iconurl: "Script icon", - icon64url: "64x64 script icon", scriptUrl: "User script URL referenced by a subscription script", - scripturl: "User script URL referenced by a subscription script", storageName: "Script value storage name, used to share one storage area across multiple scripts", - storagename: "Script value storage name, used to share one storage area across multiple scripts", tag: "Script tags, separated by commas or spaces", cloudCat: "Marks the script as exportable to a CloudCat cloud script package", - cloudcat: "Marks the script as exportable to a CloudCat cloud script package", cloudServer: "CloudCat cloud service used by the script", - cloudserver: "CloudCat cloud service used by the script", exportValue: "Script storage values to export when exporting as a cloud script", - exportvalue: "Script storage values to export when exporting as a cloud script", exportCookie: "Cookies to export when exporting as a cloud script", - exportcookie: "Cookies to export when exporting as a cloud script", crontab: `Scheduled script crontab examples (not for cloud scripts) * * * * * * Run every second * * * * * Run every minute @@ -202,9 +178,9 @@ tracking: This script tracks your user information`.replace(/\n/g, "
"), addEslintDisableNextLine: "新增 eslint-disable-next-line 註解", addEslintDisable: "新增 eslint-disable 註解", declareGlobal: "將 '{0}' 宣告為全域變數 (/* global */)", - replaceConnectWildcard: "替換為 @connect {0}", - replaceMatchWildcard: "將萬用字元 @match 替換為 @include {0}", - replaceToMatch: "替換為 @match {0}", + removeConnectWildcard: "移除 @connect 萬用字元,改為 {0}", + replaceMatchTldWildcardWithInclude: "將 @match 頂級網域萬用字元改為 @include {0}", + replaceIncludeWithMatch: "將 @include 改為 @match {0}", prompt: { name: "腳本名稱", namespace: "腳本命名空間", @@ -250,28 +226,16 @@ miner:此腳本存在挖礦行為 membership:此腳本需要註冊會員才能正常使用 tracking:此腳本會追蹤您的使用者資訊`.replace(/\n/g, "
"), updateURL: "腳本檢查更新的 url", - updateurl: "腳本檢查更新的 url", downloadURL: "腳本更新的下載網址", - downloadurl: "腳本更新的下載網址", supportURL: "支援站點、錯誤回報頁面", - supporturl: "支援站點、錯誤回報頁面", source: "腳本原始碼頁面", - homepageurl: "腳本首頁", - iconurl: "腳本圖示", - icon64url: "64x64 大小的腳本圖示", scriptUrl: "訂閱腳本中引用的使用者腳本網址", - scripturl: "訂閱腳本中引用的使用者腳本網址", storageName: "腳本值儲存空間名稱,用於讓多個腳本共享同一個儲存空間", - storagename: "腳本值儲存空間名稱,用於讓多個腳本共享同一個儲存空間", tag: "腳本標籤,多個標籤可用逗號或空格分隔", cloudCat: "標記腳本支援匯出為 CloudCat 雲端腳本套件", - cloudcat: "標記腳本支援匯出為 CloudCat 雲端腳本套件", cloudServer: "腳本使用的 CloudCat 雲端服務", - cloudserver: "腳本使用的 CloudCat 雲端服務", exportValue: "匯出為雲端腳本時需要匯出的腳本儲存值", - exportvalue: "匯出為雲端腳本時需要匯出的腳本儲存值", exportCookie: "匯出為雲端腳本時需要匯出的 Cookie", - exportcookie: "匯出為雲端腳本時需要匯出的 Cookie", crontab: `排程腳本 crontab 參考(不適用於雲端腳本) * * * * * * 每秒執行一次 * * * * * 每分鐘執行一次 @@ -299,9 +263,9 @@ tracking:此腳本會追蹤您的使用者資訊`.replace(/\n/g, "
"), addEslintDisableNextLine: "eslint-disable-next-line コメントを追加", addEslintDisable: "eslint-disable コメントを追加", declareGlobal: "'{0}' をグローバル変数として宣言 (/* global */)", - replaceConnectWildcard: "@connect {0} に置換", - replaceMatchWildcard: "ワイルドカード @match を @include {0} に置換", - replaceToMatch: "@match {0} に置換", + removeConnectWildcard: "@connect のワイルドカードを削除: {0}", + replaceMatchTldWildcardWithInclude: "@match の TLD ワイルドカードを @include {0} に置換", + replaceIncludeWithMatch: "@include を @match {0} に置換", prompt: { name: "スクリプト名", namespace: "スクリプトの名前空間", @@ -347,28 +311,16 @@ miner:このスクリプトにはマイニング動作があります membership:このスクリプトは正常に使用するためにメンバー登録が必要です tracking:このスクリプトはユーザー情報を追跡します`.replace(/\n/g, "
"), updateURL: "スクリプト更新を確認する URL", - updateurl: "スクリプト更新を確認する URL", downloadURL: "スクリプト更新をダウンロードする URL", - downloadurl: "スクリプト更新をダウンロードする URL", supportURL: "サポートサイト・バグ報告ページ", - supporturl: "サポートサイト・バグ報告ページ", source: "スクリプトのソースコードページ", - homepageurl: "スクリプトのホームページ", - iconurl: "スクリプトのアイコン", - icon64url: "64x64 サイズのスクリプトアイコン", scriptUrl: "サブスクリプションスクリプトで参照するユーザースクリプト URL", - scripturl: "サブスクリプションスクリプトで参照するユーザースクリプト URL", storageName: "複数のスクリプトで同じ保存領域を共有するためのスクリプト値ストレージ名", - storagename: "複数のスクリプトで同じ保存領域を共有するためのスクリプト値ストレージ名", tag: "スクリプトタグ。複数のタグはカンマまたはスペースで区切ります", cloudCat: "スクリプトを CloudCat クラウドスクリプトパッケージとしてエクスポート可能にする印", - cloudcat: "スクリプトを CloudCat クラウドスクリプトパッケージとしてエクスポート可能にする印", cloudServer: "スクリプトが使用する CloudCat クラウドサービス", - cloudserver: "スクリプトが使用する CloudCat クラウドサービス", exportValue: "クラウドスクリプトとしてエクスポートする際に出力するスクリプト保存値", - exportvalue: "クラウドスクリプトとしてエクスポートする際に出力するスクリプト保存値", exportCookie: "クラウドスクリプトとしてエクスポートする際に出力する Cookie", - exportcookie: "クラウドスクリプトとしてエクスポートする際に出力する Cookie", crontab: `スケジュールスクリプトの crontab 例(クラウドスクリプトには非対応) * * * * * * 毎秒実行 * * * * * 毎分実行 @@ -396,9 +348,9 @@ tracking:このスクリプトはユーザー情報を追跡します`.replace addEslintDisableNextLine: "eslint-disable-next-line Kommentar hinzufügen", addEslintDisable: "eslint-disable Kommentar hinzufügen", declareGlobal: "'{0}' als globale Variable deklarieren (/* global */)", - replaceConnectWildcard: "Durch @connect {0} ersetzen", - replaceMatchWildcard: "Wildcard-@match durch @include {0} ersetzen", - replaceToMatch: "Durch @match {0} ersetzen", + removeConnectWildcard: "@connect-Wildcard entfernen: {0}", + replaceMatchTldWildcardWithInclude: "@match-TLD-Wildcard durch @include {0} ersetzen", + replaceIncludeWithMatch: "@include durch @match {0} ersetzen", prompt: { name: "Skriptname", namespace: "Skript-Namensraum", @@ -436,7 +388,8 @@ tracking:このスクリプトはユーザー情報を追跡します`.replace unwrap: "Ermöglicht es, das Benutzerskript ohne Sandbox-Kapselung direkt in den nativen globalen Gültigkeitsbereich der Seite zu injizieren und auszuführen.
Das Skript kann direkt auf die tatsächlichen globalen Variablen der Seite zugreifen und diese verändern, kann jedoch keine privilegierten Benutzerskript-APIs wie GM.* verwenden.
Wird häufig in Szenarien eingesetzt, die eine tiefe Interaktion mit nativen Seitenskripten erfordern oder bei der Migration von normalen Seitenskripten.", definition: "Nur für ScriptCat: URL zu einer `.d.ts`-Datei für Editor-Autovervollständigung", - antifeature: `Bezieht sich auf Script-Marktplätze: unerwünschte Funktionen sollten diesen Beschreibungswert enthalten + antifeature: + `Bezieht sich auf Script-Marktplätze: unerwünschte Funktionen sollten diesen Beschreibungswert enthalten referral-link: Dieses Skript modifiziert oder leitet zu den Affiliate-Links des Autors um ads: Dieses Skript fügt Werbung auf den von Ihnen besuchten Seiten ein payment: Dieses Skript erfordert eine Zahlung für die normale Nutzung @@ -444,28 +397,16 @@ miner: Dieses Skript hat Mining-Verhalten membership: Dieses Skript erfordert eine Mitgliedschaftsregistrierung für die normale Nutzung tracking: Dieses Skript verfolgt Ihre Benutzerinformationen`.replace(/\n/g, "
"), updateURL: "URL zur Aktualisierungsprüfung des Skripts", - updateurl: "URL zur Aktualisierungsprüfung des Skripts", downloadURL: "URL zum Herunterladen von Skriptaktualisierungen", - downloadurl: "URL zum Herunterladen von Skriptaktualisierungen", supportURL: "Support-Seite / Bugtracker", - supporturl: "Support-Seite / Bugtracker", source: "Quellcode-Seite des Skripts", - homepageurl: "Skript-Homepage", - iconurl: "Skript-Symbol", - icon64url: "64x64 Skript-Symbol", scriptUrl: "Benutzerskript-URL, die von einem Abonnement-Skript referenziert wird", - scripturl: "Benutzerskript-URL, die von einem Abonnement-Skript referenziert wird", storageName: "Speichername für Skriptwerte, um einen Speicherbereich mit mehreren Skripten zu teilen", - storagename: "Speichername für Skriptwerte, um einen Speicherbereich mit mehreren Skripten zu teilen", tag: "Skript-Tags, getrennt durch Kommas oder Leerzeichen", cloudCat: "Markiert das Skript als exportierbar in ein CloudCat-Cloud-Skriptpaket", - cloudcat: "Markiert das Skript als exportierbar in ein CloudCat-Cloud-Skriptpaket", cloudServer: "Vom Skript verwendeter CloudCat-Clouddienst", - cloudserver: "Vom Skript verwendeter CloudCat-Clouddienst", exportValue: "Skript-Speicherwerte, die beim Export als Cloud-Skript exportiert werden", - exportvalue: "Skript-Speicherwerte, die beim Export als Cloud-Skript exportiert werden", exportCookie: "Cookies, die beim Export als Cloud-Skript exportiert werden", - exportcookie: "Cookies, die beim Export als Cloud-Skript exportiert werden", crontab: `Beispiele für geplante Skripte (crontab, nicht für Cloud-Skripte) * * * * * * Jede Sekunde ausführen * * * * * Jede Minute ausführen @@ -493,9 +434,9 @@ tracking: Dieses Skript verfolgt Ihre Benutzerinformationen`.replace(/\n/g, "
"), updateURL: "URL dùng để kiểm tra cập nhật script", - updateurl: "URL dùng để kiểm tra cập nhật script", downloadURL: "URL tải về bản cập nhật script", - downloadurl: "URL tải về bản cập nhật script", supportURL: "Trang hỗ trợ / báo lỗi", - supporturl: "Trang hỗ trợ / báo lỗi", source: "Trang mã nguồn script", - homepageurl: "Trang chủ script", - iconurl: "Biểu tượng script", - icon64url: "Biểu tượng script kích thước 64x64", scriptUrl: "URL user script được tham chiếu bởi script đăng ký", - scripturl: "URL user script được tham chiếu bởi script đăng ký", storageName: "Tên vùng lưu trữ giá trị script, dùng để chia sẻ cùng một vùng lưu trữ giữa nhiều script", - storagename: "Tên vùng lưu trữ giá trị script, dùng để chia sẻ cùng một vùng lưu trữ giữa nhiều script", tag: "Thẻ script, phân tách bằng dấu phẩy hoặc khoảng trắng", cloudCat: "Đánh dấu script có thể xuất thành gói cloud script CloudCat", - cloudcat: "Đánh dấu script có thể xuất thành gói cloud script CloudCat", cloudServer: "Dịch vụ CloudCat cloud mà script sử dụng", - cloudserver: "Dịch vụ CloudCat cloud mà script sử dụng", exportValue: "Giá trị lưu trữ script cần xuất khi xuất thành cloud script", - exportvalue: "Giá trị lưu trữ script cần xuất khi xuất thành cloud script", exportCookie: "Cookie cần xuất khi xuất thành cloud script", - exportcookie: "Cookie cần xuất khi xuất thành cloud script", crontab: `Ví dụ crontab cho script chạy định kỳ (không áp dụng cho script trên cloud) * * * * * * Chạy mỗi giây * * * * * Chạy mỗi phút @@ -590,9 +519,9 @@ tracking: Script này theo dõi thông tin người dùng của bạn`.replace(/ addEslintDisableNextLine: "Добавить комментарий eslint-disable-next-line", addEslintDisable: "Добавить комментарий eslint-disable", declareGlobal: "Объявить '{0}' как глобальную переменную (/* global */)", - replaceConnectWildcard: "Заменить на @connect {0}", - replaceMatchWildcard: "Заменить wildcard @match на @include {0}", - replaceToMatch: "Заменить на @match {0}", + removeConnectWildcard: "Удалить wildcard @connect: {0}", + replaceMatchTldWildcardWithInclude: "Заменить TLD wildcard @match на @include {0}", + replaceIncludeWithMatch: "Заменить @include на @match {0}", prompt: { name: "Имя скрипта", namespace: "Пространство имён скрипта", @@ -638,28 +567,17 @@ miner: Этот скрипт содержит функции майнинга membership: Этот скрипт требует регистрации членства для нормального использования tracking: Этот скрипт отслеживает информацию о пользователе`.replace(/\n/g, "
"), updateURL: "URL для проверки обновлений скрипта", - updateurl: "URL для проверки обновлений скрипта", downloadURL: "URL для загрузки обновлений скрипта", - downloadurl: "URL для загрузки обновлений скрипта", supportURL: "Страница поддержки / отчёта об ошибках", - supporturl: "Страница поддержки / отчёта об ошибках", source: "Страница с исходным кодом скрипта", - homepageurl: "Домашняя страница скрипта", - iconurl: "Иконка скрипта", - icon64url: "Иконка скрипта 64x64", scriptUrl: "URL пользовательского скрипта, на который ссылается скрипт подписки", - scripturl: "URL пользовательского скрипта, на который ссылается скрипт подписки", - storageName: "Имя хранилища значений скрипта для совместного использования одного хранилища несколькими скриптами", - storagename: "Имя хранилища значений скрипта для совместного использования одного хранилища несколькими скриптами", + storageName: + "Имя хранилища значений скрипта для совместного использования одного хранилища несколькими скриптами", tag: "Теги скрипта, разделённые запятыми или пробелами", cloudCat: "Отмечает, что скрипт можно экспортировать в пакет облачного скрипта CloudCat", - cloudcat: "Отмечает, что скрипт можно экспортировать в пакет облачного скрипта CloudCat", cloudServer: "Облачный сервис CloudCat, используемый скриптом", - cloudserver: "Облачный сервис CloudCat, используемый скриптом", exportValue: "Значения хранилища скрипта для экспорта при экспорте как облачного скрипта", - exportvalue: "Значения хранилища скрипта для экспорта при экспорте как облачного скрипта", exportCookie: "Cookie для экспорта при экспорте как облачного скрипта", - exportcookie: "Cookie для экспорта при экспорте как облачного скрипта", crontab: `Примеры crontab для планового запуска скриптов (не для облачных скриптов) * * * * * * Запуск каждую секунду * * * * * Запуск каждую минуту @@ -681,7 +599,6 @@ tracking: Этот скрипт отслеживает информацию о } as const; export type EditorLangCode = keyof typeof editorLangs; -export type EditorPrompt = (typeof editorLangs)["zh-CN"]["prompt"]; export type EditorLangEntry = (typeof editorLangs)["zh-CN"]; export function asEditorLangEntry(key: T) { From 7417bd92a04a2b184c52227fdae1917981bd7ab1 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 23 May 2026 23:37:41 +0900 Subject: [PATCH 13/29] refactor --- src/pkg/utils/monaco-editor/index.ts | 184 +++++++++++++++------------ 1 file changed, 103 insertions(+), 81 deletions(-) diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index 37a0b08bd..54f198d0c 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -40,31 +40,32 @@ type ScriptcatMonacoEnvironment = typeof window.MonacoEnvironment & { // 注册 eslint worker(全局单例) const linterWorkerDeferred = deferred(); -const langPromise = systemConfig.getLanguage(); +const configuredLanguagePromise = systemConfig.getLanguage(); -let multiLang: EditorLangEntry; -type EditorLangEntryPrompt = typeof multiLang.prompt; -let promptByLowerCase: EditorLangEntryPrompt; +let currentEditorLang: EditorLangEntry; +type EditorLangEntryPrompt = typeof currentEditorLang.prompt; +let promptByMetadataTag: EditorLangEntryPrompt; -const loadEditorLangEntry = (key: EditorLangCode) => { - multiLang = asEditorLangEntry(key); - promptByLowerCase = Object.fromEntries( - Object.entries(multiLang.prompt).map(([key, value]) => [key.toLowerCase(), value]) - ) as typeof multiLang.prompt; +const loadEditorLangEntry = (languageCode: EditorLangCode) => { + currentEditorLang = asEditorLangEntry(languageCode); + promptByMetadataTag = Object.fromEntries( + Object.entries(currentEditorLang.prompt).map(([metadataTag, prompt]) => [metadataTag.toLowerCase(), prompt]) + ) as typeof currentEditorLang.prompt; }; loadEditorLangEntry("en-US"); -const updateLang = (lang: string) => { - lang = `${lang || ""}` as EditorLangCode | ""; - const key = ((Object.hasOwn(editorLangs, lang) && lang) || "en-US") as EditorLangCode; - loadEditorLangEntry(key); +const updateEditorLang = (language: string) => { + const requestedLanguageCode = `${language || ""}` as EditorLangCode | ""; + const supportedLanguageCode = ((Object.hasOwn(editorLangs, requestedLanguageCode) && requestedLanguageCode) || + "en-US") as EditorLangCode; + loadEditorLangEntry(supportedLanguageCode); }; -langPromise.then((res) => updateLang(res)); +configuredLanguagePromise.then((language) => updateEditorLang(language)); -systemConfig.addListener("language", (lang) => { - updateLang(lang); +systemConfig.addListener("language", (language) => { + updateEditorLang(language); }); export class LinterWorkerController { @@ -90,7 +91,7 @@ export class LinterWorkerController { } } -let isRegisterEditorDone = false; +let isEditorRegistered = false; const scriptcatMarkerOwner = "ScriptCat"; const eslintMarkerOwner = "ESLint"; @@ -113,8 +114,8 @@ const getMarkerCode = (marker: editor.IMarkerData) => { return typeof marker.code === "string" ? marker.code : marker.code.value; }; -const getEslintFixKey = (marker: editor.IMarkerData, code: string) => { - return `${code}|${marker.startLineNumber}|${marker.endLineNumber}|${marker.startColumn}|${marker.endColumn}`; +const getEslintFixKey = (marker: editor.IMarkerData, eslintRuleId: string) => { + return `${eslintRuleId}|${marker.startLineNumber}|${marker.endLineNumber}|${marker.startColumn}|${marker.endColumn}`; }; const createTextEditAction = ( @@ -140,7 +141,7 @@ const createLineReplacementAction = ( title: string, diagnostics: editor.IMarkerData[], lineNumber: number, - line: string, + lineText: string, text: string, isPreferred: boolean ) => { @@ -153,7 +154,7 @@ const createLineReplacementAction = ( startLineNumber: lineNumber, startColumn: 1, endLineNumber: lineNumber, - endColumn: line.length + 1, + endColumn: lineText.length + 1, }, text, }, @@ -171,11 +172,12 @@ const isSimpleValidHost = (hostName: string) => { } }; -const parseMetadataLine = (line: string): MetadataLineParts | null => { - const match = metadataFixPattern.exec(line); - if (!match) return null; +const parseMetadataLine = (lineText: string): MetadataLineParts | null => { + if (lineText.length < 6 || !lineText.includes("@")) return null; + const metadataMatch = metadataFixPattern.exec(lineText); + if (!metadataMatch) return null; - const [, prefix, tag, spacing, value, suffix] = match; + const [, prefix, tag, spacing, value, suffix] = metadataMatch; return { prefix, tag, @@ -204,7 +206,7 @@ const getConnectMetadataFixes = ({ prefix, tag, spacing, value, suffix }: Metada const hostName = value.slice(2); if (!/\.\w{2,}$/.test(hostName) || !isSimpleValidHost(hostName)) return []; - const titleTemplate = multiLang.removeConnectWildcard; + const titleTemplate = currentEditorLang.removeConnectWildcard; return [createMetadataFix(titleTemplate, hostName, `${prefix}${tag}${spacing}${hostName}${suffix}`)]; }; @@ -216,17 +218,22 @@ const getMatchMetadataFixes = ({ suffix, }: MetadataLineParts): MetadataLineFix[] => { if (!value || value.startsWith("/")) return []; - const match = matchMetadataPattern.exec(value); - const host = match?.[2]; - if (!match || !host?.endsWith(".*") || host.includes("**") || host.includes("\\")) return []; - - const hostName = host.slice(0, -2); + const metadataValueMatch = matchMetadataPattern.exec(value); + if (!metadataValueMatch || !metadataValueMatch[2]) return []; + const hostPattern = metadataValueMatch[2]; + const wildcardNormalizedHost = hostPattern + .split(".") + .map((hostSegment) => (hostSegment.includes("*") ? "*" : hostSegment)) + .join("."); + if (!wildcardNormalizedHost.endsWith(".*") || hostPattern.includes("**") || hostPattern.includes("\\")) return []; + + const hostName = hostPattern.slice(0, -2); if (!isSimpleValidHost(hostName.replace(/\*/g, "x"))) return []; const includeSpacing = getIncludeSpacing(spacing, normalizedTag); - const tldValue = `${match[1]}://${hostName}.tld${match[3] || ""}`; + const tldValue = `${metadataValueMatch[1]}://${hostName}.tld${metadataValueMatch[3] || ""}`; - const titleTemplate = multiLang.replaceMatchTldWildcardWithInclude; + const titleTemplate = currentEditorLang.replaceMatchTldWildcardWithInclude; return [ createMetadataFix(titleTemplate, tldValue, `${prefix}include${includeSpacing}${tldValue}${suffix}`), createMetadataFix(titleTemplate, value, `${prefix}include${includeSpacing}${value}${suffix}`), @@ -240,19 +247,26 @@ const getIncludeMetadataFixes = ({ value, suffix, }: MetadataLineParts): MetadataLineFix[] => { - const match = matchMetadataPattern.exec(value); - const host = match?.[2]; - if (!match || !host || host.endsWith(".*") || host.includes("**") || host.endsWith(".tld")) return []; - if (host.split(".").every((e) => e === "*" || /^[\w-]+$/.test(e))) { + const metadataValueMatch = matchMetadataPattern.exec(value); + const hostPattern = metadataValueMatch?.[2]; + if ( + !metadataValueMatch || + !hostPattern || + hostPattern.endsWith(".*") || + hostPattern.includes("**") || + hostPattern.endsWith(".tld") + ) + return []; + if (hostPattern.split(".").every((hostSegment) => hostSegment === "*" || /^[\w-]+$/.test(hostSegment))) { const includeSpacing = getIncludeSpacing(spacing, normalizedTag); - const titleTemplate = multiLang.replaceIncludeWithMatch; + const titleTemplate = currentEditorLang.replaceIncludeWithMatch; return [createMetadataFix(titleTemplate, value, `${prefix}match ${includeSpacing}${value}${suffix}`)]; } return []; }; -const getMetadataLineFixes = (line: string): MetadataLineFix[] => { - const parts = parseMetadataLine(line); +const getMetadataLineFixes = (lineText: string): MetadataLineFix[] => { + const parts = parseMetadataLine(lineText); if (!parts) return []; switch (parts.normalizedTag) { @@ -270,18 +284,26 @@ const getMetadataLineFixes = (line: string): MetadataLineFix[] => { const getMetadataLineActions = ( model: editor.ITextModel, lineNumber: number, - line: string, + lineText: string, markers: editor.IMarkerData[] ): languages.CodeAction[] => { - const fixes = getMetadataLineFixes(line); - if (fixes.length === 0) return []; + const metadataFixes = getMetadataLineFixes(lineText); + if (metadataFixes.length === 0) return []; const diagnostics = markers.filter( (marker) => marker.source === scriptcatMarkerOwner && marker.startLineNumber === lineNumber ); - return fixes.map((fix, index) => - createLineReplacementAction(model, fix.title, diagnostics, lineNumber, line, fix.text, index === 0) + return metadataFixes.map((metadataFix, index) => + createLineReplacementAction( + model, + metadataFix.title, + diagnostics, + lineNumber, + lineText, + metadataFix.text, + index === 0 + ) ); }; @@ -304,15 +326,15 @@ const getGlobalDeclarationTextEdit = (model: editor.ITextModel, globalName: stri }; } - const oldLine = model.getLineContent(globalLine); + const existingGlobalLineText = model.getLineContent(globalLine); return { range: { startLineNumber: globalLine, startColumn: 1, endLineNumber: globalLine, - endColumn: oldLine.length + 1, + endColumn: existingGlobalLineText.length + 1, }, - text: updateGlobalCommentLine(oldLine, globalName), + text: updateGlobalCommentLine(existingGlobalLineText, globalName), }; }; @@ -322,37 +344,37 @@ const getMarkerCodeActions = ( eslintFixMap?: Map ): languages.CodeAction[] => { if (marker.source !== eslintMarkerOwner) return []; - const code = getMarkerCode(marker); - if (!code) return []; + const eslintRuleId = getMarkerCode(marker); + if (!eslintRuleId) return []; const actions: languages.CodeAction[] = []; - const fix = eslintFixMap?.get(getEslintFixKey(marker, code)); - if (fix) { + const eslintFix = eslintFixMap?.get(getEslintFixKey(marker, eslintRuleId)); + if (eslintFix) { actions.push( createTextEditAction( model, - multiLang.quickfix.replace("{0}", code), + currentEditorLang.quickfix.replace("{0}", eslintRuleId), [marker], { - range: fix.range, - text: fix.text, + range: eslintFix.range, + text: eslintFix.text, }, true ) ); } - let canApplyEslintSingleLineDisable = true; + let canAddEslintDisableNextLine = true; - switch (code) { + switch (eslintRuleId) { case "no-undef": { const globalName = getNoUndefGlobalName(marker); if (globalName) { actions.push( createTextEditAction( model, - multiLang.declareGlobal.replace("{0}", globalName), + currentEditorLang.declareGlobal.replace("{0}", globalName), [marker], getGlobalDeclarationTextEdit(model, globalName), false @@ -364,14 +386,14 @@ const getMarkerCodeActions = ( case "userscripts/align-attributes": case "userscripts/better-use-match": case "userscripts/no-invalid-headers": - canApplyEslintSingleLineDisable = false; + canAddEslintDisableNextLine = false; } - if (canApplyEslintSingleLineDisable) { + if (canAddEslintDisableNextLine) { actions.push( createTextEditAction( model, - multiLang.addEslintDisableNextLine, + currentEditorLang.addEslintDisableNextLine, [marker], { range: { @@ -380,7 +402,7 @@ const getMarkerCodeActions = ( startColumn: 1, endColumn: 1, }, - text: `// eslint-disable-next-line ${code}\n`, + text: `// eslint-disable-next-line ${eslintRuleId}\n`, }, true ) @@ -389,7 +411,7 @@ const getMarkerCodeActions = ( actions.push( createTextEditAction( model, - multiLang.addEslintDisable, + currentEditorLang.addEslintDisable, [marker], { range: { @@ -398,7 +420,7 @@ const getMarkerCodeActions = ( endLineNumber: 1, endColumn: 1, }, - text: `/* eslint-disable ${code} */\n`, + text: `/* eslint-disable ${eslintRuleId} */\n`, }, true ) @@ -413,8 +435,8 @@ const updateScriptcatMetadataMarkers = (model: editor.ITextModel) => { const markers: editor.IMarkerData[] = []; const lineCount = model.getLineCount(); for (let lineNumber = 1; lineNumber <= lineCount; lineNumber += 1) { - const line = model.getLineContent(lineNumber); - const metadataLineFixes = getMetadataLineFixes(line); + const lineText = model.getLineContent(lineNumber); + const metadataLineFixes = getMetadataLineFixes(lineText); if (metadataLineFixes.length === 0) continue; markers.push({ @@ -424,7 +446,7 @@ const updateScriptcatMetadataMarkers = (model: editor.ITextModel) => { startLineNumber: lineNumber, startColumn: 1, endLineNumber: lineNumber, - endColumn: line.length + 1, + endColumn: lineText.length + 1, }); } @@ -432,15 +454,15 @@ const updateScriptcatMetadataMarkers = (model: editor.ITextModel) => { }; const registerScriptcatMetadataMarkerProvider = () => { - const registerModel = (model: editor.ITextModel) => { + const registerMetadataModel = (model: editor.ITextModel) => { updateScriptcatMetadataMarkers(model); model.onDidChangeContent(() => { updateScriptcatMetadataMarkers(model); }); }; - editor.getModels().forEach(registerModel); - editor.onDidCreateModel(registerModel); + editor.getModels().forEach(registerMetadataModel); + editor.onDidCreateModel(registerMetadataModel); }; /** @@ -449,8 +471,8 @@ const registerScriptcatMetadataMarkerProvider = () => { */ export function registerEditor() { // 避免重复注册 - if (isRegisterEditorDone) return; - isRegisterEditorDone = true; + if (isEditorRegistered) return; + isEditorRegistered = true; // worker 初始化:复用已有 worker 或创建新的 const existingEnvironment = getMonacoEnvironment(); @@ -485,23 +507,23 @@ export function registerEditor() { languages.registerHoverProvider("javascript", { provideHover: (model, position) => { - const line = model.getLineContent(position.lineNumber); - const match = metaLinePattern.exec(line); + const lineText = model.getLineContent(position.lineNumber); + const metadataCommentMatch = metaLinePattern.exec(lineText); - if (match) { - const key = match[1].toLowerCase() as keyof EditorLangEntryPrompt; + if (metadataCommentMatch) { + const metadataTag = metadataCommentMatch[1].toLowerCase() as keyof EditorLangEntryPrompt; return { contents: [ { - value: promptByLowerCase[key] || multiLang.undefinedPrompt, + value: promptByMetadataTag[metadataTag] || currentEditorLang.undefinedPrompt, supportHtml: true, }, ], }; } - if (/==UserScript==/.test(line)) { - return { contents: [{ value: multiLang.thisIsAUserScript }] }; + if (/==UserScript==/.test(lineText)) { + return { contents: [{ value: currentEditorLang.thisIsAUserScript }] }; } return null; @@ -513,9 +535,9 @@ export function registerEditor() { { provideCodeActions: (model /** ITextModel */, range /** Range */, context /** CodeActionContext */) => { const eslintFixMap = getMonacoEnvironment()?.eslintFixMap; - const line = model.getLineContent(range.startLineNumber); + const lineText = model.getLineContent(range.startLineNumber); const actions = [ - ...getMetadataLineActions(model, range.startLineNumber, line, context.markers), + ...getMetadataLineActions(model, range.startLineNumber, lineText, context.markers), ...context.markers.flatMap((marker) => getMarkerCodeActions(model, marker, eslintFixMap)), ]; From bc759ace35fdf64caa1c6feec0f7aa224fea763b Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 23 May 2026 23:41:20 +0900 Subject: [PATCH 14/29] fix --- src/pkg/utils/monaco-editor/index.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index 54f198d0c..9450b4b08 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -200,6 +200,14 @@ const getIncludeSpacing = (spacing: string, tag: string) => { return lenDiff > 0 && spacing.length > lenDiff ? spacing.slice(0, -lenDiff) : spacing; }; +const normalizeHost = (hostPattern: string) => { + const wildcardNormalizedHost = hostPattern + .split(".") + .map((hostSegment) => (hostSegment.includes("*") ? "*" : hostSegment)) + .join("."); + return wildcardNormalizedHost; +}; + const getConnectMetadataFixes = ({ prefix, tag, spacing, value, suffix }: MetadataLineParts): MetadataLineFix[] => { if (!value.startsWith("*.") || value.includes("**")) return []; @@ -221,10 +229,7 @@ const getMatchMetadataFixes = ({ const metadataValueMatch = matchMetadataPattern.exec(value); if (!metadataValueMatch || !metadataValueMatch[2]) return []; const hostPattern = metadataValueMatch[2]; - const wildcardNormalizedHost = hostPattern - .split(".") - .map((hostSegment) => (hostSegment.includes("*") ? "*" : hostSegment)) - .join("."); + const wildcardNormalizedHost = normalizeHost(hostPattern); if (!wildcardNormalizedHost.endsWith(".*") || hostPattern.includes("**") || hostPattern.includes("\\")) return []; const hostName = hostPattern.slice(0, -2); @@ -249,15 +254,16 @@ const getIncludeMetadataFixes = ({ }: MetadataLineParts): MetadataLineFix[] => { const metadataValueMatch = matchMetadataPattern.exec(value); const hostPattern = metadataValueMatch?.[2]; + const wildcardNormalizedHost = normalizeHost(hostPattern); if ( !metadataValueMatch || !hostPattern || - hostPattern.endsWith(".*") || + wildcardNormalizedHost.endsWith(".*") || hostPattern.includes("**") || hostPattern.endsWith(".tld") ) return []; - if (hostPattern.split(".").every((hostSegment) => hostSegment === "*" || /^[\w-]+$/.test(hostSegment))) { + if (wildcardNormalizedHost.split(".").every((hostSegment) => hostSegment === "*" || /^[\w-]+$/.test(hostSegment))) { const includeSpacing = getIncludeSpacing(spacing, normalizedTag); const titleTemplate = currentEditorLang.replaceIncludeWithMatch; return [createMetadataFix(titleTemplate, value, `${prefix}match ${includeSpacing}${value}${suffix}`)]; From 87226668d1e6af164de60afc01a2e6a5b7296536 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 23 May 2026 23:41:51 +0900 Subject: [PATCH 15/29] fix --- src/pkg/utils/monaco-editor/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index 9450b4b08..8a3b680c5 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -254,7 +254,7 @@ const getIncludeMetadataFixes = ({ }: MetadataLineParts): MetadataLineFix[] => { const metadataValueMatch = matchMetadataPattern.exec(value); const hostPattern = metadataValueMatch?.[2]; - const wildcardNormalizedHost = normalizeHost(hostPattern); + const wildcardNormalizedHost = hostPattern ? normalizeHost(hostPattern) : ""; if ( !metadataValueMatch || !hostPattern || From f5d44e70bc75cb19b144a8e0facf857466e9e213 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 23 May 2026 23:44:24 +0900 Subject: [PATCH 16/29] fix --- src/pkg/utils/monaco-editor/index.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index 8a3b680c5..f3440a203 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -230,9 +230,15 @@ const getMatchMetadataFixes = ({ if (!metadataValueMatch || !metadataValueMatch[2]) return []; const hostPattern = metadataValueMatch[2]; const wildcardNormalizedHost = normalizeHost(hostPattern); - if (!wildcardNormalizedHost.endsWith(".*") || hostPattern.includes("**") || hostPattern.includes("\\")) return []; + if ( + !wildcardNormalizedHost.endsWith(".*") || + !hostPattern.includes(".") || + hostPattern.includes("**") || + hostPattern.includes("\\") + ) + return []; - const hostName = hostPattern.slice(0, -2); + const hostName = hostPattern.slice(0, hostPattern.lastIndexOf(".")); if (!isSimpleValidHost(hostName.replace(/\*/g, "x"))) return []; const includeSpacing = getIncludeSpacing(spacing, normalizedTag); From 9308a752d98665c0f7871a89ba99a60e3362f90c Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 24 May 2026 00:02:39 +0900 Subject: [PATCH 17/29] release the strict linter to avoid users seeing so many errors in userscript. --- packages/eslint/linter-config.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/eslint/linter-config.ts b/packages/eslint/linter-config.ts index d6b57a1c4..811fd4bb8 100644 --- a/packages/eslint/linter-config.ts +++ b/packages/eslint/linter-config.ts @@ -21,12 +21,12 @@ const config = { rules: { "constructor-super": ["error"], "for-direction": ["error"], - "getter-return": ["error"], + "getter-return": ["warn"], // implicitly means return undefined "no-async-promise-executor": ["error"], "no-case-declarations": ["error"], "no-class-assign": ["error"], "no-compare-neg-zero": ["error"], - "no-cond-assign": ["error"], + "no-cond-assign": ["warn"], // this is common writing style in JavaScript "no-const-assign": ["error"], "no-constant-condition": ["error"], "no-control-regex": ["error"], @@ -37,7 +37,7 @@ const config = { "no-dupe-else-if": ["error"], "no-dupe-keys": ["error"], "no-duplicate-case": ["error"], - "no-empty": ["error"], + "no-empty": ["error", { allowEmptyCatch: true }], "no-empty-character-class": ["error"], "no-empty-pattern": ["error"], "no-ex-assign": ["error"], @@ -45,7 +45,7 @@ const config = { "no-extra-semi": ["error"], "no-fallthrough": ["error"], "no-func-assign": ["error"], - "no-global-assign": ["error"], + "no-global-assign": ["warn"], // we always modify global variable in UserScript "no-import-assign": ["error"], "no-inner-declarations": ["error"], "no-invalid-regexp": ["error"], @@ -58,10 +58,10 @@ const config = { "no-obj-calls": ["error"], "no-octal": ["error"], "no-prototype-builtins": ["error"], - "no-redeclare": ["error"], + "no-redeclare": ["error", { builtinGlobals: false }], "no-regex-spaces": ["error"], "no-self-assign": ["error"], - "no-setter-return": ["error"], + "no-setter-return": ["warn"], // sometimes developers like to return true in setter "no-shadow-restricted-names": ["error"], "no-sparse-arrays": ["error"], "no-this-before-super": ["error"], @@ -75,7 +75,7 @@ const config = { "no-unused-vars": ["warn"], "no-useless-backreference": ["error"], "no-useless-catch": ["error"], - "no-useless-escape": ["error"], + "no-useless-escape": ["error", { allowRegexCharacters: ["-", "&", "/"] }], "no-with": ["error"], "require-yield": ["error"], "use-isnan": ["error"], From 6ea667b231c84072d1b575d3f8320d5e2654119c Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 24 May 2026 00:16:18 +0900 Subject: [PATCH 18/29] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20eslint=20fix=20cache?= =?UTF-8?q?=20=E5=BC=82=E5=B8=B8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/components/CodeEditor/index.tsx | 11 ++-- .../monaco-editor/eslintFixCache.test.ts | 54 +++++++++++++++++++ src/pkg/utils/monaco-editor/eslintFixCache.ts | 34 ++++++++++++ src/pkg/utils/monaco-editor/index.ts | 14 ++--- 4 files changed, 99 insertions(+), 14 deletions(-) create mode 100644 src/pkg/utils/monaco-editor/eslintFixCache.test.ts create mode 100644 src/pkg/utils/monaco-editor/eslintFixCache.ts diff --git a/src/pages/components/CodeEditor/index.tsx b/src/pages/components/CodeEditor/index.tsx index ed23bcffe..16fe9d9de 100644 --- a/src/pages/components/CodeEditor/index.tsx +++ b/src/pages/components/CodeEditor/index.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useImperativeHandle, useRef, useState } from "react"; import { systemConfig } from "@App/pages/store/global"; import { LinterWorkerController, registerEditor } from "@App/pkg/utils/monaco-editor"; import { fnPlaceHolder } from "@App/pages/store/AppContext"; +import { clearModelEslintFixes, getModelEslintFixKey } from "@App/pkg/utils/monaco-editor/eslintFixCache"; fnPlaceHolder.setEditorTheme = (theme: string) => editor.setTheme(theme); @@ -259,13 +260,13 @@ const CodeEditor = React.forwardRef<{ editor: editor.IStandaloneCodeEditor | und editor.setModelMarkers(model, "ESLint", message.markers); - // 更新 eslint-fix 快取(每次替换整个 map,避免已修复问题的过期条目残留) + // 更新当前 model 的 eslint-fix 快取,避免多个脚本编辑器互相覆盖 quick-fix。 const eslintFixMap = (window.MonacoEnvironment as any)?.eslintFixMap; if (eslintFixMap) { - eslintFixMap.clear(); + clearModelEslintFixes(eslintFixMap, model); message.markers.forEach((m: TMarker) => { if (m.fix) { - const key = `${m.code.value}|${m.startLineNumber}|${m.endLineNumber}|${m.startColumn}|${m.endColumn}`; + const key = getModelEslintFixKey(model, m.code.value, m); eslintFixMap.set(key, m.fix); } }); @@ -288,6 +289,10 @@ const CodeEditor = React.forwardRef<{ editor: editor.IStandaloneCodeEditor | und timer = null; } changeListener.dispose(); + const eslintFixMap = (window.MonacoEnvironment as any)?.eslintFixMap; + if (eslintFixMap) { + clearModelEslintFixes(eslintFixMap, model); + } LinterWorkerController.hookRemoveListener("message", messageHandler); }; }, [monacoEditor, enableEslint, eslintConfig, id]); diff --git a/src/pkg/utils/monaco-editor/eslintFixCache.test.ts b/src/pkg/utils/monaco-editor/eslintFixCache.test.ts new file mode 100644 index 000000000..06d71fb39 --- /dev/null +++ b/src/pkg/utils/monaco-editor/eslintFixCache.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import type { editor } from "monaco-editor"; +import { clearModelEslintFixes, getModelEslintFixKey, type EslintFix } from "./eslintFixCache"; + +const createMockModel = (uri: string): editor.ITextModel => + ({ + uri: { + toString: () => uri, + }, + }) as editor.ITextModel; + +const marker = { + startLineNumber: 1, + endLineNumber: 5, + startColumn: 1, + endColumn: 19, +}; + +const fix: EslintFix = { + range: { + startLineNumber: 2, + endLineNumber: 2, + startColumn: 9, + endColumn: 10, + }, + text: " ", +}; + +describe("eslint fix cache", () => { + it("uses the model uri in fix keys so identical markers from different editors do not collide", () => { + const modelA = createMockModel("inmemory://model/a"); + const modelB = createMockModel("inmemory://model/b"); + + expect(getModelEslintFixKey(modelA, "userscripts/align-attributes", marker)).not.toBe( + getModelEslintFixKey(modelB, "userscripts/align-attributes", marker) + ); + }); + + it("clears only fixes for the current model", () => { + const modelA = createMockModel("inmemory://model/a"); + const modelB = createMockModel("inmemory://model/b"); + const map = new Map(); + const keyA = getModelEslintFixKey(modelA, "userscripts/align-attributes", marker); + const keyB = getModelEslintFixKey(modelB, "userscripts/align-attributes", marker); + + map.set(keyA, fix); + map.set(keyB, fix); + + clearModelEslintFixes(map, modelA); + + expect(map.has(keyA)).toBe(false); + expect(map.has(keyB)).toBe(true); + }); +}); diff --git a/src/pkg/utils/monaco-editor/eslintFixCache.ts b/src/pkg/utils/monaco-editor/eslintFixCache.ts new file mode 100644 index 000000000..1562bf527 --- /dev/null +++ b/src/pkg/utils/monaco-editor/eslintFixCache.ts @@ -0,0 +1,34 @@ +import type { editor, IRange } from "monaco-editor"; + +export type EslintFix = { + range: IRange; + text: string; +}; + +type EslintFixMarkerPosition = Pick< + editor.IMarkerData, + "startLineNumber" | "endLineNumber" | "startColumn" | "endColumn" +>; + +export const getEslintFixKey = ( + modelUri: string, + eslintRuleId: string, + marker: EslintFixMarkerPosition +) => { + return `${modelUri}|${eslintRuleId}|${marker.startLineNumber}|${marker.endLineNumber}|${marker.startColumn}|${marker.endColumn}`; +}; + +export const getModelEslintFixKey = ( + model: editor.ITextModel, + eslintRuleId: string, + marker: EslintFixMarkerPosition +) => getEslintFixKey(model.uri.toString(), eslintRuleId, marker); + +export const clearModelEslintFixes = (eslintFixMap: Map, model: editor.ITextModel) => { + const prefix = `${model.uri.toString()}|`; + for (const key of eslintFixMap.keys()) { + if (key.startsWith(prefix)) { + eslintFixMap.delete(key); + } + } +}; diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index f3440a203..328c1ac76 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -1,20 +1,16 @@ import { systemConfig } from "@App/pages/store/global"; import EventEmitter from "eventemitter3"; -import { editor, languages, MarkerSeverity, type IRange } from "monaco-editor"; +import { editor, languages, MarkerSeverity } from "monaco-editor"; import { findGlobalInsertionInfo, updateGlobalCommentLine } from "./utils"; import type { EditorLangCode, EditorLangEntry } from "./langs"; import { asEditorLangEntry, editorLangs } from "./langs"; import { deferred } from "../utils"; +import { type EslintFix, getModelEslintFixKey } from "./eslintFixCache"; interface ILinterWorker extends Worker { myLinterHook: EventEmitter; } -type EslintFix = { - range: IRange; - text: string; -}; - type MetadataLineParts = { prefix: string; tag: string; @@ -114,10 +110,6 @@ const getMarkerCode = (marker: editor.IMarkerData) => { return typeof marker.code === "string" ? marker.code : marker.code.value; }; -const getEslintFixKey = (marker: editor.IMarkerData, eslintRuleId: string) => { - return `${eslintRuleId}|${marker.startLineNumber}|${marker.endLineNumber}|${marker.startColumn}|${marker.endColumn}`; -}; - const createTextEditAction = ( model: editor.ITextModel, title: string, @@ -361,7 +353,7 @@ const getMarkerCodeActions = ( const actions: languages.CodeAction[] = []; - const eslintFix = eslintFixMap?.get(getEslintFixKey(marker, eslintRuleId)); + const eslintFix = eslintFixMap?.get(getModelEslintFixKey(model, eslintRuleId, marker)); if (eslintFix) { actions.push( createTextEditAction( From c521ab81f49e09611dd3256602431c4e2805bd4d Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 24 May 2026 00:23:17 +0900 Subject: [PATCH 19/29] Update linter.worker.ts --- src/linter.worker.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/linter.worker.ts b/src/linter.worker.ts index d8a03a11c..416a3f934 100644 --- a/src/linter.worker.ts +++ b/src/linter.worker.ts @@ -7,7 +7,14 @@ const { rules } = require("eslint-plugin-userscripts"); const linter = new Linter({ configType: "eslintrc" }); // ScriptCat 不适用 - 有必要存在的用法 -const omitKeys = new Set(["better-use-match"]); +const omitKeys = new Set([ + // 不是所有 @include 都要改为 @match + "better-use-match", + // 不是所有语言的 @name 都要放在最前 + "require-name", + // ScriptCat 不用指定 ==UserScript== 放最前。在 ==UserScript== 前面可以写其他注释 + "no-invalid-metadata", +]); // 额外定义 userscripts 规则 const formatRules = Object.fromEntries( From f26092a08acecdb96578de66660eb9e23e8e103e Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 24 May 2026 00:32:17 +0900 Subject: [PATCH 20/29] lint --- src/pkg/utils/monaco-editor/eslintFixCache.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/pkg/utils/monaco-editor/eslintFixCache.ts b/src/pkg/utils/monaco-editor/eslintFixCache.ts index 1562bf527..0f3479b3f 100644 --- a/src/pkg/utils/monaco-editor/eslintFixCache.ts +++ b/src/pkg/utils/monaco-editor/eslintFixCache.ts @@ -10,19 +10,12 @@ type EslintFixMarkerPosition = Pick< "startLineNumber" | "endLineNumber" | "startColumn" | "endColumn" >; -export const getEslintFixKey = ( - modelUri: string, - eslintRuleId: string, - marker: EslintFixMarkerPosition -) => { +export const getEslintFixKey = (modelUri: string, eslintRuleId: string, marker: EslintFixMarkerPosition) => { return `${modelUri}|${eslintRuleId}|${marker.startLineNumber}|${marker.endLineNumber}|${marker.startColumn}|${marker.endColumn}`; }; -export const getModelEslintFixKey = ( - model: editor.ITextModel, - eslintRuleId: string, - marker: EslintFixMarkerPosition -) => getEslintFixKey(model.uri.toString(), eslintRuleId, marker); +export const getModelEslintFixKey = (model: editor.ITextModel, eslintRuleId: string, marker: EslintFixMarkerPosition) => + getEslintFixKey(model.uri.toString(), eslintRuleId, marker); export const clearModelEslintFixes = (eslintFixMap: Map, model: editor.ITextModel) => { const prefix = `${model.uri.toString()}|`; From fd374e0dff4efc0e728275691d0445db05cbf47a Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 24 May 2026 06:21:47 +0900 Subject: [PATCH 21/29] update comments --- src/linter.worker.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/linter.worker.ts b/src/linter.worker.ts index 416a3f934..310847f9e 100644 --- a/src/linter.worker.ts +++ b/src/linter.worker.ts @@ -10,9 +10,9 @@ const linter = new Linter({ configType: "eslintrc" }); const omitKeys = new Set([ // 不是所有 @include 都要改为 @match "better-use-match", - // 不是所有语言的 @name 都要放在最前 + // 不是 @name @name:en @name:zh-CN @name:zh-TW @name:ja 都要放在最前 "require-name", - // ScriptCat 不用指定 ==UserScript== 放最前。在 ==UserScript== 前面可以写其他注释 + // ScriptCat 不用指定 ==UserScript== 放最前。在 ==UserScript== 前面可以写其他注释, 例如是 License "no-invalid-metadata", ]); From d56da1da90b678830d4112118a9314d95f715150 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 24 May 2026 06:33:03 +0900 Subject: [PATCH 22/29] require to import --- src/linter.worker.ts | 9 ++++----- src/types/eslint-linter-browserify.d.ts | 3 +++ 2 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 src/types/eslint-linter-browserify.d.ts diff --git a/src/linter.worker.ts b/src/linter.worker.ts index 310847f9e..78ae72e47 100644 --- a/src/linter.worker.ts +++ b/src/linter.worker.ts @@ -1,6 +1,5 @@ -/* eslint-disable @typescript-eslint/no-require-imports */ -const { Linter } = require("eslint-linter-browserify"); -const { rules } = require("eslint-plugin-userscripts"); +import { Linter } from "eslint-linter-browserify"; +import { rules } from "eslint-plugin-userscripts"; // eslint语法检查,使用webworker @@ -17,7 +16,7 @@ const omitKeys = new Set([ ]); // 额外定义 userscripts 规则 -const formatRules = Object.fromEntries( +const formatRules: typeof rules = Object.fromEntries( Object.entries(rules).map(([key, metas]) => [ "userscripts/" + key, omitKeys.has(key) @@ -30,7 +29,7 @@ const formatRules = Object.fromEntries( : metas, ]) ); -linter.defineRules(formatRules as any); +linter.defineRules(formatRules); const getRules = linter.getRules(); diff --git a/src/types/eslint-linter-browserify.d.ts b/src/types/eslint-linter-browserify.d.ts new file mode 100644 index 000000000..1af63e438 --- /dev/null +++ b/src/types/eslint-linter-browserify.d.ts @@ -0,0 +1,3 @@ +declare module "eslint-linter-browserify" { + export { Linter } from "eslint"; +} From 7a4280cae2e9fc8b2742780a5ca39baa1906c1b1 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 24 May 2026 06:53:50 +0900 Subject: [PATCH 23/29] adjust linter --- packages/eslint/linter-config.ts | 13 ++++++++++--- src/linter.worker.ts | 22 +--------------------- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/packages/eslint/linter-config.ts b/packages/eslint/linter-config.ts index 811fd4bb8..10d87116a 100644 --- a/packages/eslint/linter-config.ts +++ b/packages/eslint/linter-config.ts @@ -1,5 +1,4 @@ -/* eslint-disable @typescript-eslint/no-require-imports */ -const { configs } = require("eslint-plugin-userscripts"); +import { configs } from "eslint-plugin-userscripts"; // 默认规则 const config = { @@ -81,7 +80,7 @@ const config = { "use-isnan": ["error"], "valid-typeof": ["error"], ...configs.recommended.rules, - }, + } as Record, env: { es6: true, browser: true, @@ -93,5 +92,13 @@ const config = { config.rules["userscripts/align-attributes"] = ["warn", 2]; config.rules["userscripts/require-download-url"] = ["warn"]; +// ScriptCat 不适用 - 有必要存在的用法 +// 不是所有 @include 都要改为 @match。改用自定义处理 +config.rules["userscripts/better-use-match"] = []; +// 不是 @name @name:en @name:zh-CN @name:zh-TW @name:ja 都要放在最前。这个连 warning 也很无谓 +config.rules["userscripts/require-name"] = []; +// ScriptCat 不用指定 ==UserScript== 放最前。在 ==UserScript== 前面可以写其他注释, 例如是 License。 不视为 invalid +config.rules["userscripts/no-invalid-metadata"] = []; + // 以文本形式导出默认规则 export const defaultConfig = JSON.stringify(config, null, 2); diff --git a/src/linter.worker.ts b/src/linter.worker.ts index 78ae72e47..3ef01daf2 100644 --- a/src/linter.worker.ts +++ b/src/linter.worker.ts @@ -5,29 +5,9 @@ import { rules } from "eslint-plugin-userscripts"; const linter = new Linter({ configType: "eslintrc" }); -// ScriptCat 不适用 - 有必要存在的用法 -const omitKeys = new Set([ - // 不是所有 @include 都要改为 @match - "better-use-match", - // 不是 @name @name:en @name:zh-CN @name:zh-TW @name:ja 都要放在最前 - "require-name", - // ScriptCat 不用指定 ==UserScript== 放最前。在 ==UserScript== 前面可以写其他注释, 例如是 License - "no-invalid-metadata", -]); - // 额外定义 userscripts 规则 const formatRules: typeof rules = Object.fromEntries( - Object.entries(rules).map(([key, metas]) => [ - "userscripts/" + key, - omitKeys.has(key) - ? { - meta: {}, - create() { - return { CallExpression() {} }; - }, - } - : metas, - ]) + Object.entries(rules).map(([key, metas]) => ["userscripts/" + key, metas]) ); linter.defineRules(formatRules); From c3f3052beaba4d44da48ca65d5413af33e4d723a Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 24 May 2026 07:09:10 +0900 Subject: [PATCH 24/29] =?UTF-8?q?=E7=94=B1=20`userscripts/align-attributes?= =?UTF-8?q?`=20=E6=94=B9=E6=88=90=20`scriptcat/align-metadata-attributes`?= =?UTF-8?q?=20=EF=BC=88=E4=B8=8D=E5=BC=BA=E5=88=B6=E7=A9=BA=E6=A0=BC?= =?UTF-8?q?=E6=95=B0=E9=87=8F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/eslint/linter-config.ts | 4 +- src/pkg/utils/monaco-editor/index.ts | 189 +++++++++++++++++++++++++++ 2 files changed, 192 insertions(+), 1 deletion(-) diff --git a/packages/eslint/linter-config.ts b/packages/eslint/linter-config.ts index 10d87116a..01a3c528e 100644 --- a/packages/eslint/linter-config.ts +++ b/packages/eslint/linter-config.ts @@ -89,7 +89,9 @@ const config = { }; // 调整规则 -config.rules["userscripts/align-attributes"] = ["warn", 2]; +// ScriptCat 在 Monaco 侧用自定义检查处理 metadata 对齐: +// 只要求 value 起始列一致,不要求固定空格数。 +config.rules["userscripts/align-attributes"] = []; config.rules["userscripts/require-download-url"] = ["warn"]; // ScriptCat 不适用 - 有必要存在的用法 diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index 328c1ac76..349340f7c 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -29,6 +29,27 @@ type MetadataLineFix = { type TextEdit = languages.IWorkspaceTextEdit["textEdit"]; +type MetadataAlignmentLine = { + lineNumber: number; + lineText: string; + prefix: string; + tag: string; + spacing: string; + value: string; + valueColumn: number; +}; + +type MetadataAlignmentBlock = { + startLineNumber: number; + endLineNumber: number; + lines: MetadataAlignmentLine[]; +}; + +type MetadataAlignmentFix = { + range: TextEdit["range"]; + text: string; +}; + type ScriptcatMonacoEnvironment = typeof window.MonacoEnvironment & { myLinterWorker?: ILinterWorker; eslintFixMap?: Map; @@ -91,10 +112,14 @@ let isEditorRegistered = false; const scriptcatMarkerOwner = "ScriptCat"; const eslintMarkerOwner = "ESLint"; +const scriptcatMetadataAlignmentRuleId = "scriptcat/align-metadata-attributes"; const quickfixKind = "quickfix"; const noop = () => {}; const metaLinePattern = /\/\/[ \t]*@(\S+)[ \t]*(.*)$/; const metadataFixPattern = /^(\s*\/\/[ \t]*@)(connect|match|include)([ \t]+)(\S+)(.*)$/i; +const metadataAlignmentPattern = /^(\s*\/\/[ \t]*@)(\S+)([ \t]+)(.*)$/; +const userscriptHeaderPattern = /^\s*\/\/[ \t]*==UserScript==[ \t]*$/; +const userscriptEndPattern = /^\s*\/\/[ \t]*==\/UserScript==[ \t]*$/; const matchMetadataPattern = /^(\*|[-a-z]+|http\*):\/\/([^/]+)(\/.*)?$/i; const noUndefMessagePattern = /^[^']*'([^']+)'[^']*$/; @@ -128,6 +153,28 @@ const createTextEditAction = ( } satisfies languages.CodeAction; }; +const createTextEditsAction = ( + model: editor.ITextModel, + title: string, + diagnostics: editor.IMarkerData[], + textEdits: TextEdit[], + isPreferred: boolean +) => { + return { + title, + diagnostics, + kind: quickfixKind, + edit: { + edits: textEdits.map((textEdit) => ({ + resource: model.uri, + textEdit, + versionId: undefined, + })), + }, + isPreferred, + } satisfies languages.CodeAction; +}; + const createLineReplacementAction = ( model: editor.ITextModel, title: string, @@ -311,6 +358,133 @@ const getMetadataLineActions = ( ); }; +const getMetadataAlignmentLine = (lineNumber: number, lineText: string): MetadataAlignmentLine | null => { + const match = metadataAlignmentPattern.exec(lineText); + if (!match) return null; + + const [, prefix, tag, spacing, value] = match; + return { + lineNumber, + lineText, + prefix, + tag, + spacing, + value, + valueColumn: prefix.length + tag.length + spacing.length, + }; +}; + +const getMetadataAlignmentBlocks = (model: editor.ITextModel): MetadataAlignmentBlock[] => { + const blocks: MetadataAlignmentBlock[] = []; + const lineCount = model.getLineCount(); + let currentBlock: MetadataAlignmentBlock | null = null; + + const finishBlock = (endLineNumber: number) => { + if (!currentBlock) return; + currentBlock.endLineNumber = endLineNumber; + blocks.push(currentBlock); + currentBlock = null; + }; + + for (let lineNumber = 1; lineNumber <= lineCount; lineNumber += 1) { + const lineText = model.getLineContent(lineNumber); + + if (userscriptHeaderPattern.test(lineText)) { + finishBlock(lineNumber - 1); + currentBlock = { + startLineNumber: lineNumber, + endLineNumber: lineNumber, + lines: [], + }; + continue; + } + + if (!currentBlock) continue; + + const alignmentLine = getMetadataAlignmentLine(lineNumber, lineText); + if (alignmentLine) { + currentBlock.lines.push(alignmentLine); + } + + if (userscriptEndPattern.test(lineText)) { + finishBlock(lineNumber); + } + } + + finishBlock(lineCount); + return blocks; +}; + +const getMetadataAlignmentTargetColumn = (lines: MetadataAlignmentLine[]) => + Math.max(...lines.map((line) => line.prefix.length + line.tag.length + 1)); + +const isMetadataAlignmentBlockAligned = (block: MetadataAlignmentBlock) => { + if (block.lines.length < 2) return true; + const firstValueColumn = block.lines[0].valueColumn; + return block.lines.every((line) => line.valueColumn === firstValueColumn); +}; + +const getMetadataAlignmentFix = (model: editor.ITextModel, block: MetadataAlignmentBlock): MetadataAlignmentFix => { + const targetColumn = getMetadataAlignmentTargetColumn(block.lines); + const lineFixes = new Map( + block.lines.map((line) => { + const spacing = " ".repeat(Math.max(1, targetColumn - line.prefix.length - line.tag.length)); + return [line.lineNumber, `${line.prefix}${line.tag}${spacing}${line.value}`]; + }) + ); + const blockLines: string[] = []; + + for (let lineNumber = block.startLineNumber; lineNumber <= block.endLineNumber; lineNumber += 1) { + blockLines.push(lineFixes.get(lineNumber) ?? model.getLineContent(lineNumber)); + } + + return { + range: { + startLineNumber: block.startLineNumber, + startColumn: 1, + endLineNumber: block.endLineNumber, + endColumn: model.getLineContent(block.endLineNumber).length + 1, + }, + text: blockLines.join("\n"), + }; +}; + +const getMetadataAlignmentBlockAtLine = (model: editor.ITextModel, lineNumber: number) => + getMetadataAlignmentBlocks(model).find( + (block) => + block.startLineNumber <= lineNumber && + lineNumber <= block.endLineNumber && + !isMetadataAlignmentBlockAligned(block) + ); + +const getMetadataAlignmentActions = ( + model: editor.ITextModel, + lineNumber: number, + markers: editor.IMarkerData[] +): languages.CodeAction[] => { + const alignmentMarkers = markers.filter( + (marker) => + marker.source === scriptcatMarkerOwner && + getMarkerCode(marker) === scriptcatMetadataAlignmentRuleId && + marker.startLineNumber <= lineNumber && + lineNumber <= marker.endLineNumber + ); + if (alignmentMarkers.length === 0) return []; + + const block = getMetadataAlignmentBlockAtLine(model, lineNumber); + if (!block) return []; + + return [ + createTextEditsAction( + model, + currentEditorLang.quickfix.replace("{0}", scriptcatMetadataAlignmentRuleId), + alignmentMarkers, + [getMetadataAlignmentFix(model, block)], + true + ), + ]; +}; + const getNoUndefGlobalName = (marker: editor.IMarkerData) => { return noUndefMessagePattern.exec(marker.message)?.[1] || null; }; @@ -437,6 +611,20 @@ const updateScriptcatMetadataMarkers = (model: editor.ITextModel) => { if (model.getLanguageId() !== "javascript") return; const markers: editor.IMarkerData[] = []; + for (const block of getMetadataAlignmentBlocks(model)) { + if (isMetadataAlignmentBlockAligned(block)) continue; + markers.push({ + severity: MarkerSeverity.Warning, + message: currentEditorLang.quickfix.replace("{0}", scriptcatMetadataAlignmentRuleId), + source: scriptcatMarkerOwner, + code: scriptcatMetadataAlignmentRuleId, + startLineNumber: block.startLineNumber, + startColumn: 1, + endLineNumber: block.endLineNumber, + endColumn: model.getLineContent(block.endLineNumber).length + 1, + }); + } + const lineCount = model.getLineCount(); for (let lineNumber = 1; lineNumber <= lineCount; lineNumber += 1) { const lineText = model.getLineContent(lineNumber); @@ -542,6 +730,7 @@ export function registerEditor() { const lineText = model.getLineContent(range.startLineNumber); const actions = [ ...getMetadataLineActions(model, range.startLineNumber, lineText, context.markers), + ...getMetadataAlignmentActions(model, range.startLineNumber, context.markers), ...context.markers.flatMap((marker) => getMarkerCodeActions(model, marker, eslintFixMap)), ]; From eee87d439f18057f39c93bdd8d53cb1296dbb1e5 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 24 May 2026 07:13:37 +0900 Subject: [PATCH 25/29] =?UTF-8?q?=E8=AE=A9=20action=20=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E5=AF=B9=E5=BA=94=20code=20=E7=9A=84=20diagnostic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pkg/utils/monaco-editor/index.ts | 51 ++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index 349340f7c..f2b9fddd9 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -23,6 +23,7 @@ type MetadataLineParts = { type MetadataTag = "connect" | "match" | "include"; type MetadataLineFix = { + code: string; title: string; text: string; }; @@ -113,6 +114,9 @@ let isEditorRegistered = false; const scriptcatMarkerOwner = "ScriptCat"; const eslintMarkerOwner = "ESLint"; const scriptcatMetadataAlignmentRuleId = "scriptcat/align-metadata-attributes"; +const scriptcatRemoveConnectWildcardRuleId = "scriptcat/remove-connect-wildcard"; +const scriptcatReplaceMatchTldWildcardRuleId = "scriptcat/replace-match-tld-wildcard-with-include"; +const scriptcatReplaceIncludeWithMatchRuleId = "scriptcat/replace-include-with-match"; const quickfixKind = "quickfix"; const noop = () => {}; const metaLinePattern = /\/\/[ \t]*@(\S+)[ \t]*(.*)$/; @@ -227,8 +231,9 @@ const parseMetadataLine = (lineText: string): MetadataLineParts | null => { }; }; -const createMetadataFix = (titleTemplate: string, titleValue: string, text: string): MetadataLineFix => { +const createMetadataFix = (code: string, titleTemplate: string, titleValue: string, text: string): MetadataLineFix => { return { + code, title: titleTemplate.replace("{0}", titleValue), text, }; @@ -254,7 +259,14 @@ const getConnectMetadataFixes = ({ prefix, tag, spacing, value, suffix }: Metada if (!/\.\w{2,}$/.test(hostName) || !isSimpleValidHost(hostName)) return []; const titleTemplate = currentEditorLang.removeConnectWildcard; - return [createMetadataFix(titleTemplate, hostName, `${prefix}${tag}${spacing}${hostName}${suffix}`)]; + return [ + createMetadataFix( + scriptcatRemoveConnectWildcardRuleId, + titleTemplate, + hostName, + `${prefix}${tag}${spacing}${hostName}${suffix}` + ), + ]; }; const getMatchMetadataFixes = ({ @@ -285,8 +297,18 @@ const getMatchMetadataFixes = ({ const titleTemplate = currentEditorLang.replaceMatchTldWildcardWithInclude; return [ - createMetadataFix(titleTemplate, tldValue, `${prefix}include${includeSpacing}${tldValue}${suffix}`), - createMetadataFix(titleTemplate, value, `${prefix}include${includeSpacing}${value}${suffix}`), + createMetadataFix( + scriptcatReplaceMatchTldWildcardRuleId, + titleTemplate, + tldValue, + `${prefix}include${includeSpacing}${tldValue}${suffix}` + ), + createMetadataFix( + scriptcatReplaceMatchTldWildcardRuleId, + titleTemplate, + value, + `${prefix}include${includeSpacing}${value}${suffix}` + ), ]; }; @@ -311,7 +333,14 @@ const getIncludeMetadataFixes = ({ if (wildcardNormalizedHost.split(".").every((hostSegment) => hostSegment === "*" || /^[\w-]+$/.test(hostSegment))) { const includeSpacing = getIncludeSpacing(spacing, normalizedTag); const titleTemplate = currentEditorLang.replaceIncludeWithMatch; - return [createMetadataFix(titleTemplate, value, `${prefix}match ${includeSpacing}${value}${suffix}`)]; + return [ + createMetadataFix( + scriptcatReplaceIncludeWithMatchRuleId, + titleTemplate, + value, + `${prefix}match ${includeSpacing}${value}${suffix}` + ), + ]; } return []; }; @@ -341,15 +370,16 @@ const getMetadataLineActions = ( const metadataFixes = getMetadataLineFixes(lineText); if (metadataFixes.length === 0) return []; - const diagnostics = markers.filter( - (marker) => marker.source === scriptcatMarkerOwner && marker.startLineNumber === lineNumber - ); - return metadataFixes.map((metadataFix, index) => createLineReplacementAction( model, metadataFix.title, - diagnostics, + markers.filter( + (marker) => + marker.source === scriptcatMarkerOwner && + marker.startLineNumber === lineNumber && + getMarkerCode(marker) === metadataFix.code + ), lineNumber, lineText, metadataFix.text, @@ -635,6 +665,7 @@ const updateScriptcatMetadataMarkers = (model: editor.ITextModel) => { severity: MarkerSeverity.Warning, message: metadataLineFixes[0].title, source: scriptcatMarkerOwner, + code: metadataLineFixes[0].code, startLineNumber: lineNumber, startColumn: 1, endLineNumber: lineNumber, From d8eadf3eda56d5ec38c66db93a2ca4f9e88dd95c Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 24 May 2026 07:17:31 +0900 Subject: [PATCH 26/29] fix `.tld` fix --- src/pkg/utils/monaco-editor/index.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index f2b9fddd9..151134d8b 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -296,20 +296,26 @@ const getMatchMetadataFixes = ({ const tldValue = `${metadataValueMatch[1]}://${hostName}.tld${metadataValueMatch[3] || ""}`; const titleTemplate = currentEditorLang.replaceMatchTldWildcardWithInclude; - return [ - createMetadataFix( - scriptcatReplaceMatchTldWildcardRuleId, - titleTemplate, - tldValue, - `${prefix}include${includeSpacing}${tldValue}${suffix}` - ), + const actions = []; + if (hostPattern.endsWith(".*")) { + actions.push( + createMetadataFix( + scriptcatReplaceMatchTldWildcardRuleId, + titleTemplate, + tldValue, + `${prefix}include${includeSpacing}${tldValue}${suffix}` + ) + ); + } + actions.push( createMetadataFix( scriptcatReplaceMatchTldWildcardRuleId, titleTemplate, value, `${prefix}include${includeSpacing}${value}${suffix}` - ), - ]; + ) + ); + return actions; }; const getIncludeMetadataFixes = ({ From 1ccfcfcd8e11cc07c6103b86691e83afad0ad012 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 24 May 2026 07:39:42 +0900 Subject: [PATCH 27/29] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20getGrantValueHoverPr?= =?UTF-8?q?ompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pkg/utils/monaco-editor/index.ts | 49 +++++ src/pkg/utils/monaco-editor/langs.ts | 304 +++++++++++++++++++++++++++ 2 files changed, 353 insertions(+) diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index 151134d8b..28efa7106 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -120,6 +120,7 @@ const scriptcatReplaceIncludeWithMatchRuleId = "scriptcat/replace-include-with-m const quickfixKind = "quickfix"; const noop = () => {}; const metaLinePattern = /\/\/[ \t]*@(\S+)[ \t]*(.*)$/; +const metadataHoverPattern = /^(\s*\/\/[ \t]*@)(\S+)([ \t]*)(.*)$/; const metadataFixPattern = /^(\s*\/\/[ \t]*@)(connect|match|include)([ \t]+)(\S+)(.*)$/i; const metadataAlignmentPattern = /^(\s*\/\/[ \t]*@)(\S+)([ \t]+)(.*)$/; const userscriptHeaderPattern = /^\s*\/\/[ \t]*==UserScript==[ \t]*$/; @@ -139,6 +140,42 @@ const getMarkerCode = (marker: editor.IMarkerData) => { return typeof marker.code === "string" ? marker.code : marker.code.value; }; +const normalizeGrantValue = (grantValue: string) => { + switch (grantValue) { + case "GM.xmlHttpRequest": + return "GM_xmlhttpRequest"; + case "GM.cookie": + return "GM_cookie"; + default: + return grantValue.startsWith("GM.") ? grantValue.replace("GM.", "GM_") : grantValue; + } +}; + +const getGrantValueHoverPrompt = (lineText: string, column: number) => { + const match = metadataHoverPattern.exec(lineText); + if (!match) return null; + + const [, prefix, tag, spacing, value] = match; + if (tag.toLowerCase() !== "grant") return null; + + const grantValueMatch = /^\S+/.exec(value); + if (!grantValueMatch) return null; + + const valueStartColumn = prefix.length + tag.length + spacing.length + 1; + const valueEndColumn = valueStartColumn + grantValueMatch[0].length; + if (column < valueStartColumn || column > valueEndColumn) return null; + + const grantValue = grantValueMatch[0]; + const prompt = + currentEditorLang.grantValuePrompts[grantValue as keyof typeof currentEditorLang.grantValuePrompts] ?? + currentEditorLang.grantValuePrompts[ + normalizeGrantValue(grantValue) as keyof typeof currentEditorLang.grantValuePrompts + ]; + if (!prompt) return null; + + return `\`${grantValue}\`
${prompt}`; +}; + const createTextEditAction = ( model: editor.ITextModel, title: string, @@ -737,6 +774,18 @@ export function registerEditor() { languages.registerHoverProvider("javascript", { provideHover: (model, position) => { const lineText = model.getLineContent(position.lineNumber); + const grantValuePrompt = getGrantValueHoverPrompt(lineText, position.column); + if (grantValuePrompt) { + return { + contents: [ + { + value: grantValuePrompt, + supportHtml: true, + }, + ], + }; + } + const metadataCommentMatch = metaLinePattern.exec(lineText); if (metadataCommentMatch) { diff --git a/src/pkg/utils/monaco-editor/langs.ts b/src/pkg/utils/monaco-editor/langs.ts index ed2147e7e..510e485ee 100644 --- a/src/pkg/utils/monaco-editor/langs.ts +++ b/src/pkg/utils/monaco-editor/langs.ts @@ -1,3 +1,300 @@ +const grantValuePromptsZhCN = { + none: "不申请特殊 GM API 权限,脚本会以接近普通页面脚本的方式运行。", + unsafeWindow: "访问页面自身的 window 对象,用于和网页原生脚本交互。", + GM_getValue: "读取脚本持久化存储中的单个值。", + GM_getValues: "批量读取脚本持久化存储中的多个值。", + GM_setValue: "写入脚本持久化存储中的单个值。", + GM_setValues: "批量写入脚本持久化存储中的多个值。", + GM_deleteValue: "删除脚本持久化存储中的单个值。", + GM_deleteValues: "批量删除脚本持久化存储中的多个值。", + GM_listValues: "列出脚本持久化存储中的所有键名。", + GM_addValueChangeListener: "监听脚本存储值的变化。", + GM_removeValueChangeListener: "移除脚本存储值变化监听器。", + GM_xmlhttpRequest: "发起跨域网络请求;请求目标通常需要配合 @connect 声明允许的域名。", + GM_download: + "下载文件。支持传入 URL 和文件名,或传入包含 url、name、headers、saveAs 等字段的详情对象,并返回可 abort 的句柄。", + GM_openInTab: "打开新标签页,并可控制前后台打开等选项。", + GM_closeInTab: "关闭由脚本打开或管理的标签页。", + GM_getTab: "读取当前标签页关联的临时数据。", + GM_saveTab: "保存当前标签页关联的临时数据。", + GM_getTabs: "读取脚本保存的所有标签页临时数据。", + GM_notification: "显示浏览器通知,并可处理点击、关闭等回调。", + GM_closeNotification: "关闭指定的脚本通知。", + GM_updateNotification: "更新指定的脚本通知内容。", + GM_setClipboard: "写入系统剪贴板。", + GM_registerMenuCommand: "注册脚本菜单命令。", + GM_unregisterMenuCommand: "取消注册脚本菜单命令。", + CAT_registerMenuInput: "ScriptCat 扩展 API:注册带输入框的脚本菜单命令。", + CAT_unregisterMenuInput: "ScriptCat 扩展 API:取消注册带输入框的脚本菜单命令。", + GM_addStyle: "向页面注入 CSS 样式。", + GM_addElement: "向页面创建并插入元素。", + GM_getResourceText: "读取 @resource 声明资源的文本内容。", + GM_getResourceURL: "获取 @resource 声明资源的 URL。", + GM_cookie: "访问 Cookie API,用于读取、写入或删除 Cookie。", + CAT_fetchBlob: "ScriptCat 内部扩展 API:读取扩展侧可访问资源并返回 Blob。", + CAT_fileStorage: "ScriptCat 扩展 API:访问脚本文件存储能力。", + CAT_userConfig: "ScriptCat 扩展 API:访问脚本用户配置。", + CAT_scriptLoaded: "ScriptCat 扩展 API:在 @early-start 场景下等待脚本完整加载完成。", + "window.close": "允许脚本调用 window.close()。", + "window.focus": "允许脚本调用 window.focus()。", + "window.onurlchange": "允许脚本监听 URL 变化事件。", +} as const; + +const grantValuePromptsEnUS = { + none: "Request no special GM API permissions; the script runs more like a regular page script.", + unsafeWindow: "Access the page's own window object for interaction with native page scripts.", + GM_getValue: "Read one value from the script's persistent storage.", + GM_getValues: "Read multiple values from the script's persistent storage.", + GM_setValue: "Write one value to the script's persistent storage.", + GM_setValues: "Write multiple values to the script's persistent storage.", + GM_deleteValue: "Delete one value from the script's persistent storage.", + GM_deleteValues: "Delete multiple values from the script's persistent storage.", + GM_listValues: "List all keys in the script's persistent storage.", + GM_addValueChangeListener: "Listen for changes to script storage values.", + GM_removeValueChangeListener: "Remove a script storage value change listener.", + GM_xmlhttpRequest: "Make cross-origin network requests; target hosts usually need to be allowed with @connect.", + GM_download: + "Download files. Accepts a URL and filename, or a details object with fields such as url, name, headers, and saveAs, and returns an abortable handle.", + GM_openInTab: "Open a new tab, with options such as foreground or background opening.", + GM_closeInTab: "Close a tab opened or managed by the script.", + GM_getTab: "Read temporary data associated with the current tab.", + GM_saveTab: "Save temporary data associated with the current tab.", + GM_getTabs: "Read all temporary tab data saved by the script.", + GM_notification: "Show a browser notification and handle events such as click or close.", + GM_closeNotification: "Close a specific script notification.", + GM_updateNotification: "Update a specific script notification.", + GM_setClipboard: "Write to the system clipboard.", + GM_registerMenuCommand: "Register a script menu command.", + GM_unregisterMenuCommand: "Unregister a script menu command.", + CAT_registerMenuInput: "ScriptCat API: register a script menu command with an input field.", + CAT_unregisterMenuInput: "ScriptCat API: unregister a script menu command with an input field.", + GM_addStyle: "Inject CSS into the page.", + GM_addElement: "Create and insert an element into the page.", + GM_getResourceText: "Read the text content of a resource declared with @resource.", + GM_getResourceURL: "Get the URL of a resource declared with @resource.", + GM_cookie: "Access the Cookie API to read, write, or delete cookies.", + CAT_fetchBlob: "ScriptCat internal API: read an extension-side accessible resource and return a Blob.", + CAT_fileStorage: "ScriptCat API: access script file storage.", + CAT_userConfig: "ScriptCat API: access script user configuration.", + CAT_scriptLoaded: "ScriptCat API: wait until the script is fully loaded in @early-start scenarios.", + "window.close": "Allow the script to call window.close().", + "window.focus": "Allow the script to call window.focus().", + "window.onurlchange": "Allow the script to listen for URL change events.", +} as const; + +const grantValuePromptsZhTW = { + none: "不申請特殊 GM API 權限,腳本會以接近一般頁面腳本的方式執行。", + unsafeWindow: "存取頁面自身的 window 物件,用於和網頁原生腳本互動。", + GM_getValue: "讀取腳本持久化儲存中的單一值。", + GM_getValues: "批次讀取腳本持久化儲存中的多個值。", + GM_setValue: "寫入腳本持久化儲存中的單一值。", + GM_setValues: "批次寫入腳本持久化儲存中的多個值。", + GM_deleteValue: "刪除腳本持久化儲存中的單一值。", + GM_deleteValues: "批次刪除腳本持久化儲存中的多個值。", + GM_listValues: "列出腳本持久化儲存中的所有鍵名。", + GM_addValueChangeListener: "監聽腳本儲存值的變化。", + GM_removeValueChangeListener: "移除腳本儲存值變化監聽器。", + GM_xmlhttpRequest: "發起跨來源網路請求;目標主機通常需要配合 @connect 宣告允許的網域。", + GM_download: + "下載檔案。支援傳入 URL 與檔名,或傳入包含 url、name、headers、saveAs 等欄位的詳細物件,並回傳可 abort 的控制代碼。", + GM_openInTab: "開啟新分頁,並可控制前景或背景開啟等選項。", + GM_closeInTab: "關閉由腳本開啟或管理的分頁。", + GM_getTab: "讀取目前分頁關聯的暫存資料。", + GM_saveTab: "儲存目前分頁關聯的暫存資料。", + GM_getTabs: "讀取腳本儲存的所有分頁暫存資料。", + GM_notification: "顯示瀏覽器通知,並可處理點擊、關閉等回呼。", + GM_closeNotification: "關閉指定的腳本通知。", + GM_updateNotification: "更新指定的腳本通知內容。", + GM_setClipboard: "寫入系統剪貼簿。", + GM_registerMenuCommand: "註冊腳本選單命令。", + GM_unregisterMenuCommand: "取消註冊腳本選單命令。", + CAT_registerMenuInput: "ScriptCat 擴充 API:註冊帶輸入框的腳本選單命令。", + CAT_unregisterMenuInput: "ScriptCat 擴充 API:取消註冊帶輸入框的腳本選單命令。", + GM_addStyle: "向頁面注入 CSS 樣式。", + GM_addElement: "向頁面建立並插入元素。", + GM_getResourceText: "讀取 @resource 宣告資源的文字內容。", + GM_getResourceURL: "取得 @resource 宣告資源的 URL。", + GM_cookie: "存取 Cookie API,用於讀取、寫入或刪除 Cookie。", + CAT_fetchBlob: "ScriptCat 內部擴充 API:讀取擴充側可存取資源並回傳 Blob。", + CAT_fileStorage: "ScriptCat 擴充 API:存取腳本檔案儲存能力。", + CAT_userConfig: "ScriptCat 擴充 API:存取腳本使用者設定。", + CAT_scriptLoaded: "ScriptCat 擴充 API:在 @early-start 場景下等待腳本完整載入完成。", + "window.close": "允許腳本呼叫 window.close()。", + "window.focus": "允許腳本呼叫 window.focus()。", + "window.onurlchange": "允許腳本監聽 URL 變化事件。", +} as const; + +const grantValuePromptsJaJP = { + none: "特別な GM API 権限を要求せず、通常のページスクリプトに近い形で実行します。", + unsafeWindow: "ページ自身の window オブジェクトにアクセスし、ページのネイティブスクリプトと連携します。", + GM_getValue: "スクリプトの永続ストレージから 1 つの値を読み取ります。", + GM_getValues: "スクリプトの永続ストレージから複数の値をまとめて読み取ります。", + GM_setValue: "スクリプトの永続ストレージに 1 つの値を書き込みます。", + GM_setValues: "スクリプトの永続ストレージに複数の値をまとめて書き込みます。", + GM_deleteValue: "スクリプトの永続ストレージから 1 つの値を削除します。", + GM_deleteValues: "スクリプトの永続ストレージから複数の値をまとめて削除します。", + GM_listValues: "スクリプトの永続ストレージ内のすべてのキーを列挙します。", + GM_addValueChangeListener: "スクリプトのストレージ値の変更を監視します。", + GM_removeValueChangeListener: "ストレージ値変更リスナーを削除します。", + GM_xmlhttpRequest: + "クロスオリジンのネットワークリクエストを行います。対象ホストは通常 @connect で許可する必要があります。", + GM_download: + "ファイルをダウンロードします。URL とファイル名、または url、name、headers、saveAs などを含む詳細オブジェクトを受け取り、abort 可能なハンドルを返します。", + GM_openInTab: "新しいタブを開き、前面または背面で開くなどのオプションを指定できます。", + GM_closeInTab: "スクリプトが開いた、または管理しているタブを閉じます。", + GM_getTab: "現在のタブに関連付けられた一時データを読み取ります。", + GM_saveTab: "現在のタブに関連付けられた一時データを保存します。", + GM_getTabs: "スクリプトが保存したすべてのタブ一時データを読み取ります。", + GM_notification: "ブラウザー通知を表示し、クリックや閉じる操作などを処理できます。", + GM_closeNotification: "指定したスクリプト通知を閉じます。", + GM_updateNotification: "指定したスクリプト通知を更新します。", + GM_setClipboard: "システムクリップボードへ書き込みます。", + GM_registerMenuCommand: "スクリプトメニューコマンドを登録します。", + GM_unregisterMenuCommand: "スクリプトメニューコマンドの登録を解除します。", + CAT_registerMenuInput: "ScriptCat API: 入力欄付きのスクリプトメニューコマンドを登録します。", + CAT_unregisterMenuInput: "ScriptCat API: 入力欄付きのスクリプトメニューコマンドの登録を解除します。", + GM_addStyle: "ページに CSS スタイルを注入します。", + GM_addElement: "ページに要素を作成して挿入します。", + GM_getResourceText: "@resource で宣言されたリソースのテキスト内容を読み取ります。", + GM_getResourceURL: "@resource で宣言されたリソースの URL を取得します。", + GM_cookie: "Cookie API にアクセスし、Cookie の読み取り、書き込み、削除を行います。", + CAT_fetchBlob: "ScriptCat 内部 API: 拡張機能側でアクセス可能なリソースを読み取り Blob を返します。", + CAT_fileStorage: "ScriptCat API: スクリプトのファイルストレージへアクセスします。", + CAT_userConfig: "ScriptCat API: スクリプトのユーザー設定へアクセスします。", + CAT_scriptLoaded: "ScriptCat API: @early-start の場面でスクリプトが完全に読み込まれるまで待機します。", + "window.close": "スクリプトによる window.close() の呼び出しを許可します。", + "window.focus": "スクリプトによる window.focus() の呼び出しを許可します。", + "window.onurlchange": "スクリプトによる URL 変更イベントの監視を許可します。", +} as const; + +const grantValuePromptsDeDE = { + none: "Fordert keine speziellen GM-API-Berechtigungen an; das Skript läuft eher wie ein normales Seitenskript.", + unsafeWindow: "Greift auf das window-Objekt der Seite zu, um mit nativen Seitenskripten zu interagieren.", + GM_getValue: "Liest einen Wert aus dem persistenten Skriptspeicher.", + GM_getValues: "Liest mehrere Werte aus dem persistenten Skriptspeicher.", + GM_setValue: "Schreibt einen Wert in den persistenten Skriptspeicher.", + GM_setValues: "Schreibt mehrere Werte in den persistenten Skriptspeicher.", + GM_deleteValue: "Löscht einen Wert aus dem persistenten Skriptspeicher.", + GM_deleteValues: "Löscht mehrere Werte aus dem persistenten Skriptspeicher.", + GM_listValues: "Listet alle Schlüssel im persistenten Skriptspeicher auf.", + GM_addValueChangeListener: "Überwacht Änderungen an Skriptspeicherwerten.", + GM_removeValueChangeListener: "Entfernt einen Listener für Änderungen an Skriptspeicherwerten.", + GM_xmlhttpRequest: + "Führt Cross-Origin-Netzwerkanfragen aus; Zielhosts müssen normalerweise mit @connect erlaubt werden.", + GM_download: + "Lädt Dateien herunter. Akzeptiert URL und Dateiname oder ein Detailobjekt mit Feldern wie url, name, headers und saveAs und gibt ein abbrechbares Handle zurück.", + GM_openInTab: "Öffnet einen neuen Tab mit Optionen wie Öffnen im Vorder- oder Hintergrund.", + GM_closeInTab: "Schließt einen vom Skript geöffneten oder verwalteten Tab.", + GM_getTab: "Liest temporäre Daten, die dem aktuellen Tab zugeordnet sind.", + GM_saveTab: "Speichert temporäre Daten, die dem aktuellen Tab zugeordnet sind.", + GM_getTabs: "Liest alle vom Skript gespeicherten temporären Tabdaten.", + GM_notification: "Zeigt eine Browserbenachrichtigung an und verarbeitet Ereignisse wie Klick oder Schließen.", + GM_closeNotification: "Schließt eine bestimmte Skriptbenachrichtigung.", + GM_updateNotification: "Aktualisiert eine bestimmte Skriptbenachrichtigung.", + GM_setClipboard: "Schreibt in die Systemzwischenablage.", + GM_registerMenuCommand: "Registriert einen Skript-Menübefehl.", + GM_unregisterMenuCommand: "Hebt die Registrierung eines Skript-Menübefehls auf.", + CAT_registerMenuInput: "ScriptCat-API: Registriert einen Skript-Menübefehl mit Eingabefeld.", + CAT_unregisterMenuInput: "ScriptCat-API: Hebt die Registrierung eines Skript-Menübefehls mit Eingabefeld auf.", + GM_addStyle: "Injiziert CSS-Stile in die Seite.", + GM_addElement: "Erstellt ein Element und fügt es in die Seite ein.", + GM_getResourceText: "Liest den Textinhalt einer mit @resource deklarierten Ressource.", + GM_getResourceURL: "Ruft die URL einer mit @resource deklarierten Ressource ab.", + GM_cookie: "Greift auf die Cookie-API zu, um Cookies zu lesen, zu schreiben oder zu löschen.", + CAT_fetchBlob: + "Interne ScriptCat-API: Liest eine erweiterungsseitig verfügbare Ressource und gibt einen Blob zurück.", + CAT_fileStorage: "ScriptCat-API: Zugriff auf den Dateispeicher des Skripts.", + CAT_userConfig: "ScriptCat-API: Zugriff auf die Benutzerkonfiguration des Skripts.", + CAT_scriptLoaded: "ScriptCat-API: Wartet in @early-start-Szenarien, bis das Skript vollständig geladen ist.", + "window.close": "Erlaubt dem Skript, window.close() aufzurufen.", + "window.focus": "Erlaubt dem Skript, window.focus() aufzurufen.", + "window.onurlchange": "Erlaubt dem Skript, URL-Änderungsereignisse zu überwachen.", +} as const; + +const grantValuePromptsViVN = { + none: "Không yêu cầu quyền GM API đặc biệt; script chạy gần giống script trang thông thường.", + unsafeWindow: "Truy cập đối tượng window thật của trang để tương tác với script gốc của trang.", + GM_getValue: "Đọc một giá trị từ bộ nhớ lưu trữ bền vững của script.", + GM_getValues: "Đọc nhiều giá trị từ bộ nhớ lưu trữ bền vững của script.", + GM_setValue: "Ghi một giá trị vào bộ nhớ lưu trữ bền vững của script.", + GM_setValues: "Ghi nhiều giá trị vào bộ nhớ lưu trữ bền vững của script.", + GM_deleteValue: "Xóa một giá trị khỏi bộ nhớ lưu trữ bền vững của script.", + GM_deleteValues: "Xóa nhiều giá trị khỏi bộ nhớ lưu trữ bền vững của script.", + GM_listValues: "Liệt kê tất cả khóa trong bộ nhớ lưu trữ bền vững của script.", + GM_addValueChangeListener: "Theo dõi thay đổi của giá trị trong bộ nhớ script.", + GM_removeValueChangeListener: "Gỡ bộ lắng nghe thay đổi giá trị trong bộ nhớ script.", + GM_xmlhttpRequest: "Gửi yêu cầu mạng cross-origin; host đích thường cần được cho phép bằng @connect.", + GM_download: + "Tải tệp xuống. Nhận URL và tên tệp, hoặc đối tượng chi tiết có các trường như url, name, headers, saveAs, và trả về handle có thể abort.", + GM_openInTab: "Mở tab mới, có thể chọn mở ở nền hoặc phía trước.", + GM_closeInTab: "Đóng tab do script mở hoặc quản lý.", + GM_getTab: "Đọc dữ liệu tạm thời gắn với tab hiện tại.", + GM_saveTab: "Lưu dữ liệu tạm thời gắn với tab hiện tại.", + GM_getTabs: "Đọc tất cả dữ liệu tab tạm thời mà script đã lưu.", + GM_notification: "Hiển thị thông báo trình duyệt và xử lý các sự kiện như nhấp hoặc đóng.", + GM_closeNotification: "Đóng một thông báo script cụ thể.", + GM_updateNotification: "Cập nhật một thông báo script cụ thể.", + GM_setClipboard: "Ghi vào clipboard hệ thống.", + GM_registerMenuCommand: "Đăng ký lệnh menu của script.", + GM_unregisterMenuCommand: "Hủy đăng ký lệnh menu của script.", + CAT_registerMenuInput: "API ScriptCat: đăng ký lệnh menu script có ô nhập.", + CAT_unregisterMenuInput: "API ScriptCat: hủy đăng ký lệnh menu script có ô nhập.", + GM_addStyle: "Chèn CSS vào trang.", + GM_addElement: "Tạo và chèn phần tử vào trang.", + GM_getResourceText: "Đọc nội dung văn bản của tài nguyên khai báo bằng @resource.", + GM_getResourceURL: "Lấy URL của tài nguyên khai báo bằng @resource.", + GM_cookie: "Truy cập Cookie API để đọc, ghi hoặc xóa cookie.", + CAT_fetchBlob: "API nội bộ ScriptCat: đọc tài nguyên có thể truy cập từ phía tiện ích và trả về Blob.", + CAT_fileStorage: "API ScriptCat: truy cập bộ nhớ tệp của script.", + CAT_userConfig: "API ScriptCat: truy cập cấu hình người dùng của script.", + CAT_scriptLoaded: "API ScriptCat: chờ script tải hoàn tất trong tình huống @early-start.", + "window.close": "Cho phép script gọi window.close().", + "window.focus": "Cho phép script gọi window.focus().", + "window.onurlchange": "Cho phép script lắng nghe sự kiện thay đổi URL.", +} as const; + +const grantValuePromptsRuRU = { + none: "Не запрашивает специальные права GM API; скрипт работает ближе к обычному скрипту страницы.", + unsafeWindow: "Доступ к собственному объекту window страницы для взаимодействия с нативными скриптами страницы.", + GM_getValue: "Читает одно значение из постоянного хранилища скрипта.", + GM_getValues: "Читает несколько значений из постоянного хранилища скрипта.", + GM_setValue: "Записывает одно значение в постоянное хранилище скрипта.", + GM_setValues: "Записывает несколько значений в постоянное хранилище скрипта.", + GM_deleteValue: "Удаляет одно значение из постоянного хранилища скрипта.", + GM_deleteValues: "Удаляет несколько значений из постоянного хранилища скрипта.", + GM_listValues: "Перечисляет все ключи в постоянном хранилище скрипта.", + GM_addValueChangeListener: "Отслеживает изменения значений в хранилище скрипта.", + GM_removeValueChangeListener: "Удаляет слушатель изменений значений в хранилище скрипта.", + GM_xmlhttpRequest: "Выполняет cross-origin сетевые запросы; целевые хосты обычно нужно разрешить через @connect.", + GM_download: + "Загружает файлы. Принимает URL и имя файла либо объект параметров с полями url, name, headers, saveAs и возвращает дескриптор с abort.", + GM_openInTab: "Открывает новую вкладку с параметрами, например в фоне или на переднем плане.", + GM_closeInTab: "Закрывает вкладку, открытую или управляемую скриптом.", + GM_getTab: "Читает временные данные, связанные с текущей вкладкой.", + GM_saveTab: "Сохраняет временные данные, связанные с текущей вкладкой.", + GM_getTabs: "Читает все временные данные вкладок, сохраненные скриптом.", + GM_notification: "Показывает уведомление браузера и обрабатывает события, например клик или закрытие.", + GM_closeNotification: "Закрывает указанное уведомление скрипта.", + GM_updateNotification: "Обновляет указанное уведомление скрипта.", + GM_setClipboard: "Записывает данные в системный буфер обмена.", + GM_registerMenuCommand: "Регистрирует команду меню скрипта.", + GM_unregisterMenuCommand: "Отменяет регистрацию команды меню скрипта.", + CAT_registerMenuInput: "API ScriptCat: регистрирует команду меню скрипта с полем ввода.", + CAT_unregisterMenuInput: "API ScriptCat: отменяет регистрацию команды меню скрипта с полем ввода.", + GM_addStyle: "Внедряет CSS-стили на страницу.", + GM_addElement: "Создает и вставляет элемент на страницу.", + GM_getResourceText: "Читает текстовое содержимое ресурса, объявленного через @resource.", + GM_getResourceURL: "Получает URL ресурса, объявленного через @resource.", + GM_cookie: "Доступ к Cookie API для чтения, записи или удаления cookie.", + CAT_fetchBlob: "Внутренний API ScriptCat: читает доступный со стороны расширения ресурс и возвращает Blob.", + CAT_fileStorage: "API ScriptCat: доступ к файловому хранилищу скрипта.", + CAT_userConfig: "API ScriptCat: доступ к пользовательской конфигурации скрипта.", + CAT_scriptLoaded: "API ScriptCat: ожидает полной загрузки скрипта в сценариях @early-start.", + "window.close": "Разрешает скрипту вызывать window.close().", + "window.focus": "Разрешает скрипту вызывать window.focus().", + "window.onurlchange": "Разрешает скрипту слушать события изменения URL.", +} as const; + export const editorLangs = { "zh-CN": { title: "简体中文", @@ -10,6 +307,7 @@ export const editorLangs = { removeConnectWildcard: "移除 @connect 通配符,改为 {0}", replaceMatchTldWildcardWithInclude: "将 @match 顶级域名通配符改为 @include {0}", replaceIncludeWithMatch: "将 @include 改为 @match {0}", + grantValuePrompts: grantValuePromptsZhCN, prompt: { name: "脚本名称", namespace: "脚本命名空间", @@ -96,6 +394,7 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), removeConnectWildcard: "Remove @connect wildcard: {0}", replaceMatchTldWildcardWithInclude: "Replace @match TLD wildcard with @include {0}", replaceIncludeWithMatch: "Replace @include with @match {0}", + grantValuePrompts: grantValuePromptsEnUS, prompt: { name: "Script name", namespace: "Script namespace", @@ -181,6 +480,7 @@ tracking: This script tracks your user information`.replace(/\n/g, "
"), removeConnectWildcard: "移除 @connect 萬用字元,改為 {0}", replaceMatchTldWildcardWithInclude: "將 @match 頂級網域萬用字元改為 @include {0}", replaceIncludeWithMatch: "將 @include 改為 @match {0}", + grantValuePrompts: grantValuePromptsZhTW, prompt: { name: "腳本名稱", namespace: "腳本命名空間", @@ -266,6 +566,7 @@ tracking:此腳本會追蹤您的使用者資訊`.replace(/\n/g, "
"), removeConnectWildcard: "@connect のワイルドカードを削除: {0}", replaceMatchTldWildcardWithInclude: "@match の TLD ワイルドカードを @include {0} に置換", replaceIncludeWithMatch: "@include を @match {0} に置換", + grantValuePrompts: grantValuePromptsJaJP, prompt: { name: "スクリプト名", namespace: "スクリプトの名前空間", @@ -351,6 +652,7 @@ tracking:このスクリプトはユーザー情報を追跡します`.replace removeConnectWildcard: "@connect-Wildcard entfernen: {0}", replaceMatchTldWildcardWithInclude: "@match-TLD-Wildcard durch @include {0} ersetzen", replaceIncludeWithMatch: "@include durch @match {0} ersetzen", + grantValuePrompts: grantValuePromptsDeDE, prompt: { name: "Skriptname", namespace: "Skript-Namensraum", @@ -437,6 +739,7 @@ tracking: Dieses Skript verfolgt Ihre Benutzerinformationen`.replace(/\n/g, "
Date: Sun, 24 May 2026 07:49:48 +0900 Subject: [PATCH 28/29] added `scriptcat/grant-none-conflict` --- src/pkg/utils/monaco-editor/index.ts | 37 ++++++++++++++++++++++++++++ src/pkg/utils/monaco-editor/langs.ts | 8 ++++++ 2 files changed, 45 insertions(+) diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index 28efa7106..70bf2c1d3 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -117,6 +117,7 @@ const scriptcatMetadataAlignmentRuleId = "scriptcat/align-metadata-attributes"; const scriptcatRemoveConnectWildcardRuleId = "scriptcat/remove-connect-wildcard"; const scriptcatReplaceMatchTldWildcardRuleId = "scriptcat/replace-match-tld-wildcard-with-include"; const scriptcatReplaceIncludeWithMatchRuleId = "scriptcat/replace-include-with-match"; +const scriptcatGrantNoneConflictRuleId = "scriptcat/grant-none-conflict"; const quickfixKind = "quickfix"; const noop = () => {}; const metaLinePattern = /\/\/[ \t]*@(\S+)[ \t]*(.*)$/; @@ -176,6 +177,8 @@ const getGrantValueHoverPrompt = (lineText: string, column: number) => { return `\`${grantValue}\`
${prompt}`; }; +const getMetadataValueToken = (value: string) => /^\S+/.exec(value)?.[0] || ""; + const createTextEditAction = ( model: editor.ITextModel, title: string, @@ -558,6 +561,38 @@ const getMetadataAlignmentActions = ( ]; }; +const getGrantNoneConflictMarkers = (model: editor.ITextModel): editor.IMarkerData[] => { + const markers: editor.IMarkerData[] = []; + + for (const block of getMetadataAlignmentBlocks(model)) { + const grantLines = block.lines + .map((line) => ({ + ...line, + grantValue: getMetadataValueToken(line.value), + })) + .filter((line) => line.tag.toLowerCase() === "grant" && line.grantValue); + const hasNone = grantLines.some((line) => line.grantValue === "none"); + const hasGmApi = grantLines.some((line) => line.grantValue.startsWith("GM")); + if (!hasNone || !hasGmApi) continue; + + for (const line of grantLines) { + if (line.grantValue !== "none" && !line.grantValue.startsWith("GM")) continue; + markers.push({ + severity: MarkerSeverity.Warning, + message: currentEditorLang.grantConflict, + source: scriptcatMarkerOwner, + code: scriptcatGrantNoneConflictRuleId, + startLineNumber: line.lineNumber, + startColumn: 1, + endLineNumber: line.lineNumber, + endColumn: line.lineText.length + 1, + }); + } + } + + return markers; +}; + const getNoUndefGlobalName = (marker: editor.IMarkerData) => { return noUndefMessagePattern.exec(marker.message)?.[1] || null; }; @@ -684,6 +719,8 @@ const updateScriptcatMetadataMarkers = (model: editor.ITextModel) => { if (model.getLanguageId() !== "javascript") return; const markers: editor.IMarkerData[] = []; + markers.push(...getGrantNoneConflictMarkers(model)); + for (const block of getMetadataAlignmentBlocks(model)) { if (isMetadataAlignmentBlockAligned(block)) continue; markers.push({ diff --git a/src/pkg/utils/monaco-editor/langs.ts b/src/pkg/utils/monaco-editor/langs.ts index 510e485ee..7805a4923 100644 --- a/src/pkg/utils/monaco-editor/langs.ts +++ b/src/pkg/utils/monaco-editor/langs.ts @@ -307,6 +307,7 @@ export const editorLangs = { removeConnectWildcard: "移除 @connect 通配符,改为 {0}", replaceMatchTldWildcardWithInclude: "将 @match 顶级域名通配符改为 @include {0}", replaceIncludeWithMatch: "将 @include 改为 @match {0}", + grantConflict: "@grant none 不能和 GM API 同时使用;请移除 none 或所有 GM API。", grantValuePrompts: grantValuePromptsZhCN, prompt: { name: "脚本名称", @@ -394,6 +395,7 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), removeConnectWildcard: "Remove @connect wildcard: {0}", replaceMatchTldWildcardWithInclude: "Replace @match TLD wildcard with @include {0}", replaceIncludeWithMatch: "Replace @include with @match {0}", + grantConflict: "@grant none cannot be used with GM APIs. Remove none or all GM APIs.", grantValuePrompts: grantValuePromptsEnUS, prompt: { name: "Script name", @@ -480,6 +482,7 @@ tracking: This script tracks your user information`.replace(/\n/g, "
"), removeConnectWildcard: "移除 @connect 萬用字元,改為 {0}", replaceMatchTldWildcardWithInclude: "將 @match 頂級網域萬用字元改為 @include {0}", replaceIncludeWithMatch: "將 @include 改為 @match {0}", + grantConflict: "@grant none 不能和 GM API 同時使用;請移除 none 或所有 GM API。", grantValuePrompts: grantValuePromptsZhTW, prompt: { name: "腳本名稱", @@ -566,6 +569,7 @@ tracking:此腳本會追蹤您的使用者資訊`.replace(/\n/g, "
"), removeConnectWildcard: "@connect のワイルドカードを削除: {0}", replaceMatchTldWildcardWithInclude: "@match の TLD ワイルドカードを @include {0} に置換", replaceIncludeWithMatch: "@include を @match {0} に置換", + grantConflict: "@grant none は GM API と同時に使えません。none またはすべての GM API を削除してください。", grantValuePrompts: grantValuePromptsJaJP, prompt: { name: "スクリプト名", @@ -652,6 +656,8 @@ tracking:このスクリプトはユーザー情報を追跡します`.replace removeConnectWildcard: "@connect-Wildcard entfernen: {0}", replaceMatchTldWildcardWithInclude: "@match-TLD-Wildcard durch @include {0} ersetzen", replaceIncludeWithMatch: "@include durch @match {0} ersetzen", + grantConflict: + "@grant none kann nicht zusammen mit GM-APIs verwendet werden. Entfernen Sie none oder alle GM-APIs.", grantValuePrompts: grantValuePromptsDeDE, prompt: { name: "Skriptname", @@ -739,6 +745,7 @@ tracking: Dieses Skript verfolgt Ihre Benutzerinformationen`.replace(/\n/g, "
Date: Sun, 24 May 2026 17:45:04 +0900 Subject: [PATCH 29/29] fix --- packages/eslint/linter-config.ts | 8 +- src/pkg/utils/monaco-editor/index.ts | 116 +++++++++++++++++++-------- 2 files changed, 87 insertions(+), 37 deletions(-) diff --git a/packages/eslint/linter-config.ts b/packages/eslint/linter-config.ts index 01a3c528e..2ca706ecb 100644 --- a/packages/eslint/linter-config.ts +++ b/packages/eslint/linter-config.ts @@ -91,16 +91,16 @@ const config = { // 调整规则 // ScriptCat 在 Monaco 侧用自定义检查处理 metadata 对齐: // 只要求 value 起始列一致,不要求固定空格数。 -config.rules["userscripts/align-attributes"] = []; +config.rules["userscripts/align-attributes"] = ["off"]; config.rules["userscripts/require-download-url"] = ["warn"]; // ScriptCat 不适用 - 有必要存在的用法 // 不是所有 @include 都要改为 @match。改用自定义处理 -config.rules["userscripts/better-use-match"] = []; +config.rules["userscripts/better-use-match"] = ["off"]; // 不是 @name @name:en @name:zh-CN @name:zh-TW @name:ja 都要放在最前。这个连 warning 也很无谓 -config.rules["userscripts/require-name"] = []; +config.rules["userscripts/require-name"] = ["off"]; // ScriptCat 不用指定 ==UserScript== 放最前。在 ==UserScript== 前面可以写其他注释, 例如是 License。 不视为 invalid -config.rules["userscripts/no-invalid-metadata"] = []; +config.rules["userscripts/no-invalid-metadata"] = ["off"]; // 以文本形式导出默认规则 export const defaultConfig = JSON.stringify(config, null, 2); diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index 70bf2c1d3..e85452353 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -124,6 +124,7 @@ const metaLinePattern = /\/\/[ \t]*@(\S+)[ \t]*(.*)$/; const metadataHoverPattern = /^(\s*\/\/[ \t]*@)(\S+)([ \t]*)(.*)$/; const metadataFixPattern = /^(\s*\/\/[ \t]*@)(connect|match|include)([ \t]+)(\S+)(.*)$/i; const metadataAlignmentPattern = /^(\s*\/\/[ \t]*@)(\S+)([ \t]+)(.*)$/; +const metadataLineStartPattern = /^\s*\/\/[ \t]*@/; const userscriptHeaderPattern = /^\s*\/\/[ \t]*==UserScript==[ \t]*$/; const userscriptEndPattern = /^\s*\/\/[ \t]*==\/UserScript==[ \t]*$/; const matchMetadataPattern = /^(\*|[-a-z]+|http\*):\/\/([^/]+)(\/.*)?$/i; @@ -281,7 +282,9 @@ const createMetadataFix = (code: string, titleTemplate: string, titleValue: stri const getIncludeSpacing = (spacing: string, tag: string) => { const lenDiff = "include".length - tag.length; - return lenDiff > 0 && spacing.length > lenDiff ? spacing.slice(0, -lenDiff) : spacing; + if (lenDiff <= 0) return spacing; + const targetLength = Math.max(1, spacing.length - lenDiff); + return spacing.slice(0, targetLength); }; const normalizeHost = (hostPattern: string) => { @@ -376,7 +379,7 @@ const getIncludeMetadataFixes = ({ hostPattern.endsWith(".tld") ) return []; - if (wildcardNormalizedHost.split(".").every((hostSegment) => hostSegment === "*" || /^[\w-]+$/.test(hostSegment))) { + if (isSimpleValidHost(wildcardNormalizedHost.replace(/\*/g, "x"))) { const includeSpacing = getIncludeSpacing(spacing, normalizedTag); const titleTemplate = currentEditorLang.replaceIncludeWithMatch; return [ @@ -561,22 +564,29 @@ const getMetadataAlignmentActions = ( ]; }; -const getGrantNoneConflictMarkers = (model: editor.ITextModel): editor.IMarkerData[] => { +const getGrantNoneConflictMarkers = (blocks: MetadataAlignmentBlock[]): editor.IMarkerData[] => { const markers: editor.IMarkerData[] = []; - for (const block of getMetadataAlignmentBlocks(model)) { - const grantLines = block.lines - .map((line) => ({ - ...line, - grantValue: getMetadataValueToken(line.value), - })) - .filter((line) => line.tag.toLowerCase() === "grant" && line.grantValue); - const hasNone = grantLines.some((line) => line.grantValue === "none"); - const hasGmApi = grantLines.some((line) => line.grantValue.startsWith("GM")); + for (const block of blocks) { + const grantLines: Array<{ line: MetadataAlignmentLine; grantValue: string }> = []; + let hasNone = false; + let hasGmApi = false; + + for (const line of block.lines) { + if (line.tag.toLowerCase() !== "grant") continue; + + const grantValue = getMetadataValueToken(line.value); + if (!grantValue) continue; + + grantLines.push({ line, grantValue }); + hasNone ||= grantValue === "none"; + hasGmApi ||= grantValue.startsWith("GM"); + } + if (!hasNone || !hasGmApi) continue; - for (const line of grantLines) { - if (line.grantValue !== "none" && !line.grantValue.startsWith("GM")) continue; + for (const { line, grantValue } of grantLines) { + if (grantValue !== "none" && !grantValue.startsWith("GM")) continue; markers.push({ severity: MarkerSeverity.Warning, message: currentEditorLang.grantConflict, @@ -715,13 +725,47 @@ const getMarkerCodeActions = ( return actions; }; +const lineCanAffectMetadataMarkers = (lineText: string) => + metadataLineStartPattern.test(lineText) || + userscriptHeaderPattern.test(lineText) || + userscriptEndPattern.test(lineText); + +const commentEditCanAffectMetadataMarkers = (lineText: string, change: editor.IModelContentChange) => + /^\s*\/\//.test(lineText) || (change.rangeLength > 0 && change.range.startColumn <= 12); + +const contentChangeCanAffectMetadataMarkers = (model: editor.ITextModel, event: editor.IModelContentChangedEvent) => { + if (event.isFlush || event.isEolChange) return true; + + for (const change of event.changes) { + if ( + change.range.startLineNumber !== change.range.endLineNumber || + change.text.includes("\n") || + change.text.includes("@") || + change.text.includes("UserScript") + ) { + return true; + } + + const lineText = model.getLineContent(change.range.startLineNumber); + if (lineCanAffectMetadataMarkers(lineText) || commentEditCanAffectMetadataMarkers(lineText, change)) { + return true; + } + } + + return false; +}; + const updateScriptcatMetadataMarkers = (model: editor.ITextModel) => { - if (model.getLanguageId() !== "javascript") return; + if (model.getLanguageId() !== "javascript") { + editor.setModelMarkers(model, scriptcatMarkerOwner, []); + return; + } + const metadataBlocks = getMetadataAlignmentBlocks(model); const markers: editor.IMarkerData[] = []; - markers.push(...getGrantNoneConflictMarkers(model)); + markers.push(...getGrantNoneConflictMarkers(metadataBlocks)); - for (const block of getMetadataAlignmentBlocks(model)) { + for (const block of metadataBlocks) { if (isMetadataAlignmentBlockAligned(block)) continue; markers.push({ severity: MarkerSeverity.Warning, @@ -735,22 +779,22 @@ const updateScriptcatMetadataMarkers = (model: editor.ITextModel) => { }); } - const lineCount = model.getLineCount(); - for (let lineNumber = 1; lineNumber <= lineCount; lineNumber += 1) { - const lineText = model.getLineContent(lineNumber); - const metadataLineFixes = getMetadataLineFixes(lineText); - if (metadataLineFixes.length === 0) continue; + for (const block of metadataBlocks) { + for (const line of block.lines) { + const metadataLineFixes = getMetadataLineFixes(line.lineText); + if (metadataLineFixes.length === 0) continue; - markers.push({ - severity: MarkerSeverity.Warning, - message: metadataLineFixes[0].title, - source: scriptcatMarkerOwner, - code: metadataLineFixes[0].code, - startLineNumber: lineNumber, - startColumn: 1, - endLineNumber: lineNumber, - endColumn: lineText.length + 1, - }); + markers.push({ + severity: MarkerSeverity.Warning, + message: metadataLineFixes[0].title, + source: scriptcatMarkerOwner, + code: metadataLineFixes[0].code, + startLineNumber: line.lineNumber, + startColumn: 1, + endLineNumber: line.lineNumber, + endColumn: line.lineText.length + 1, + }); + } } editor.setModelMarkers(model, scriptcatMarkerOwner, markers); @@ -759,7 +803,13 @@ const updateScriptcatMetadataMarkers = (model: editor.ITextModel) => { const registerScriptcatMetadataMarkerProvider = () => { const registerMetadataModel = (model: editor.ITextModel) => { updateScriptcatMetadataMarkers(model); - model.onDidChangeContent(() => { + model.onDidChangeContent((event) => { + if (model.getLanguageId() !== "javascript") return; + if (contentChangeCanAffectMetadataMarkers(model, event)) { + updateScriptcatMetadataMarkers(model); + } + }); + model.onDidChangeLanguage(() => { updateScriptcatMetadataMarkers(model); }); };